From 274b428db08381acac7dcdee60e597e7f6f35929 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Wed, 23 Feb 2022 16:54:23 +0530 Subject: [PATCH 01/86] Four year vesting contracts --- .../Vesting/fouryear/FourYearVesting.sol | 63 ++ .../fouryear/FourYearVestingFactory.sol | 46 + .../Vesting/fouryear/FourYearVestingLogic.sol | 297 ++++++ .../fouryear/FourYearVestingStorage.sol | 58 + .../Vesting/fouryear/IFourYearVesting.sol | 16 + .../fouryear/IFourYearVestingFactory.sol | 20 + tests/vesting/FourYearVesting.js | 989 ++++++++++++++++++ 7 files changed, 1489 insertions(+) create mode 100644 contracts/governance/Vesting/fouryear/FourYearVesting.sol create mode 100644 contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol create mode 100644 contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol create mode 100644 contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol create mode 100644 contracts/governance/Vesting/fouryear/IFourYearVesting.sol create mode 100644 contracts/governance/Vesting/fouryear/IFourYearVestingFactory.sol create mode 100644 tests/vesting/FourYearVesting.js diff --git a/contracts/governance/Vesting/fouryear/FourYearVesting.sol b/contracts/governance/Vesting/fouryear/FourYearVesting.sol new file mode 100644 index 000000000..a25598968 --- /dev/null +++ b/contracts/governance/Vesting/fouryear/FourYearVesting.sol @@ -0,0 +1,63 @@ +pragma solidity ^0.5.17; +pragma experimental ABIEncoderV2; + +import "../../../openzeppelin/Ownable.sol"; +import "../../../interfaces/IERC20.sol"; +import "../../Staking/Staking.sol"; +import "../../IFeeSharingProxy.sol"; +import "../../ApprovalReceiver.sol"; +import "./FourYearVestingStorage.sol"; +import "../../../proxy/Proxy.sol"; + +/** + * @title Four Year Vesting Contract. + * + * @notice A four year vesting contract. + * + * @dev Vesting contracts shouldn't be upgradable, + * use Proxy instead of UpgradableProxy. + * */ +contract FourYearVesting is FourYearVestingStorage, Proxy { + /** + * @notice Setup the vesting schedule. + * @param _logic The address of logic contract. + * @param _SOV The SOV token address. + * @param _tokenOwner The owner of the tokens. + * @param _cliff The time interval to the first withdraw in seconds. + * @param _duration The total duration in seconds. + * @param _feeSharingProxy Fee sharing proxy address. + * */ + constructor( + address _logic, + address _SOV, + address _stakingAddress, + address _tokenOwner, + uint256 _cliff, + uint256 _duration, + address _feeSharingProxy + ) public { + require(_SOV != address(0), "SOV address invalid"); + require(_stakingAddress != address(0), "staking address invalid"); + require(_tokenOwner != address(0), "token owner address invalid"); + require(_duration >= _cliff, "duration must be bigger than or equal to the cliff"); + require(_feeSharingProxy != address(0), "feeSharingProxy address invalid"); + + _setImplementation(_logic); + SOV = IERC20(_SOV); + staking = Staking(_stakingAddress); + require(_duration <= staking.MAX_DURATION(), "duration may not exceed the max duration"); + tokenOwner = _tokenOwner; + cliff = _cliff; + duration = _duration; + feeSharingProxy = IFeeSharingProxy(_feeSharingProxy); + maxDuration = 18 * FOUR_WEEKS; + } + + /** + * @dev We need to add this implementation to prevent proxy call FourYearVestingLogic.governanceWithdrawTokens + * @param receiver The receiver of the token withdrawal. + * */ + function governanceWithdrawTokens(address receiver) public { + revert("operation not supported"); + } +} diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol b/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol new file mode 100644 index 000000000..cc66b4572 --- /dev/null +++ b/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol @@ -0,0 +1,46 @@ +pragma solidity ^0.5.17; + +import "../../../openzeppelin/Ownable.sol"; +import "./FourYearVesting.sol"; +import "./IFourYearVestingFactory.sol"; + +/** + * @title Four Year Vesting Factory: Contract to deploy four year vesting contracts. + * @notice Factory pattern allows to create multiple instances + * of the same contract and keep track of them easier. + * */ +contract FourYearVestingFactory is IFourYearVestingFactory, Ownable { + address public fourYearVestingLogic; + + constructor(address _fourYearVestingLogic) public { + require(_fourYearVestingLogic != address(0), "invalid four year vesting logic address"); + fourYearVestingLogic = _fourYearVestingLogic; + } + + /** + * @notice Deploys four year vesting contract. + * @param _SOV the address of SOV token. + * @param _staking The address of staking contract. + * @param _tokenOwner The owner of the tokens. + * @param _cliff The time interval to the first withdraw in seconds. + * @param _duration The total duration in seconds. + * @param _feeSharing The address of fee sharing contract. + * @param _vestingOwner The address of an owner of vesting contract. + * @return The four year vesting contract address. + * */ + function deployFourYearVesting( + address _SOV, + address _staking, + address _tokenOwner, + uint256 _cliff, + uint256 _duration, + address _feeSharing, + address _vestingOwner + ) external onlyOwner returns (address) { + address fourYearVesting = address( + new FourYearVesting(fourYearVestingLogic, _SOV, _staking, _tokenOwner, _cliff, _duration, _feeSharing) + ); + Ownable(fourYearVesting).transferOwnership(_vestingOwner); + return fourYearVesting; + } +} diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol new file mode 100644 index 000000000..54f692779 --- /dev/null +++ b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol @@ -0,0 +1,297 @@ +pragma solidity ^0.5.17; +pragma experimental ABIEncoderV2; + +import "../../../openzeppelin/Ownable.sol"; +import "../../../interfaces/IERC20.sol"; +import "../../Staking/Staking.sol"; +import "../../IFeeSharingProxy.sol"; +import "./IFourYearVesting.sol"; +import "../../ApprovalReceiver.sol"; +import "./FourYearVestingStorage.sol"; +import "../../../openzeppelin/SafeMath.sol"; + +/** + * @title Four Year Vesting Logic contract. + * @notice Staking, delegating and withdrawal functionality. + * @dev Deployed by FourYearVestingFactory contract. + * */ +contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, ApprovalReceiver { + using SafeMath for uint256; + + /* Events */ + event TokensStaked(address indexed caller, uint256 amount); + event VotesDelegated(address indexed caller, address delegatee); + event TokensWithdrawn(address indexed caller, address receiver); + event DividendsCollected(address indexed caller, address loanPoolToken, address receiver, uint32 maxCheckpoints); + event MigratedToNewStakingContract(address indexed caller, address newStakingContract); + + /* Modifiers */ + /** + * @dev Throws if called by any account other than the token owner or the contract owner. + */ + modifier onlyOwners() { + require(msg.sender == tokenOwner || isOwner(), "unauthorized"); + _; + } + + /** + * @dev Throws if called by any account other than the token owner. + */ + modifier onlyTokenOwner() { + require(msg.sender == tokenOwner, "unauthorized"); + _; + } + + /* Functions */ + + /** + * @notice Stakes tokens according to the vesting schedule. + * @param _amount The amount of tokens to stake. + * @param _restartStakeSchedule The time from which staking schedule restarts. + * The issue is that we can only stake tokens for a max duration. Thus, we need to restart + * from the lastSchedule. + * @return lastSchedule The max duration for which tokens were staked. + * @return remainingAmount The amount outstanding - to be staked. + * */ + function stakeTokens(uint256 _amount, uint256 _restartStakeSchedule) public returns (uint256 lastSchedule, uint256 remainingAmount) { + (lastSchedule, remainingAmount) = _stakeTokens(msg.sender, _amount, _restartStakeSchedule); + } + + /** + * @notice Stakes tokens according to the vesting schedule. + * @dev This function will be invoked from receiveApproval. + * @dev SOV.approveAndCall -> this.receiveApproval -> this.stakeTokensWithApproval + * @param _sender The sender of SOV.approveAndCall + * @param _amount The amount of tokens to stake. + * @param _restartStakeSchedule The time from which staking schedule restarts. + * The issue is that we can only stake tokens for a max duration. Thus, we need to restart + * from the lastSchedule. + * @return lastSchedule The max duration for which tokens were staked. + * @return remainingAmount The amount outstanding - to be staked. + * */ + function stakeTokensWithApproval( + address _sender, + uint256 _amount, + uint256 _restartStakeSchedule + ) public onlyThisContract returns (uint256 lastSchedule, uint256 remainingAmount) { + (lastSchedule, remainingAmount) = _stakeTokens(_sender, _amount, _restartStakeSchedule); + } + + /** + * @notice Stakes tokens according to the vesting schedule. Low level function. + * @dev Once here the allowance of tokens is taken for granted. + * @param _sender The sender of tokens to stake. + * @param _amount The amount of tokens to stake. + * @param _restartStakeSchedule The time from which staking schedule restarts. + * The issue is that we can only stake tokens for a max duration. Thus, we need to restart + * from the lastSchedule. + * @return lastSchedule The max duration for which tokens were staked. + * @return remainingAmount The amount outstanding - to be staked. + * */ + function _stakeTokens( + address _sender, + uint256 _amount, + uint256 _restartStakeSchedule + ) internal returns (uint256 lastSchedule, uint256 remainingAmount) { + uint256 restartDate; + uint256 relativeAmount; + uint256 periods; + if (startDate == 0 && _restartStakeSchedule == 0) { + startDate = staking.timestampToLockDate(block.timestamp); + durationLeft = duration; + cliffAdded = cliff; + } + if (_restartStakeSchedule > 0 && _restartStakeSchedule == lastStakingSchedule && _amount == remainingStakeAmount) { + restartDate = _restartStakeSchedule; + } else { + restartDate = startDate; + } + if (endDate == 0) { + endDate = staking.timestampToLockDate(block.timestamp.add(duration)); + } + uint256 addedMaxDuration = restartDate.add(maxDuration); + if (addedMaxDuration < endDate) { + // Runs for max duration + lastStakingSchedule = addedMaxDuration; + periods = (lastStakingSchedule.sub(restartDate)).div(cliff); + relativeAmount = (_amount.mul(periods).mul(cliff)).div(durationLeft); + durationLeft = durationLeft.sub(periods.mul(cliff)); + remainingStakeAmount = _amount.sub(relativeAmount); + } else { + // Normal run + lastStakingSchedule = endDate; + remainingStakeAmount = 0; + durationLeft = 0; + relativeAmount = _amount; + } + + /// @dev Transfer the tokens to this contract. + bool success = SOV.transferFrom(_sender, address(this), relativeAmount); + require(success); + + /// @dev Allow the staking contract to access them. + SOV.approve(address(staking), relativeAmount); + + staking.stakesBySchedule(relativeAmount, cliffAdded, duration.sub(durationLeft), FOUR_WEEKS, address(this), tokenOwner); + if (durationLeft == 0) { + cliffAdded = 0; + } else { + cliffAdded = cliffAdded.add(periods.mul(cliff)); + } + + emit TokensStaked(_sender, relativeAmount); + return (lastStakingSchedule, remainingStakeAmount); + } + + /** + * @notice Delegate votes from `msg.sender` which are locked until lockDate + * to `delegatee`. + * @param _delegatee The address to delegate votes to. + * */ + function delegate(address _delegatee) public onlyTokenOwner { + require(_delegatee != address(0), "delegatee address invalid"); + + /// @dev Withdraw for each unlocked position. + /// @dev Don't change FOUR_WEEKS to TWO_WEEKS, a lot of vestings already deployed with FOUR_WEEKS + /// workaround found, but it doesn't work with TWO_WEEKS + for (uint256 i = startDate.add(cliff); i <= endDate; i += FOUR_WEEKS) { + staking.delegate(_delegatee, i); + } + emit VotesDelegated(msg.sender, _delegatee); + } + + /** + * @notice Withdraws all tokens from the staking contract and + * forwards them to an address specified by the token owner. + * @param receiver The receiving address. + * @dev Can be called only by owner. + * */ + function governanceWithdrawTokens(address receiver) public { + require(msg.sender == address(staking), "unauthorized"); + _withdrawTokens(receiver, true); + } + + /** + * @notice Withdraws unlocked tokens from the staking contract and + * forwards them to an address specified by the token owner. + * @param receiver The receiving address. + * */ + function withdrawTokens(address receiver) public onlyOwners { + _withdrawTokens(receiver, false); + } + + /** + * @notice Withdraws tokens from the staking contract and forwards them + * to an address specified by the token owner. Low level function. + * @dev Once here the caller permission is taken for granted. + * @param receiver The receiving address. + * @param isGovernance Whether all tokens (true) + * or just unlocked tokens (false). + * */ + function _withdrawTokens(address receiver, bool isGovernance) internal { + require(receiver != address(0), "receiver address invalid"); + + uint96 stake; + + /// @dev Usually we just need to iterate over the possible dates until now. + uint256 end; + + /// @dev In the unlikely case that all tokens have been unlocked early, + /// allow to withdraw all of them. + if (staking.allUnlocked() || isGovernance) { + end = endDate; + } else { + end = block.timestamp; + // For four year vesting withdrawal of stakes for the first year is not allowed. These + // stakes are extended for three years. + require(end > startDate.add(52 weeks), "cannot withdraw in the first year"); + } + + /// @dev Withdraw for each unlocked position. + /// @dev Don't change FOUR_WEEKS to TWO_WEEKS, a lot of vestings already deployed with FOUR_WEEKS + /// workaround found, but it doesn't work with TWO_WEEKS + for (uint256 i = startDate.add(cliff); i <= end; i += FOUR_WEEKS) { + /// @dev Read amount to withdraw. + stake = staking.getPriorUserStakeByDate(address(this), i, block.number.sub(1)); + + /// @dev Withdraw if > 0 + if (stake > 0) { + if (isGovernance) { + staking.governanceWithdraw(stake, i, receiver); + } else { + staking.withdraw(stake, i, receiver); + } + } + } + + emit TokensWithdrawn(msg.sender, receiver); + } + + /** + * @notice Extends first year stakes for four year vesting contracts. + * @dev Tokens are vested for 4 years. Since the max staking + * period is 3 years and the tokens are unlocked only after the first year is + * passed, hence, we extend the duration of staking for all unlocked tokens for the first + * year by 3 years. + * */ + function extendStaking() public { + uint256[] memory dates; + uint96[] memory stakes; + (dates, stakes) = staking.getStakes(address(this)); + + for (uint256 i = 0; i < dates.length; i++) { + if ((dates[i] < block.timestamp) && (dates[i] <= startDate.add(52 weeks)) && (stakes[i] > 0)) { + staking.extendStakingDuration(dates[i], dates[i].add(156 weeks)); + } + } + } + + /** + * @notice Collect dividends from fee sharing proxy. + * @param _loanPoolToken The loan pool token address. + * @param _maxCheckpoints Maximum number of checkpoints to be processed. + * @param _receiver The receiver of tokens or msg.sender + * */ + function collectDividends( + address _loanPoolToken, + uint32 _maxCheckpoints, + address _receiver + ) public onlyOwners { + require(_receiver != address(0), "receiver address invalid"); + + /// @dev Invokes the fee sharing proxy. + feeSharingProxy.withdraw(_loanPoolToken, _maxCheckpoints, _receiver); + + emit DividendsCollected(msg.sender, _loanPoolToken, _receiver, _maxCheckpoints); + } + + /** + * @notice Allows the owners to migrate the positions + * to a new staking contract. + * */ + function migrateToNewStakingContract() public onlyOwners { + staking.migrateToNewStakingContract(); + staking = Staking(staking.newStakingContract()); + emit MigratedToNewStakingContract(msg.sender, address(staking)); + } + + /** + * @notice Overrides default ApprovalReceiver._getToken function to + * register SOV token on this contract. + * @return The address of SOV token. + * */ + function _getToken() internal view returns (address) { + return address(SOV); + } + + /** + * @notice Overrides default ApprovalReceiver._getSelectors function to + * register stakeTokensWithApproval selector on this contract. + * @return The array of registered selectors on this contract. + * */ + function _getSelectors() internal view returns (bytes4[] memory) { + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = this.stakeTokensWithApproval.selector; + return selectors; + } +} diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol b/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol new file mode 100644 index 000000000..0b83f6eeb --- /dev/null +++ b/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol @@ -0,0 +1,58 @@ +pragma solidity ^0.5.17; + +import "../../../openzeppelin/Ownable.sol"; +import "../../../interfaces/IERC20.sol"; +import "../../Staking/Staking.sol"; +import "../../IFeeSharingProxy.sol"; + +/** + * @title Four Year Vesting Storage Contract. + * + * @notice This contract is just the storage required for four year vesting. + * It is parent of FourYearVestingLogic and FourYearVesting. + * + * @dev Use Ownable as a parent to align storage structure for Logic and Proxy contracts. + * */ +contract FourYearVestingStorage is Ownable { + /// @notice The SOV token contract. + IERC20 public SOV; + + /// @notice The staking contract address. + Staking public staking; + + /// @notice The owner of the vested tokens. + address public tokenOwner; + + /// @notice Fee sharing Proxy. + IFeeSharingProxy public feeSharingProxy; + + /// @notice The cliff. After this time period the tokens begin to unlock. + uint256 public cliff; + + /// @notice The duration. After this period all tokens will have been unlocked. + uint256 public duration; + + /// @notice The start date of the vesting. + uint256 public startDate; + + /// @notice The end date of the vesting. + uint256 public endDate; + + /// @notice Constant used for computing the vesting dates. + uint256 constant FOUR_WEEKS = 4 weeks; + + /// @notice Maximum duration to stake tokens at one go + uint256 public maxDuration; + + /// @notice End of previous staking schedule + uint256 public lastStakingSchedule; + + /// @notice Amount of shares left to be staked + uint256 public remainingStakeAmount; + + /// @notice Durations left + uint256 public durationLeft; + + /// @notice Cliffs added + uint256 public cliffAdded; +} diff --git a/contracts/governance/Vesting/fouryear/IFourYearVesting.sol b/contracts/governance/Vesting/fouryear/IFourYearVesting.sol new file mode 100644 index 000000000..0127cfc46 --- /dev/null +++ b/contracts/governance/Vesting/fouryear/IFourYearVesting.sol @@ -0,0 +1,16 @@ +pragma solidity ^0.5.17; + +/** + * @title Interface for Four Year Vesting contract. + * @dev Interfaces are used to cast a contract address into a callable instance. + * This interface is used by FourYearVestingLogic contract to implement stakeTokens function + * and on VestingRegistry contract to call IFourYearVesting(vesting).stakeTokens function + * at a vesting instance. + */ +interface IFourYearVesting { + function duration() external returns (uint256); + + function endDate() external returns (uint256); + + function stakeTokens(uint256 _amount, uint256 _restartStakeSchedule) external returns (uint256 lastSchedule, uint256 remainingAmount); +} diff --git a/contracts/governance/Vesting/fouryear/IFourYearVestingFactory.sol b/contracts/governance/Vesting/fouryear/IFourYearVestingFactory.sol new file mode 100644 index 000000000..50be88a37 --- /dev/null +++ b/contracts/governance/Vesting/fouryear/IFourYearVestingFactory.sol @@ -0,0 +1,20 @@ +pragma solidity ^0.5.17; + +/** + * @title Interface for Four Year Vesting Factory contract. + * @dev Interfaces are used to cast a contract address into a callable instance. + * This interface is used by FourYearVestingFactory contract to override empty + * implemention of deployFourYearVesting function + * and use an instance of FourYearVestingFactory. + */ +interface IFourYearVestingFactory { + function deployFourYearVesting( + address _SOV, + address _staking, + address _tokenOwner, + uint256 _cliff, + uint256 _duration, + address _feeSharing, + address _owner + ) external returns (address); +} diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js new file mode 100644 index 000000000..95117722c --- /dev/null +++ b/tests/vesting/FourYearVesting.js @@ -0,0 +1,989 @@ +const { expect } = require("chai"); +const { expectRevert, expectEvent, constants, BN } = require("@openzeppelin/test-helpers"); +const { + address, + minerStart, + minerStop, + unlockedAccount, + mineBlock, + etherMantissa, + etherUnsigned, + setTime, + increaseTime, + lastBlock, +} = require("../Utils/Ethereum"); + +const StakingLogic = artifacts.require("Staking"); +const StakingProxy = artifacts.require("StakingProxy"); +const SOV = artifacts.require("SOV"); +const TestWrbtc = artifacts.require("TestWrbtc"); +const FeeSharingProxy = artifacts.require("FeeSharingProxyMockup"); +const VestingLogic = artifacts.require("FourYearVestingLogic"); +const Vesting = artifacts.require("FourYearVesting"); +//Upgradable Vesting Registry +const VestingRegistryLogic = artifacts.require("VestingRegistryLogicMockup"); +const VestingRegistryProxy = artifacts.require("VestingRegistryProxy"); + +const MAX_DURATION = new BN(24 * 60 * 60).mul(new BN(1092)); +const WEEK = new BN(7 * 24 * 60 * 60); + +const TOTAL_SUPPLY = "10000000000000000000000000"; +const ONE_MILLON = "1000000000000000000000000"; +const ONE_ETHER = "1000000000000000000"; + +contract("FourYearVesting", (accounts) => { + const name = "Test token"; + const symbol = "TST"; + + let root, a1, a2, a3; + let token, staking, stakingLogic, feeSharingProxy; + let vestingLogic; + let kickoffTS; + + let cliff = "1"; + let duration = "39"; + + before(async () => { + [root, a1, a2, a3, ...accounts] = accounts; + token = await SOV.new(TOTAL_SUPPLY); + wrbtc = await TestWrbtc.new(); + + vestingLogic = await VestingLogic.new(); + + feeSharingProxy = await FeeSharingProxy.new(constants.ZERO_ADDRESS, constants.ZERO_ADDRESS); + + stakingLogic = await StakingLogic.new(token.address); + staking = await StakingProxy.new(token.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); + //Upgradable Vesting Registry + vestingRegistryLogic = await VestingRegistryLogic.new(); + vestingReg = await VestingRegistryProxy.new(); + await vestingReg.setImplementation(vestingRegistryLogic.address); + vestingReg = await VestingRegistryLogic.at(vestingReg.address); + await staking.setVestingRegistry(vestingReg.address); + + await token.transfer(a2, "1000"); + await token.approve(staking.address, "1000", { from: a2 }); + + kickoffTS = await staking.kickoffTS.call(); + }); + + describe("constructor", () => { + it("sets the expected values", async () => { + let vestingInstance = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + cliff, + duration, + feeSharingProxy.address + ); + vestingInstance = await VestingLogic.at(vestingInstance.address); + + // Check data + let _sov = await vestingInstance.SOV(); + let _stackingAddress = await vestingInstance.staking(); + let _tokenOwner = await vestingInstance.tokenOwner(); + let _cliff = await vestingInstance.cliff(); + let _duration = await vestingInstance.duration(); + let _feeSharingProxy = await vestingInstance.feeSharingProxy(); + + assert.equal(_sov, token.address); + assert.equal(_stackingAddress, staking.address); + assert.equal(_tokenOwner, root); + assert.equal(_cliff.toString(), cliff); + assert.equal(_duration.toString(), duration); + assert.equal(_feeSharingProxy, feeSharingProxy.address); + }); + + it("fails if the 0 address is passed as SOV address", async () => { + await expectRevert( + Vesting.new(vestingLogic.address, constants.ZERO_ADDRESS, staking.address, root, cliff, duration, feeSharingProxy.address), + + "SOV address invalid" + ); + }); + + it("fails if the 0 address is passed as token owner address", async () => { + await expectRevert( + Vesting.new( + vestingLogic.address, + token.address, + staking.address, + constants.ZERO_ADDRESS, + cliff, + duration, + feeSharingProxy.address + ), + "token owner address invalid" + ); + }); + + it("fails if the 0 address is passed as staking address", async () => { + await expectRevert( + Vesting.new(vestingLogic.address, token.address, constants.ZERO_ADDRESS, root, cliff, duration, feeSharingProxy.address), + "staking address invalid" + ); + }); + + it("fails if the vesting duration is bigger than the max staking duration", async () => { + await expectRevert( + Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + cliff, + MAX_DURATION.add(new BN(1)), + feeSharingProxy.address + ), + "duration may not exceed the max duration" + ); + }); + + it("fails if the vesting duration is shorter than the cliff", async () => { + await expectRevert( + Vesting.new(vestingLogic.address, token.address, staking.address, root, 100, 99, feeSharingProxy.address), + "duration must be bigger than or equal to the cliff" + ); + }); + + it("fails if the 0 address is passed as feeSharingProxy address", async () => { + await expectRevert( + Vesting.new(vestingLogic.address, token.address, staking.address, root, cliff, duration, constants.ZERO_ADDRESS), + "feeSharingProxy address invalid" + ); + }); + }); + + describe("delegate", () => { + let vesting; + it("should stake tokens and delegate voting power", async () => { + let toStake = ONE_MILLON; + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + a2, + 16 * WEEK, + 26 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await token.approve(vesting.address, toStake); + await vesting.stakeTokens(toStake, 0); + + // check delegatee + let data = await staking.getStakes.call(vesting.address); + /// @dev Optimization: This loop through 40 steps is a bottleneck + for (let i = 0; i < data.dates.length; i++) { + let delegatee = await staking.delegates(vesting.address, data.dates[i]); + expect(delegatee).equal(a2); + } + + // delegate + let tx = await vesting.delegate(a1, { from: a2 }); + + expectEvent(tx, "VotesDelegated", { + caller: a2, + delegatee: a1, + }); + + // check new delegatee + data = await staking.getStakes.call(vesting.address); + /// @dev Optimization: This loop through 40 steps is a bottleneck + for (let i = 0; i < data.dates.length; i++) { + let delegatee = await staking.delegates(vesting.address, data.dates[i]); + expect(delegatee).equal(a1); + } + }); + + it("should stake tokens 1 time and delegate voting power (using vesting logic with bug in delegation)", async () => { + let toStake = ONE_MILLON; + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + a2, + 16 * WEEK, + 26 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await token.approve(vesting.address, toStake); + await vesting.stakeTokens(toStake, 0); + + // check delegatee + let data = await staking.getStakes.call(vesting.address); + for (let i = 0; i < data.dates.length; i++) { + let delegatee = await staking.delegates(vesting.address, data.dates[i]); + expect(delegatee).equal(a2); + } + + // delegate + let tx = await vesting.delegate(a1, { from: a2 }); + + expectEvent(tx, "VotesDelegated", { + caller: a2, + delegatee: a1, + }); + + // check new delegatee + data = await staking.getStakes.call(vesting.address); + for (let i = 0; i < data.dates.length; i++) { + let delegatee = await staking.delegates(vesting.address, data.dates[i]); + expect(delegatee).equal(a1); + } + }); + + it("fails if delegatee is zero address", async () => { + await expectRevert(vesting.delegate(constants.ZERO_ADDRESS, { from: a2 }), "delegatee address invalid"); + }); + + it("fails if not a token owner", async () => { + await expectRevert(vesting.delegate(a1, { from: a1 }), "unauthorized"); + }); + }); + + describe("stakeTokens; using Ganache", () => { + let vesting; + it("should stake 1,000,000 SOV with a duration of 104 weeks and a 26 week cliff", async () => { + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + 26 * WEEK, + 104 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + await token.approve(vesting.address, ONE_MILLON); + let tx = await vesting.stakeTokens(ONE_MILLON, 0); + + expectEvent(tx, "TokensStaked", { + caller: root, + amount: ONE_MILLON, + }); + + // check delegatee + let data = await staking.getStakes.call(vesting.address); + for (let i = 0; i < data.dates.length; i++) { + let delegatee = await staking.delegates(vesting.address, data.dates[i]); + expect(delegatee).equal(root); + } + }); + + it("should stake 1,000,000 SOV with a duration of 104 weeks and a 26 week cliff", async () => { + // let block = await web3.eth.getBlock("latest"); + let block = await lastBlock(); // ethers.provider.getBlock("latest"); + let timestamp = parseInt(block.timestamp); + + let kickoffTS = await staking.kickoffTS(); + + let start = timestamp + 26 * WEEK; + let end = timestamp + 104 * WEEK; + + let numIntervals = Math.floor((end - start) / (4 * WEEK)) + 1; + let stakedPerInterval = ONE_MILLON / numIntervals; + + // positive case + for (let i = start; i <= end; i += 4 * WEEK) { + let periodFromKickoff = Math.floor((i - kickoffTS.toNumber()) / (2 * WEEK)); + let startBuf = periodFromKickoff * 2 * WEEK + kickoffTS.toNumber(); + let userStakingCheckpoints = await staking.userStakingCheckpoints(vesting.address, startBuf, 0); + + assert.equal(userStakingCheckpoints.fromBlock.toNumber(), block.number); + assert.equal(userStakingCheckpoints.stake.toString(), stakedPerInterval); + + let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints(vesting.address, startBuf); + assert.equal(numUserStakingCheckpoints.toString(), "1"); + } + + // negative cases + + // start-10 to avoid coming to active checkpoint + let periodFromKickoff = Math.floor((start - 10 - kickoffTS.toNumber()) / (2 * WEEK)); + let startBuf = periodFromKickoff * 2 * WEEK + kickoffTS.toNumber(); + let userStakingCheckpoints = await staking.userStakingCheckpoints(vesting.address, startBuf, 0); + + assert.equal(userStakingCheckpoints.fromBlock.toNumber(), 0); + assert.equal(userStakingCheckpoints.stake.toString(), 0); + + let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints(vesting.address, startBuf); + assert.equal(numUserStakingCheckpoints.toString(), "0"); + + periodFromKickoff = Math.floor((end + 1 - kickoffTS.toNumber()) / (2 * WEEK)); + startBuf = periodFromKickoff * 2 * WEEK + kickoffTS.toNumber(); + userStakingCheckpoints = await staking.userStakingCheckpoints(vesting.address, startBuf, 0); + + assert.equal(userStakingCheckpoints.fromBlock.toNumber(), 0); + assert.equal(userStakingCheckpoints.stake.toString(), 0); + + numUserStakingCheckpoints = await staking.numUserStakingCheckpoints(vesting.address, startBuf); + assert.equal(numUserStakingCheckpoints.toString(), "0"); + }); + + it("should stake 2 times 1,000,000 SOV with a duration of 104 weeks and a 26 week cliff", async () => { + let amount = 1000; + let cliff = 28 * WEEK; + let duration = 104 * WEEK; + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + cliff, + duration, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await token.approve(vesting.address, amount); + await vesting.stakeTokens(amount, 0); + + let block1 = await web3.eth.getBlock("latest"); + let timestamp1 = block1.timestamp; + + let start = timestamp1 + cliff; + let end = timestamp1 + duration; + + let numIntervals = Math.floor((end - start) / (4 * WEEK)) + 1; + let stakedPerInterval = amount / numIntervals; + + await increaseTime(52 * WEEK); + await token.approve(vesting.address, amount); + await vesting.stakeTokens(amount, 0); + + let block2 = await web3.eth.getBlock("latest"); + let timestamp2 = block2.timestamp; + + let start2 = await staking.timestampToLockDate(timestamp2 + cliff); + let end2 = timestamp2 + duration; + + // positive case + for (let i = start; i <= end2; i += 4 * WEEK) { + let lockedTS = await staking.timestampToLockDate(i); + let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints(vesting.address, lockedTS); + let userStakingCheckpoints = await staking.userStakingCheckpoints(vesting.address, lockedTS, numUserStakingCheckpoints - 1); + if (i < start2 || i > end) { + assert.equal(numUserStakingCheckpoints.toString(), "1"); + assert.equal(userStakingCheckpoints.stake.toString(), stakedPerInterval); + } else { + assert.equal(numUserStakingCheckpoints.toString(), "2"); + assert.equal(userStakingCheckpoints.stake.toString(), stakedPerInterval * 2); + } + } + }); + + it("should stake 1000 tokens with a duration of 34 weeks and a 26 week cliff (dust on rounding)", async () => { + let amount = 1000; + let cliff = 26 * WEEK; + let duration = 34 * WEEK; + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + cliff, + duration, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await token.approve(vesting.address, amount); + await vesting.stakeTokens(amount, 0); + + let block = await web3.eth.getBlock("latest"); + let timestamp = block.timestamp; + + let start = timestamp + cliff; + let end = timestamp + duration; + + let numIntervals = Math.floor((end - start) / (4 * WEEK)) + 1; + let stakedPerInterval = Math.floor(amount / numIntervals); + + let stakeForFirstInterval = amount - stakedPerInterval * (numIntervals - 1); + + // positive case + for (let i = start; i <= end; i += 4 * WEEK) { + let periodFromKickoff = Math.floor((i - kickoffTS.toNumber()) / (2 * WEEK)); + let startBuf = periodFromKickoff * 2 * WEEK + kickoffTS.toNumber(); + let userStakingCheckpoints = await staking.userStakingCheckpoints(vesting.address, startBuf, 0); + + assert.equal(userStakingCheckpoints.fromBlock.toNumber(), block.number); + if (i === start) { + assert.equal(userStakingCheckpoints.stake.toString(), stakeForFirstInterval); + } else { + assert.equal(userStakingCheckpoints.stake.toString(), stakedPerInterval); + } + + let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints(vesting.address, startBuf); + assert.equal(numUserStakingCheckpoints.toString(), "1"); + } + }); + }); + + describe("stakeTokensWithApproval", () => { + let vesting; + + it("fails if invoked directly", async () => { + let amount = 1000; + let cliff = 26 * WEEK; + let duration = 34 * WEEK; + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + cliff, + duration, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + await expectRevert(vesting.stakeTokensWithApproval(root, amount, 0), "unauthorized"); + }); + + it("fails if pass wrong method in data", async () => { + let amount = 1000; + let cliff = 26 * WEEK; + let duration = 34 * WEEK; + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + cliff, + duration, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + let contract = new web3.eth.Contract(vesting.abi, vesting.address); + let sender = root; + let data = contract.methods.stakeTokens(amount, 0).encodeABI(); + + await expectRevert(token.approveAndCall(vesting.address, amount, data, { from: sender }), "method is not allowed"); + }); + + it("should stake 1000 tokens with a duration of 34 weeks and a 26 week cliff (dust on rounding)", async () => { + let amount = 1000; + let cliff = 26 * WEEK; + let duration = 34 * WEEK; + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + cliff, + duration, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + let contract = new web3.eth.Contract(vesting.abi, vesting.address); + let sender = root; + let data = contract.methods.stakeTokensWithApproval(sender, amount, 0).encodeABI(); + await token.approveAndCall(vesting.address, amount, data, { from: sender }); + + let block = await web3.eth.getBlock("latest"); + let timestamp = block.timestamp; + + let start = timestamp + cliff; + let end = timestamp + duration; + + let numIntervals = Math.floor((end - start) / (4 * WEEK)) + 1; + let stakedPerInterval = Math.floor(amount / numIntervals); + + let stakeForFirstInterval = amount - stakedPerInterval * (numIntervals - 1); + + // positive case + for (let i = start; i <= end; i += 4 * WEEK) { + let periodFromKickoff = Math.floor((i - kickoffTS.toNumber()) / (2 * WEEK)); + let startBuf = periodFromKickoff * 2 * WEEK + kickoffTS.toNumber(); + let userStakingCheckpoints = await staking.userStakingCheckpoints(vesting.address, startBuf, 0); + + assert.equal(userStakingCheckpoints.fromBlock.toNumber(), block.number); + if (i === start) { + assert.equal(userStakingCheckpoints.stake.toString(), stakeForFirstInterval); + } else { + assert.equal(userStakingCheckpoints.stake.toString(), stakedPerInterval); + } + + let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints(vesting.address, startBuf); + assert.equal(numUserStakingCheckpoints.toString(), "1"); + } + }); + }); + + describe("withdrawTokens", () => { + let vesting; + + it("should not withdraw unlocked tokens (cliff = 3 weeks)", async () => { + // Save current amount + let previousAmount = await token.balanceOf(root); + let toStake = ONE_ETHER; + + await increaseTime(3 * WEEK); + + // Stake + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + 3 * WEEK, + 3 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await token.approve(vesting.address, toStake); + await vesting.stakeTokens(toStake, 0); + + // time travel + await increaseTime(3 * WEEK); + + // withdraw + await expectRevert(vesting.withdrawTokens(root), "cannot withdraw in the first year"); + }); + + it("should withdraw unlocked tokens", async () => { + // Save current amount + let previousAmount = await token.balanceOf(root); + let toStake = ONE_MILLON; + + // Stake + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + 26 * WEEK, + 104 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await token.approve(vesting.address, toStake); + let tx = await vesting.stakeTokens(toStake, 0); + let lastStakingSchedule = await vesting.lastStakingSchedule(); + let remainingStakeAmount = await vesting.remainingStakeAmount(); + + tx = await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + + let amountAfterStake = await token.balanceOf(root); + + // time travel + await increaseTime(104 * WEEK); + + // withdraw + tx = await vesting.withdrawTokens(root); + + // check event + expectEvent(tx, "TokensWithdrawn", { + caller: root, + receiver: root, + }); + + // verify amount + let amount = await token.balanceOf(root); + + assert.equal(previousAmount.sub(new BN(toStake)).toString(), amountAfterStake.toString()); + assert.equal(previousAmount.toString(), amount.toString()); + }); + + it("should not withdraw unlocked tokens in the first year", async () => { + // Save current amount + let previousAmount = await token.balanceOf(root); + let toStake = ONE_MILLON; + + // Stake + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + 16 * WEEK, + 34 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await token.approve(vesting.address, toStake); + await vesting.stakeTokens(toStake, 0); + + // time travel + await increaseTime(34 * WEEK); + + // withdraw + await expectRevert(vesting.withdrawTokens(root), "cannot withdraw in the first year"); + }); + + it("should not withdraw unlocked tokens for 2 stakes (current time >= last locking date of the second stake)", async () => { + // Save current amount + let previousAmount = await token.balanceOf(root); + let toStake = ONE_ETHER; + + // Stake + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + 4 * WEEK, + 20 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await token.approve(vesting.address, toStake); + await vesting.stakeTokens(toStake, 0); + + // time travel + await increaseTime(20 * WEEK); + + // withdraw + await expectRevert(vesting.withdrawTokens(root), "cannot withdraw in the first year"); + }); + + it("should not withdraw unlocked tokens for 2 stakes (shouldn't withdraw the latest stake)", async () => { + // Save current amount + let toStake = ONE_MILLON; + + // Stake + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + 4 * WEEK, + 20 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await token.approve(vesting.address, toStake); + await vesting.stakeTokens(toStake, 0); + + // time travel + await increaseTime(18 * WEEK); + + // withdraw + await expectRevert(vesting.withdrawTokens(root), "cannot withdraw in the first year"); + }); + + it("should do nothing if withdrawing a second time", async () => { + await expectRevert(vesting.withdrawTokens(root), "cannot withdraw in the first year"); + }); + + it("should do nothing if withdrawing before reaching the cliff", async () => { + let toStake = ONE_MILLON; + + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + a1, + 26 * WEEK, + 104 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await token.approve(vesting.address, toStake); + await vesting.stakeTokens(toStake, 0); + + // time travel + await increaseTime(25 * WEEK); + + await expectRevert(vesting.withdrawTokens(root, { from: a1 }), "cannot withdraw in the first year"); + }); + + it("should fail if the caller is neither owner nor token owner", async () => { + await expectRevert(vesting.withdrawTokens(root, { from: a2 }), "unauthorized"); + await expectRevert(vesting.withdrawTokens(root, { from: a3 }), "unauthorized"); + + await expectRevert(vesting.withdrawTokens(root, { from: root }), "cannot withdraw in the first year"); + await expectRevert(vesting.withdrawTokens(root, { from: a1 }), "cannot withdraw in the first year"); + }); + + it("Shouldn't be possible to use governanceWithdrawVesting by not owner", async () => { + let toStake = ONE_MILLON; + + // Stake + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + 26 * WEEK, + 104 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await token.approve(vesting.address, toStake); + await vesting.stakeTokens(toStake, 0); + + await expectRevert(staking.governanceWithdrawVesting(vesting.address, root, { from: a1 }), "unauthorized"); + }); + + it("Shouldn't be possible to use governanceWithdraw by user", async () => { + let toStake = ONE_MILLON; + + // Stake + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + 26 * WEEK, + 104 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await token.approve(vesting.address, toStake); + await vesting.stakeTokens(toStake, 0); + + await expectRevert(staking.governanceWithdraw(100, kickoffTS.toNumber() + 52 * WEEK, root), "unauthorized"); + }); + + it("Shouldn't be possible to use governanceWithdrawTokens by user", async () => { + let toStake = ONE_ETHER; + + // Stake + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + 26 * WEEK, + 104 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await token.approve(vesting.address, toStake); + await vesting.stakeTokens(toStake, 0); + + await expectRevert(vesting.governanceWithdrawTokens(root), "operation not supported"); + }); + + it("governanceWithdrawTokens", async () => { + let previousAmount = await token.balanceOf(root); + let toStake = ONE_ETHER; + + // Stake + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + 16 * WEEK, + 36 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await token.approve(vesting.address, toStake); + await vesting.stakeTokens(toStake, 0); + + await staking.addAdmin(a1); + // governance withdraw must fail for four year vesting + await expectRevert(staking.governanceWithdrawVesting(vesting.address, root, { from: a1 }), "operation not supported"); + }); + }); + + describe("collectDividends", async () => { + it("should fail if the caller is neither owner nor token owner", async () => { + let vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + a1, + 26 * WEEK, + 104 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + await expectRevert(vesting.collectDividends(root, 10, a1, { from: a2 }), "unauthorized"); + await expectRevert(vesting.collectDividends(root, 10, a1, { from: a3 }), "unauthorized"); + }); + + it("should collect dividends", async () => { + let vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + a1, + 26 * WEEK, + 104 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + let maxCheckpoints = new BN(10); + let tx = await vesting.collectDividends(a1, maxCheckpoints, a2); + + let testData = await feeSharingProxy.testData.call(); + expect(testData.loanPoolToken).to.be.equal(a1); + expect(testData.maxCheckpoints).to.be.bignumber.equal(maxCheckpoints); + expect(testData.receiver).to.be.equal(a2); + + expectEvent(tx, "DividendsCollected", { + caller: root, + loanPoolToken: a1, + receiver: a2, + maxCheckpoints: maxCheckpoints, + }); + }); + }); + + describe("migrateToNewStakingContract", async () => { + let vesting; + it("should set the new staking contract", async () => { + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + a1, + 26 * WEEK, + 104 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + // 1. set new staking contract address on staking contract + + let newStaking = await StakingProxy.new(token.address); + await newStaking.setImplementation(stakingLogic.address); + newStaking = await StakingLogic.at(newStaking.address); + + await staking.setNewStakingContract(newStaking.address); + + // 2. call migrateToNewStakingContract + let tx = await vesting.migrateToNewStakingContract(); + expectEvent(tx, "MigratedToNewStakingContract", { + caller: root, + newStakingContract: newStaking.address, + }); + let _staking = await vesting.staking(); + assert.equal(_staking, newStaking.address); + }); + + it("should fail if there is no new staking contract set", async () => { + let newStaking = await StakingProxy.new(token.address); + await newStaking.setImplementation(stakingLogic.address); + newStaking = await StakingLogic.at(newStaking.address); + + vesting = await Vesting.new( + vestingLogic.address, + token.address, + newStaking.address, + a1, + 26 * WEEK, + 104 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + await expectRevert(vesting.migrateToNewStakingContract(), "there is no new staking contract set"); + }); + + it("should fail if the caller is neither owner nor token owner", async () => { + let newStaking = await StakingProxy.new(token.address); + await newStaking.setImplementation(stakingLogic.address); + newStaking = await StakingLogic.at(newStaking.address); + + vesting = await Vesting.new( + vestingLogic.address, + token.address, + newStaking.address, + a1, + 26 * WEEK, + 104 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await newStaking.setNewStakingContract(newStaking.address); + + await expectRevert(vesting.migrateToNewStakingContract({ from: a2 }), "unauthorized"); + await expectRevert(vesting.migrateToNewStakingContract({ from: a3 }), "unauthorized"); + + await vesting.migrateToNewStakingContract(); + await vesting.migrateToNewStakingContract({ from: a1 }); + }); + }); + + describe("fouryearvesting", async () => { + let vesting, dates0, dates3, dates5; + it("staking schedule must run for max duration", async () => { + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + 4 * WEEK, + 39 * 4 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + await token.approve(vesting.address, ONE_MILLON); + let tx = await vesting.stakeTokens(ONE_MILLON, 0); + let lastStakingSchedule = await vesting.lastStakingSchedule(); + let remainingStakeAmount = await vesting.remainingStakeAmount(); + + tx = await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + + tx = await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + let data = await staking.getStakes.call(vesting.address); + assert.equal(data.dates.length, 39); + assert.equal(data.stakes.length, 39); + expect(data.stakes[0]).to.be.bignumber.equal(data.stakes[15]); + dates0 = data.dates[0]; + dates5 = data.dates[5]; + }); + + it("should extend duration of first 5 staking periods", async () => { + await increaseTime(20 * WEEK); + tx = await vesting.extendStaking(); + data = await staking.getStakes.call(vesting.address); + expect(data.stakes[0]).to.be.bignumber.equal(data.stakes[15]); + expect(dates0).to.be.bignumber.not.equal(data.dates[0]); + expect(dates5).to.be.bignumber.equal(data.dates[0]); + dates0 = data.dates[0]; + dates5 = data.dates[5]; + }); + + it("should extend duration of next 5 staking periods", async () => { + await increaseTime(20 * WEEK); + tx = await vesting.extendStaking(); + data = await staking.getStakes.call(vesting.address); + expect(data.stakes[0]).to.be.bignumber.equal(data.stakes[15]); + expect(dates0).to.be.bignumber.not.equal(data.dates[0]); + expect(dates5).to.be.bignumber.equal(data.dates[0]); + dates0 = data.dates[0]; + dates3 = data.dates[3]; + }); + + it("should extend duration of next 3 staking periods only", async () => { + await increaseTime(20 * WEEK); + tx = await vesting.extendStaking(); + data = await staking.getStakes.call(vesting.address); + expect(data.stakes[0]).to.be.bignumber.equal(data.stakes[15]); + expect(dates0).to.be.bignumber.not.equal(data.dates[0]); + expect(dates3).to.be.bignumber.equal(data.dates[0]); + }); + }); +}); From fc45033fe7378bb86781cf97f55b729d93fb4d4f Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Wed, 23 Feb 2022 16:55:05 +0530 Subject: [PATCH 02/86] Ran prettier --- .../fouryear/FourYearVestingFactory.sol | 5 +- .../contractInteraction/ABIs/FastBTCBiDi.json | 2214 ++++++++--------- tests/vesting/FourYearVesting.js | 2 +- 3 files changed, 1110 insertions(+), 1111 deletions(-) diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol b/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol index cc66b4572..4a97b3751 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol @@ -37,9 +37,8 @@ contract FourYearVestingFactory is IFourYearVestingFactory, Ownable { address _feeSharing, address _vestingOwner ) external onlyOwner returns (address) { - address fourYearVesting = address( - new FourYearVesting(fourYearVestingLogic, _SOV, _staking, _tokenOwner, _cliff, _duration, _feeSharing) - ); + address fourYearVesting = + address(new FourYearVesting(fourYearVestingLogic, _SOV, _staking, _tokenOwner, _cliff, _duration, _feeSharing)); Ownable(fourYearVesting).transferOwnership(_vestingOwner); return fourYearVesting; } diff --git a/scripts/contractInteraction/ABIs/FastBTCBiDi.json b/scripts/contractInteraction/ABIs/FastBTCBiDi.json index 768856b47..38923c3b6 100644 --- a/scripts/contractInteraction/ABIs/FastBTCBiDi.json +++ b/scripts/contractInteraction/ABIs/FastBTCBiDi.json @@ -1,1108 +1,1108 @@ [ - { - "inputs": [ - { - "internalType": "address", - "name": "accessControl", - "type": "address" - }, - { - "internalType": "contract IBTCAddressValidator", - "name": "newBtcAddressValidator", - "type": "address" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "bytes32", - "name": "bitcoinTxHash", - "type": "bytes32" - }, - { - "indexed": false, - "internalType": "uint8", - "name": "transferBatchSize", - "type": "uint8" - } - ], - "name": "BitcoinTransferBatchSending", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "baseFeeSatoshi", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "dynamicFee", - "type": "uint256" - } - ], - "name": "BitcoinTransferFeeChanged", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "bytes32", - "name": "transferId", - "type": "bytes32" - }, - { - "indexed": false, - "internalType": "enum FastBTCBridge.BitcoinTransferStatus", - "name": "newStatus", - "type": "uint8" - } - ], - "name": "BitcoinTransferStatusUpdated", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "Frozen", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "bytes32", - "name": "transferId", - "type": "bytes32" - }, - { - "indexed": false, - "internalType": "string", - "name": "btcAddress", - "type": "string" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "nonce", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amountSatoshi", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "feeSatoshi", - "type": "uint256" - }, - { - "indexed": true, - "internalType": "address", - "name": "rskAddress", - "type": "address" - } - ], - "name": "NewBitcoinTransfer", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "Paused", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "Unfrozen", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "Unpaused", - "type": "event" - }, - { - "inputs": [], - "name": "DYNAMIC_FEE_DIVISOR", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "MAXIMUM_VALID_NONCE", - "outputs": [ - { - "internalType": "uint8", - "name": "", - "type": "uint8" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "MAX_BASE_FEE_SATOSHI", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "SATOSHI_DIVISOR", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "accessControl", - "outputs": [ - { - "internalType": "contract IFastBTCAccessControl", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "feeStructureIndex", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "newBaseFeeSatoshi", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "newDynamicFee", - "type": "uint256" - } - ], - "name": "addFeeStructure", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "baseFeeSatoshi", - "outputs": [ - { - "internalType": "uint32", - "name": "", - "type": "uint32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "btcAddressValidator", - "outputs": [ - { - "internalType": "contract IBTCAddressValidator", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "amountSatoshi", - "type": "uint256" - } - ], - "name": "calculateCurrentFeeSatoshi", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "amountWei", - "type": "uint256" - } - ], - "name": "calculateCurrentFeeWei", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "currentFeeStructureIndex", - "outputs": [ - { - "internalType": "uint8", - "name": "", - "type": "uint8" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes", - "name": "userData", - "type": "bytes" - } - ], - "name": "decodeBridgeUserData", - "outputs": [ - { - "internalType": "address", - "name": "rskAddress", - "type": "address" - }, - { - "internalType": "string", - "name": "btcAddress", - "type": "string" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [], - "name": "dynamicFee", - "outputs": [ - { - "internalType": "uint16", - "name": "", - "type": "uint16" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "rskAddress", - "type": "address" - }, - { - "internalType": "string", - "name": "btcAddress", - "type": "string" - } - ], - "name": "encodeBridgeUserData", - "outputs": [ - { - "internalType": "bytes", - "name": "userData", - "type": "bytes" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [], - "name": "federators", - "outputs": [ - { - "internalType": "address[]", - "name": "addresses", - "type": "address[]" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "name": "feeStructures", - "outputs": [ - { - "internalType": "uint32", - "name": "baseFeeSatoshi", - "type": "uint32" - }, - { - "internalType": "uint16", - "name": "dynamicFee", - "type": "uint16" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "freeze", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "frozen", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "string", - "name": "btcAddress", - "type": "string" - } - ], - "name": "getNextNonce", - "outputs": [ - { - "internalType": "uint8", - "name": "", - "type": "uint8" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "string", - "name": "btcAddress", - "type": "string" - }, - { - "internalType": "uint8", - "name": "nonce", - "type": "uint8" - } - ], - "name": "getTransfer", - "outputs": [ - { - "components": [ - { - "internalType": "address", - "name": "rskAddress", - "type": "address" - }, - { - "internalType": "enum FastBTCBridge.BitcoinTransferStatus", - "name": "status", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "nonce", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "feeStructureIndex", - "type": "uint8" - }, - { - "internalType": "uint32", - "name": "blockNumber", - "type": "uint32" - }, - { - "internalType": "uint40", - "name": "totalAmountSatoshi", - "type": "uint40" - }, - { - "internalType": "string", - "name": "btcAddress", - "type": "string" - } - ], - "internalType": "struct FastBTCBridge.BitcoinTransfer", - "name": "transfer", - "type": "tuple" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32[]", - "name": "transferIds", - "type": "bytes32[]" - }, - { - "internalType": "enum FastBTCBridge.BitcoinTransferStatus", - "name": "newStatus", - "type": "uint8" - } - ], - "name": "getTransferBatchUpdateHash", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "bitcoinTxHash", - "type": "bytes32" - }, - { - "internalType": "bytes32[]", - "name": "transferIds", - "type": "bytes32[]" - }, - { - "internalType": "enum FastBTCBridge.BitcoinTransferStatus", - "name": "newStatus", - "type": "uint8" - } - ], - "name": "getTransferBatchUpdateHashWithTxHash", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "transferId", - "type": "bytes32" - } - ], - "name": "getTransferByTransferId", - "outputs": [ - { - "components": [ - { - "internalType": "address", - "name": "rskAddress", - "type": "address" - }, - { - "internalType": "enum FastBTCBridge.BitcoinTransferStatus", - "name": "status", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "nonce", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "feeStructureIndex", - "type": "uint8" - }, - { - "internalType": "uint32", - "name": "blockNumber", - "type": "uint32" - }, - { - "internalType": "uint40", - "name": "totalAmountSatoshi", - "type": "uint40" - }, - { - "internalType": "string", - "name": "btcAddress", - "type": "string" - } - ], - "internalType": "struct FastBTCBridge.BitcoinTransfer", - "name": "transfer", - "type": "tuple" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "string", - "name": "btcAddress", - "type": "string" - }, - { - "internalType": "uint256", - "name": "nonce", - "type": "uint256" - } - ], - "name": "getTransferId", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "string[]", - "name": "btcAddresses", - "type": "string[]" - }, - { - "internalType": "uint8[]", - "name": "nonces", - "type": "uint8[]" - } - ], - "name": "getTransfers", - "outputs": [ - { - "components": [ - { - "internalType": "address", - "name": "rskAddress", - "type": "address" - }, - { - "internalType": "enum FastBTCBridge.BitcoinTransferStatus", - "name": "status", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "nonce", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "feeStructureIndex", - "type": "uint8" - }, - { - "internalType": "uint32", - "name": "blockNumber", - "type": "uint32" - }, - { - "internalType": "uint40", - "name": "totalAmountSatoshi", - "type": "uint40" - }, - { - "internalType": "string", - "name": "btcAddress", - "type": "string" - } - ], - "internalType": "struct FastBTCBridge.BitcoinTransfer[]", - "name": "ret", - "type": "tuple[]" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32[]", - "name": "transferIds", - "type": "bytes32[]" - } - ], - "name": "getTransfersByTransferId", - "outputs": [ - { - "components": [ - { - "internalType": "address", - "name": "rskAddress", - "type": "address" - }, - { - "internalType": "enum FastBTCBridge.BitcoinTransferStatus", - "name": "status", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "nonce", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "feeStructureIndex", - "type": "uint8" - }, - { - "internalType": "uint32", - "name": "blockNumber", - "type": "uint32" - }, - { - "internalType": "uint40", - "name": "totalAmountSatoshi", - "type": "uint40" - }, - { - "internalType": "string", - "name": "btcAddress", - "type": "string" - } - ], - "internalType": "struct FastBTCBridge.BitcoinTransfer[]", - "name": "ret", - "type": "tuple[]" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "string", - "name": "btcAddress", - "type": "string" - } - ], - "name": "isValidBtcAddress", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32[]", - "name": "transferIds", - "type": "bytes32[]" - }, - { - "internalType": "bytes[]", - "name": "signatures", - "type": "bytes[]" - } - ], - "name": "markTransfersAsMined", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "bitcoinTxHash", - "type": "bytes32" - }, - { - "internalType": "bytes32[]", - "name": "transferIds", - "type": "bytes32[]" - }, - { - "internalType": "bytes[]", - "name": "signatures", - "type": "bytes[]" - } - ], - "name": "markTransfersAsSending", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "maxTransferSatoshi", - "outputs": [ - { - "internalType": "uint40", - "name": "", - "type": "uint40" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "minTransferSatoshi", - "outputs": [ - { - "internalType": "uint40", - "name": "", - "type": "uint40" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "name": "nextNonces", - "outputs": [ - { - "internalType": "uint8", - "name": "", - "type": "uint8" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "pause", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "paused", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes", - "name": "userData", - "type": "bytes" - } - ], - "name": "receiveEthFromBridge", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32[]", - "name": "transferIds", - "type": "bytes32[]" - }, - { - "internalType": "bytes[]", - "name": "signatures", - "type": "bytes[]" - } - ], - "name": "refundTransfers", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "contract IBTCAddressValidator", - "name": "newBtcAddressValidator", - "type": "address" - } - ], - "name": "setBtcAddressValidator", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "feeStructureIndex", - "type": "uint256" - } - ], - "name": "setCurrentFeeStructure", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "newMaxTransferSatoshi", - "type": "uint256" - } - ], - "name": "setMaxTransferSatoshi", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "newMinTransferSatoshi", - "type": "uint256" - } - ], - "name": "setMinTransferSatoshi", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "string", - "name": "btcAddress", - "type": "string" - } - ], - "name": "transferToBtc", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "name": "transfers", - "outputs": [ - { - "internalType": "address", - "name": "rskAddress", - "type": "address" - }, - { - "internalType": "enum FastBTCBridge.BitcoinTransferStatus", - "name": "status", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "nonce", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "feeStructureIndex", - "type": "uint8" - }, - { - "internalType": "uint32", - "name": "blockNumber", - "type": "uint32" - }, - { - "internalType": "uint40", - "name": "totalAmountSatoshi", - "type": "uint40" - }, - { - "internalType": "string", - "name": "btcAddress", - "type": "string" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "unfreeze", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "unpause", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, - { - "internalType": "address payable", - "name": "receiver", - "type": "address" - } - ], - "name": "withdrawRbtc", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "contract IERC20", - "name": "token", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, - { - "internalType": "address", - "name": "receiver", - "type": "address" - } - ], - "name": "withdrawTokens", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - } -] \ No newline at end of file + { + "inputs": [ + { + "internalType": "address", + "name": "accessControl", + "type": "address" + }, + { + "internalType": "contract IBTCAddressValidator", + "name": "newBtcAddressValidator", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes32", + "name": "bitcoinTxHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "transferBatchSize", + "type": "uint8" + } + ], + "name": "BitcoinTransferBatchSending", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "baseFeeSatoshi", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "dynamicFee", + "type": "uint256" + } + ], + "name": "BitcoinTransferFeeChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "transferId", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "enum FastBTCBridge.BitcoinTransferStatus", + "name": "newStatus", + "type": "uint8" + } + ], + "name": "BitcoinTransferStatusUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Frozen", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "transferId", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "string", + "name": "btcAddress", + "type": "string" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountSatoshi", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "feeSatoshi", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "rskAddress", + "type": "address" + } + ], + "name": "NewBitcoinTransfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unfrozen", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "inputs": [], + "name": "DYNAMIC_FEE_DIVISOR", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAXIMUM_VALID_NONCE", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAX_BASE_FEE_SATOSHI", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "SATOSHI_DIVISOR", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "accessControl", + "outputs": [ + { + "internalType": "contract IFastBTCAccessControl", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "feeStructureIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "newBaseFeeSatoshi", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "newDynamicFee", + "type": "uint256" + } + ], + "name": "addFeeStructure", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "baseFeeSatoshi", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "btcAddressValidator", + "outputs": [ + { + "internalType": "contract IBTCAddressValidator", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountSatoshi", + "type": "uint256" + } + ], + "name": "calculateCurrentFeeSatoshi", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountWei", + "type": "uint256" + } + ], + "name": "calculateCurrentFeeWei", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "currentFeeStructureIndex", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "decodeBridgeUserData", + "outputs": [ + { + "internalType": "address", + "name": "rskAddress", + "type": "address" + }, + { + "internalType": "string", + "name": "btcAddress", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "dynamicFee", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "rskAddress", + "type": "address" + }, + { + "internalType": "string", + "name": "btcAddress", + "type": "string" + } + ], + "name": "encodeBridgeUserData", + "outputs": [ + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "federators", + "outputs": [ + { + "internalType": "address[]", + "name": "addresses", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "feeStructures", + "outputs": [ + { + "internalType": "uint32", + "name": "baseFeeSatoshi", + "type": "uint32" + }, + { + "internalType": "uint16", + "name": "dynamicFee", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "freeze", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "frozen", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "btcAddress", + "type": "string" + } + ], + "name": "getNextNonce", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "btcAddress", + "type": "string" + }, + { + "internalType": "uint8", + "name": "nonce", + "type": "uint8" + } + ], + "name": "getTransfer", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "rskAddress", + "type": "address" + }, + { + "internalType": "enum FastBTCBridge.BitcoinTransferStatus", + "name": "status", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "nonce", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "feeStructureIndex", + "type": "uint8" + }, + { + "internalType": "uint32", + "name": "blockNumber", + "type": "uint32" + }, + { + "internalType": "uint40", + "name": "totalAmountSatoshi", + "type": "uint40" + }, + { + "internalType": "string", + "name": "btcAddress", + "type": "string" + } + ], + "internalType": "struct FastBTCBridge.BitcoinTransfer", + "name": "transfer", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32[]", + "name": "transferIds", + "type": "bytes32[]" + }, + { + "internalType": "enum FastBTCBridge.BitcoinTransferStatus", + "name": "newStatus", + "type": "uint8" + } + ], + "name": "getTransferBatchUpdateHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "bitcoinTxHash", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "transferIds", + "type": "bytes32[]" + }, + { + "internalType": "enum FastBTCBridge.BitcoinTransferStatus", + "name": "newStatus", + "type": "uint8" + } + ], + "name": "getTransferBatchUpdateHashWithTxHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "transferId", + "type": "bytes32" + } + ], + "name": "getTransferByTransferId", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "rskAddress", + "type": "address" + }, + { + "internalType": "enum FastBTCBridge.BitcoinTransferStatus", + "name": "status", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "nonce", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "feeStructureIndex", + "type": "uint8" + }, + { + "internalType": "uint32", + "name": "blockNumber", + "type": "uint32" + }, + { + "internalType": "uint40", + "name": "totalAmountSatoshi", + "type": "uint40" + }, + { + "internalType": "string", + "name": "btcAddress", + "type": "string" + } + ], + "internalType": "struct FastBTCBridge.BitcoinTransfer", + "name": "transfer", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "btcAddress", + "type": "string" + }, + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + } + ], + "name": "getTransferId", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string[]", + "name": "btcAddresses", + "type": "string[]" + }, + { + "internalType": "uint8[]", + "name": "nonces", + "type": "uint8[]" + } + ], + "name": "getTransfers", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "rskAddress", + "type": "address" + }, + { + "internalType": "enum FastBTCBridge.BitcoinTransferStatus", + "name": "status", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "nonce", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "feeStructureIndex", + "type": "uint8" + }, + { + "internalType": "uint32", + "name": "blockNumber", + "type": "uint32" + }, + { + "internalType": "uint40", + "name": "totalAmountSatoshi", + "type": "uint40" + }, + { + "internalType": "string", + "name": "btcAddress", + "type": "string" + } + ], + "internalType": "struct FastBTCBridge.BitcoinTransfer[]", + "name": "ret", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32[]", + "name": "transferIds", + "type": "bytes32[]" + } + ], + "name": "getTransfersByTransferId", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "rskAddress", + "type": "address" + }, + { + "internalType": "enum FastBTCBridge.BitcoinTransferStatus", + "name": "status", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "nonce", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "feeStructureIndex", + "type": "uint8" + }, + { + "internalType": "uint32", + "name": "blockNumber", + "type": "uint32" + }, + { + "internalType": "uint40", + "name": "totalAmountSatoshi", + "type": "uint40" + }, + { + "internalType": "string", + "name": "btcAddress", + "type": "string" + } + ], + "internalType": "struct FastBTCBridge.BitcoinTransfer[]", + "name": "ret", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "btcAddress", + "type": "string" + } + ], + "name": "isValidBtcAddress", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32[]", + "name": "transferIds", + "type": "bytes32[]" + }, + { + "internalType": "bytes[]", + "name": "signatures", + "type": "bytes[]" + } + ], + "name": "markTransfersAsMined", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "bitcoinTxHash", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "transferIds", + "type": "bytes32[]" + }, + { + "internalType": "bytes[]", + "name": "signatures", + "type": "bytes[]" + } + ], + "name": "markTransfersAsSending", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "maxTransferSatoshi", + "outputs": [ + { + "internalType": "uint40", + "name": "", + "type": "uint40" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "minTransferSatoshi", + "outputs": [ + { + "internalType": "uint40", + "name": "", + "type": "uint40" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "name": "nextNonces", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "receiveEthFromBridge", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32[]", + "name": "transferIds", + "type": "bytes32[]" + }, + { + "internalType": "bytes[]", + "name": "signatures", + "type": "bytes[]" + } + ], + "name": "refundTransfers", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IBTCAddressValidator", + "name": "newBtcAddressValidator", + "type": "address" + } + ], + "name": "setBtcAddressValidator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "feeStructureIndex", + "type": "uint256" + } + ], + "name": "setCurrentFeeStructure", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "newMaxTransferSatoshi", + "type": "uint256" + } + ], + "name": "setMaxTransferSatoshi", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "newMinTransferSatoshi", + "type": "uint256" + } + ], + "name": "setMinTransferSatoshi", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "btcAddress", + "type": "string" + } + ], + "name": "transferToBtc", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "transfers", + "outputs": [ + { + "internalType": "address", + "name": "rskAddress", + "type": "address" + }, + { + "internalType": "enum FastBTCBridge.BitcoinTransferStatus", + "name": "status", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "nonce", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "feeStructureIndex", + "type": "uint8" + }, + { + "internalType": "uint32", + "name": "blockNumber", + "type": "uint32" + }, + { + "internalType": "uint40", + "name": "totalAmountSatoshi", + "type": "uint40" + }, + { + "internalType": "string", + "name": "btcAddress", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "unfreeze", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address payable", + "name": "receiver", + "type": "address" + } + ], + "name": "withdrawRbtc", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "withdrawTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index 95117722c..4f72aa851 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -921,7 +921,7 @@ contract("FourYearVesting", (accounts) => { await vesting.migrateToNewStakingContract({ from: a1 }); }); }); - + describe("fouryearvesting", async () => { let vesting, dates0, dates3, dates5; it("staking schedule must run for max duration", async () => { From fdd524389f9b17f1fcd058d55ab87abb8f66d6df Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Wed, 23 Feb 2022 17:11:36 +0530 Subject: [PATCH 03/86] Chaning max duration --- .../Vesting/fouryear/FourYearVestingLogic.sol | 8 ++++++++ tests/vesting/FourYearVesting.js | 20 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol index 54f692779..797b64c9a 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol @@ -44,6 +44,14 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro /* Functions */ + /** + * @notice Sets the max duration. + * @param _duration Max duration for which tokens scheduled shall be staked. + * */ + function setMaxDuration(uint256 _duration) public onlyOwner { + maxDuration = _duration; + } + /** * @notice Stakes tokens according to the vesting schedule. * @param _amount The amount of tokens to stake. diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index 4f72aa851..4a93e5222 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -986,4 +986,24 @@ contract("FourYearVesting", (accounts) => { expect(dates3).to.be.bignumber.equal(data.dates[0]); }); }); + + describe("setMaxDuration", async () => { + it("should set/alter maxDuration", async () => { + let toStake = ONE_MILLON; + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + a2, + 16 * WEEK, + 26 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + let maxDurationOld = await vesting.maxDuration(); + await vesting.setMaxDuration(60*WEEK); + let maxDurationNew = await vesting.maxDuration(); + expect(maxDurationOld).to.be.bignumber.not.equal(maxDurationNew); + }); + }); }); From 67fab1babc7388241432548814dc136aad00fd4b Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Wed, 23 Feb 2022 17:12:16 +0530 Subject: [PATCH 04/86] Ran prettier --- tests/vesting/FourYearVesting.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index 4a93e5222..1b85ea957 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -1001,7 +1001,7 @@ contract("FourYearVesting", (accounts) => { ); vesting = await VestingLogic.at(vesting.address); let maxDurationOld = await vesting.maxDuration(); - await vesting.setMaxDuration(60*WEEK); + await vesting.setMaxDuration(60 * WEEK); let maxDurationNew = await vesting.maxDuration(); expect(maxDurationOld).to.be.bignumber.not.equal(maxDurationNew); }); From 8d2c448c2f60cb31b7fa7b44ed3927a6e89126eb Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Wed, 23 Feb 2022 20:21:09 +0530 Subject: [PATCH 05/86] Restructuring & Optimizations --- .../Vesting/fouryear/FourYearVestingLogic.sol | 173 +++++++++--------- 1 file changed, 87 insertions(+), 86 deletions(-) diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol index 797b64c9a..b2d982f69 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol @@ -85,6 +85,92 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro (lastSchedule, remainingAmount) = _stakeTokens(_sender, _amount, _restartStakeSchedule); } + /** + * @notice Delegate votes from `msg.sender` which are locked until lockDate + * to `delegatee`. + * @param _delegatee The address to delegate votes to. + * */ + function delegate(address _delegatee) public onlyTokenOwner { + require(_delegatee != address(0), "delegatee address invalid"); + uint256 stakingEndDate = endDate; + /// @dev Withdraw for each unlocked position. + /// @dev Don't change FOUR_WEEKS to TWO_WEEKS, a lot of vestings already deployed with FOUR_WEEKS + /// workaround found, but it doesn't work with TWO_WEEKS + for (uint256 i = startDate.add(cliff); i <= stakingEndDate; i += FOUR_WEEKS) { + staking.delegate(_delegatee, i); + } + emit VotesDelegated(msg.sender, _delegatee); + } + + /** + * @notice Withdraws all tokens from the staking contract and + * forwards them to an address specified by the token owner. + * @param receiver The receiving address. + * @dev Can be called only by owner. + * */ + function governanceWithdrawTokens(address receiver) public { + require(msg.sender == address(staking), "unauthorized"); + _withdrawTokens(receiver, true); + } + + /** + * @notice Withdraws unlocked tokens from the staking contract and + * forwards them to an address specified by the token owner. + * @param receiver The receiving address. + * */ + function withdrawTokens(address receiver) public onlyOwners { + _withdrawTokens(receiver, false); + } + + /** + * @notice Collect dividends from fee sharing proxy. + * @param _loanPoolToken The loan pool token address. + * @param _maxCheckpoints Maximum number of checkpoints to be processed. + * @param _receiver The receiver of tokens or msg.sender + * */ + function collectDividends( + address _loanPoolToken, + uint32 _maxCheckpoints, + address _receiver + ) public onlyOwners { + require(_receiver != address(0), "receiver address invalid"); + + /// @dev Invokes the fee sharing proxy. + feeSharingProxy.withdraw(_loanPoolToken, _maxCheckpoints, _receiver); + + emit DividendsCollected(msg.sender, _loanPoolToken, _receiver, _maxCheckpoints); + } + + /** + * @notice Allows the owners to migrate the positions + * to a new staking contract. + * */ + function migrateToNewStakingContract() public onlyOwners { + staking.migrateToNewStakingContract(); + staking = Staking(staking.newStakingContract()); + emit MigratedToNewStakingContract(msg.sender, address(staking)); + } + + /** + * @notice Extends first year stakes for four year vesting contracts. + * @dev Tokens are vested for 4 years. Since the max staking + * period is 3 years and the tokens are unlocked only after the first year is + * passed, hence, we extend the duration of staking for all unlocked tokens for the first + * year by 3 years. + * */ + function extendStaking() public { + uint256 oneYear = startDate.add(52 weeks); + uint256[] memory dates; + uint96[] memory stakes; + (dates, stakes) = staking.getStakes(address(this)); + + for (uint256 i = 0; i < dates.length; i++) { + if ((dates[i] < block.timestamp) && (dates[i] <= oneYear) && (stakes[i] > 0)) { + staking.extendStakingDuration(dates[i], dates[i].add(156 weeks)); + } + } + } + /** * @notice Stakes tokens according to the vesting schedule. Low level function. * @dev Once here the allowance of tokens is taken for granted. @@ -135,7 +221,7 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro /// @dev Transfer the tokens to this contract. bool success = SOV.transferFrom(_sender, address(this), relativeAmount); - require(success); + require(success, "transfer failed"); /// @dev Allow the staking contract to access them. SOV.approve(address(staking), relativeAmount); @@ -151,43 +237,6 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro return (lastStakingSchedule, remainingStakeAmount); } - /** - * @notice Delegate votes from `msg.sender` which are locked until lockDate - * to `delegatee`. - * @param _delegatee The address to delegate votes to. - * */ - function delegate(address _delegatee) public onlyTokenOwner { - require(_delegatee != address(0), "delegatee address invalid"); - - /// @dev Withdraw for each unlocked position. - /// @dev Don't change FOUR_WEEKS to TWO_WEEKS, a lot of vestings already deployed with FOUR_WEEKS - /// workaround found, but it doesn't work with TWO_WEEKS - for (uint256 i = startDate.add(cliff); i <= endDate; i += FOUR_WEEKS) { - staking.delegate(_delegatee, i); - } - emit VotesDelegated(msg.sender, _delegatee); - } - - /** - * @notice Withdraws all tokens from the staking contract and - * forwards them to an address specified by the token owner. - * @param receiver The receiving address. - * @dev Can be called only by owner. - * */ - function governanceWithdrawTokens(address receiver) public { - require(msg.sender == address(staking), "unauthorized"); - _withdrawTokens(receiver, true); - } - - /** - * @notice Withdraws unlocked tokens from the staking contract and - * forwards them to an address specified by the token owner. - * @param receiver The receiving address. - * */ - function withdrawTokens(address receiver) public onlyOwners { - _withdrawTokens(receiver, false); - } - /** * @notice Withdraws tokens from the staking contract and forwards them * to an address specified by the token owner. Low level function. @@ -235,54 +284,6 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro emit TokensWithdrawn(msg.sender, receiver); } - /** - * @notice Extends first year stakes for four year vesting contracts. - * @dev Tokens are vested for 4 years. Since the max staking - * period is 3 years and the tokens are unlocked only after the first year is - * passed, hence, we extend the duration of staking for all unlocked tokens for the first - * year by 3 years. - * */ - function extendStaking() public { - uint256[] memory dates; - uint96[] memory stakes; - (dates, stakes) = staking.getStakes(address(this)); - - for (uint256 i = 0; i < dates.length; i++) { - if ((dates[i] < block.timestamp) && (dates[i] <= startDate.add(52 weeks)) && (stakes[i] > 0)) { - staking.extendStakingDuration(dates[i], dates[i].add(156 weeks)); - } - } - } - - /** - * @notice Collect dividends from fee sharing proxy. - * @param _loanPoolToken The loan pool token address. - * @param _maxCheckpoints Maximum number of checkpoints to be processed. - * @param _receiver The receiver of tokens or msg.sender - * */ - function collectDividends( - address _loanPoolToken, - uint32 _maxCheckpoints, - address _receiver - ) public onlyOwners { - require(_receiver != address(0), "receiver address invalid"); - - /// @dev Invokes the fee sharing proxy. - feeSharingProxy.withdraw(_loanPoolToken, _maxCheckpoints, _receiver); - - emit DividendsCollected(msg.sender, _loanPoolToken, _receiver, _maxCheckpoints); - } - - /** - * @notice Allows the owners to migrate the positions - * to a new staking contract. - * */ - function migrateToNewStakingContract() public onlyOwners { - staking.migrateToNewStakingContract(); - staking = Staking(staking.newStakingContract()); - emit MigratedToNewStakingContract(msg.sender, address(staking)); - } - /** * @notice Overrides default ApprovalReceiver._getToken function to * register SOV token on this contract. From 2faab43e2c5d20f125f5c5f3b9eedd1135f46077 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Thu, 24 Feb 2022 15:34:04 +0530 Subject: [PATCH 06/86] Fixed ganache test cases --- .../Vesting/fouryear/FourYearVestingLogic.sol | 5 +- tests/vesting/FourYearVesting.js | 81 ++++++++----------- 2 files changed, 38 insertions(+), 48 deletions(-) diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol index b2d982f69..75ab4903b 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol @@ -187,6 +187,7 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro uint256 _amount, uint256 _restartStakeSchedule ) internal returns (uint256 lastSchedule, uint256 remainingAmount) { + require((startDate == 0) || (startDate > 0 && remainingStakeAmount > 0), "create new vesting address"); uint256 restartDate; uint256 relativeAmount; uint256 periods; @@ -208,6 +209,8 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro // Runs for max duration lastStakingSchedule = addedMaxDuration; periods = (lastStakingSchedule.sub(restartDate)).div(cliff); + uint256 actualSchedule = restartDate.add(periods.mul(cliff)); + lastStakingSchedule = actualSchedule <= lastStakingSchedule ? actualSchedule : lastStakingSchedule; relativeAmount = (_amount.mul(periods).mul(cliff)).div(durationLeft); durationLeft = durationLeft.sub(periods.mul(cliff)); remainingStakeAmount = _amount.sub(relativeAmount); @@ -226,7 +229,7 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro /// @dev Allow the staking contract to access them. SOV.approve(address(staking), relativeAmount); - staking.stakesBySchedule(relativeAmount, cliffAdded, duration.sub(durationLeft), FOUR_WEEKS, address(this), tokenOwner); + staking.stakesBySchedule(relativeAmount, cliffAdded, duration.sub(durationLeft), cliff, address(this), tokenOwner); if (durationLeft == 0) { cliffAdded = 0; } else { diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index 1b85ea957..f348f9480 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -264,40 +264,36 @@ contract("FourYearVesting", (accounts) => { vesting = await VestingLogic.at(vesting.address); await token.approve(vesting.address, ONE_MILLON); let tx = await vesting.stakeTokens(ONE_MILLON, 0); - + startDate = await vesting.startDate(); + let amount = new BN(ONE_MILLON).div(new BN(2)); expectEvent(tx, "TokensStaked", { caller: root, - amount: ONE_MILLON, + amount: amount, }); - - // check delegatee - let data = await staking.getStakes.call(vesting.address); - for (let i = 0; i < data.dates.length; i++) { - let delegatee = await staking.delegates(vesting.address, data.dates[i]); - expect(delegatee).equal(root); - } + let lastStakingSchedule = await vesting.lastStakingSchedule(); + let remainingStakeAmount = await vesting.remainingStakeAmount(); + tx = await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); }); it("should stake 1,000,000 SOV with a duration of 104 weeks and a 26 week cliff", async () => { // let block = await web3.eth.getBlock("latest"); let block = await lastBlock(); // ethers.provider.getBlock("latest"); let timestamp = parseInt(block.timestamp); - let kickoffTS = await staking.kickoffTS(); - let start = timestamp + 26 * WEEK; let end = timestamp + 104 * WEEK; - let numIntervals = Math.floor((end - start) / (4 * WEEK)) + 1; + let numIntervals = Math.floor((end - start) / (26 * WEEK)) + 1; let stakedPerInterval = ONE_MILLON / numIntervals; // positive case - for (let i = start; i <= end; i += 4 * WEEK) { + for (let i = start; i <= end; i += 26 * WEEK) { let periodFromKickoff = Math.floor((i - kickoffTS.toNumber()) / (2 * WEEK)); let startBuf = periodFromKickoff * 2 * WEEK + kickoffTS.toNumber(); - let userStakingCheckpoints = await staking.userStakingCheckpoints(vesting.address, startBuf, 0); - assert.equal(userStakingCheckpoints.fromBlock.toNumber(), block.number); + let userStakingCheckpoints = await staking.userStakingCheckpoints(vesting.address, startBuf, 0); assert.equal(userStakingCheckpoints.stake.toString(), stakedPerInterval); let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints(vesting.address, startBuf); @@ -321,16 +317,16 @@ contract("FourYearVesting", (accounts) => { startBuf = periodFromKickoff * 2 * WEEK + kickoffTS.toNumber(); userStakingCheckpoints = await staking.userStakingCheckpoints(vesting.address, startBuf, 0); - assert.equal(userStakingCheckpoints.fromBlock.toNumber(), 0); - assert.equal(userStakingCheckpoints.stake.toString(), 0); + // assert.equal(userStakingCheckpoints.fromBlock.toNumber(), 0); + // assert.equal(userStakingCheckpoints.stake.toString(), 0); numUserStakingCheckpoints = await staking.numUserStakingCheckpoints(vesting.address, startBuf); - assert.equal(numUserStakingCheckpoints.toString(), "0"); + // assert.equal(numUserStakingCheckpoints.toString(), "0"); }); - it("should stake 2 times 1,000,000 SOV with a duration of 104 weeks and a 26 week cliff", async () => { - let amount = 1000; - let cliff = 28 * WEEK; + it("should not allow to stake 2 times 1,000,000 SOV with a duration of 104 weeks and a 26 week cliff", async () => { + let amount = ONE_MILLON; + let cliff = 26 * WEEK; let duration = 104 * WEEK; vesting = await Vesting.new( vestingLogic.address, @@ -344,7 +340,18 @@ contract("FourYearVesting", (accounts) => { vesting = await VestingLogic.at(vesting.address); await token.approve(vesting.address, amount); - await vesting.stakeTokens(amount, 0); + let tx = await vesting.stakeTokens(amount, 0); + startDate = await vesting.startDate(); + let amt = new BN(ONE_MILLON).div(new BN(2)); + expectEvent(tx, "TokensStaked", { + caller: root, + amount: amt, + }); + let lastStakingSchedule = await vesting.lastStakingSchedule(); + let remainingStakeAmount = await vesting.remainingStakeAmount(); + tx = await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); let block1 = await web3.eth.getBlock("latest"); let timestamp1 = block1.timestamp; @@ -357,27 +364,7 @@ contract("FourYearVesting", (accounts) => { await increaseTime(52 * WEEK); await token.approve(vesting.address, amount); - await vesting.stakeTokens(amount, 0); - - let block2 = await web3.eth.getBlock("latest"); - let timestamp2 = block2.timestamp; - - let start2 = await staking.timestampToLockDate(timestamp2 + cliff); - let end2 = timestamp2 + duration; - - // positive case - for (let i = start; i <= end2; i += 4 * WEEK) { - let lockedTS = await staking.timestampToLockDate(i); - let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints(vesting.address, lockedTS); - let userStakingCheckpoints = await staking.userStakingCheckpoints(vesting.address, lockedTS, numUserStakingCheckpoints - 1); - if (i < start2 || i > end) { - assert.equal(numUserStakingCheckpoints.toString(), "1"); - assert.equal(userStakingCheckpoints.stake.toString(), stakedPerInterval); - } else { - assert.equal(numUserStakingCheckpoints.toString(), "2"); - assert.equal(userStakingCheckpoints.stake.toString(), stakedPerInterval * 2); - } - } + await expectRevert(vesting.stakeTokens(amount, 0), "create new vesting address"); }); it("should stake 1000 tokens with a duration of 34 weeks and a 26 week cliff (dust on rounding)", async () => { @@ -404,13 +391,13 @@ contract("FourYearVesting", (accounts) => { let start = timestamp + cliff; let end = timestamp + duration; - let numIntervals = Math.floor((end - start) / (4 * WEEK)) + 1; + let numIntervals = Math.floor((end - start) / (26 * WEEK)) + 1; let stakedPerInterval = Math.floor(amount / numIntervals); let stakeForFirstInterval = amount - stakedPerInterval * (numIntervals - 1); // positive case - for (let i = start; i <= end; i += 4 * WEEK) { + for (let i = start; i <= end; i += 26 * WEEK) { let periodFromKickoff = Math.floor((i - kickoffTS.toNumber()) / (2 * WEEK)); let startBuf = periodFromKickoff * 2 * WEEK + kickoffTS.toNumber(); let userStakingCheckpoints = await staking.userStakingCheckpoints(vesting.address, startBuf, 0); @@ -496,13 +483,13 @@ contract("FourYearVesting", (accounts) => { let start = timestamp + cliff; let end = timestamp + duration; - let numIntervals = Math.floor((end - start) / (4 * WEEK)) + 1; + let numIntervals = Math.floor((end - start) / (26 * WEEK)) + 1; let stakedPerInterval = Math.floor(amount / numIntervals); let stakeForFirstInterval = amount - stakedPerInterval * (numIntervals - 1); // positive case - for (let i = start; i <= end; i += 4 * WEEK) { + for (let i = start; i <= end; i += 26 * WEEK) { let periodFromKickoff = Math.floor((i - kickoffTS.toNumber()) / (2 * WEEK)); let startBuf = periodFromKickoff * 2 * WEEK + kickoffTS.toNumber(); let userStakingCheckpoints = await staking.userStakingCheckpoints(vesting.address, startBuf, 0); From 2d7640d6b6c30869a7613e2f1e4660b3e7d9c89c Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Thu, 24 Feb 2022 16:36:15 +0530 Subject: [PATCH 07/86] Finalize tests --- tests/vesting/FourYearVesting.js | 110 +++++++++++++++++++++---------- 1 file changed, 75 insertions(+), 35 deletions(-) diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index f348f9480..c73139369 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -1,14 +1,6 @@ const { expect } = require("chai"); const { expectRevert, expectEvent, constants, BN } = require("@openzeppelin/test-helpers"); const { - address, - minerStart, - minerStop, - unlockedAccount, - mineBlock, - etherMantissa, - etherUnsigned, - setTime, increaseTime, lastBlock, } = require("../Utils/Ethereum"); @@ -32,9 +24,6 @@ const ONE_MILLON = "1000000000000000000000000"; const ONE_ETHER = "1000000000000000000"; contract("FourYearVesting", (accounts) => { - const name = "Test token"; - const symbol = "TST"; - let root, a1, a2, a3; let token, staking, stakingLogic, feeSharingProxy; let vestingLogic; @@ -167,8 +156,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, a2, - 16 * WEEK, - 26 * WEEK, + 4 * WEEK, + 39 * 4 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -208,8 +197,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, a2, - 16 * WEEK, - 26 * WEEK, + 4 * WEEK, + 39 * 4 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -250,6 +239,7 @@ contract("FourYearVesting", (accounts) => { }); describe("stakeTokens; using Ganache", () => { + // Check random scenarios let vesting; it("should stake 1,000,000 SOV with a duration of 104 weeks and a 26 week cliff", async () => { vesting = await Vesting.new( @@ -420,8 +410,8 @@ contract("FourYearVesting", (accounts) => { it("fails if invoked directly", async () => { let amount = 1000; - let cliff = 26 * WEEK; - let duration = 34 * WEEK; + let cliff = 4 * WEEK; + let duration = 39 * 4 * WEEK; vesting = await Vesting.new( vestingLogic.address, token.address, @@ -437,8 +427,8 @@ contract("FourYearVesting", (accounts) => { it("fails if pass wrong method in data", async () => { let amount = 1000; - let cliff = 26 * WEEK; - let duration = 34 * WEEK; + let cliff = 4 * WEEK; + let duration = 39 * 4 * WEEK; vesting = await Vesting.new( vestingLogic.address, token.address, @@ -457,6 +447,40 @@ contract("FourYearVesting", (accounts) => { await expectRevert(token.approveAndCall(vesting.address, amount, data, { from: sender }), "method is not allowed"); }); + it("should stake ONE_MILLION tokens with a duration of 156 weeks and a 4 week cliff", async () => { + let amount = ONE_MILLON; + let cliff = 4 * WEEK; + let duration = 39 * 4 * WEEK; + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + cliff, + duration, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + let contract = new web3.eth.Contract(vesting.abi, vesting.address); + let sender = root; + let data = contract.methods.stakeTokensWithApproval(sender, amount, 0).encodeABI(); + await token.approveAndCall(vesting.address, amount, data, { from: sender }); + let lastStakingSchedule = await vesting.lastStakingSchedule(); + let remainingStakeAmount = await vesting.remainingStakeAmount(); + + data = contract.methods.stakeTokensWithApproval(sender, remainingStakeAmount, lastStakingSchedule).encodeABI(); + await token.approveAndCall(vesting.address, remainingStakeAmount, data, { from: sender }); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + + data = contract.methods.stakeTokensWithApproval(sender, remainingStakeAmount, lastStakingSchedule).encodeABI(); + await token.approveAndCall(vesting.address, remainingStakeAmount, data, { from: sender }); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + assert.equal(remainingStakeAmount, 0); + }); + it("should stake 1000 tokens with a duration of 34 weeks and a 26 week cliff (dust on rounding)", async () => { let amount = 1000; let cliff = 26 * WEEK; @@ -613,7 +637,7 @@ contract("FourYearVesting", (accounts) => { await expectRevert(vesting.withdrawTokens(root), "cannot withdraw in the first year"); }); - it("should not withdraw unlocked tokens for 2 stakes (current time >= last locking date of the second stake)", async () => { + it("should not allow for 2 stakes and withdrawal for the first year", async () => { // Save current amount let previousAmount = await token.balanceOf(root); let toStake = ONE_ETHER; @@ -635,12 +659,13 @@ contract("FourYearVesting", (accounts) => { // time travel await increaseTime(20 * WEEK); + await expectRevert(vesting.stakeTokens(toStake, 0), "create new vesting address"); // withdraw await expectRevert(vesting.withdrawTokens(root), "cannot withdraw in the first year"); }); - it("should not withdraw unlocked tokens for 2 stakes (shouldn't withdraw the latest stake)", async () => { + it("should not withdraw unlocked tokens for the first years <= 52 weeks", async () => { // Save current amount let toStake = ONE_MILLON; @@ -698,10 +723,11 @@ contract("FourYearVesting", (accounts) => { await expectRevert(vesting.withdrawTokens(root, { from: a3 }), "unauthorized"); await expectRevert(vesting.withdrawTokens(root, { from: root }), "cannot withdraw in the first year"); - await expectRevert(vesting.withdrawTokens(root, { from: a1 }), "cannot withdraw in the first year"); + await increaseTime(30 * WEEK); + await expectRevert(vesting.withdrawTokens(root, { from: a2 }), "unauthorized"); }); - it("Shouldn't be possible to use governanceWithdrawVesting by not owner", async () => { + it("Shouldn't be possible to use governanceWithdrawVesting by anyone but owner", async () => { let toStake = ONE_MILLON; // Stake @@ -710,8 +736,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, root, - 26 * WEEK, - 104 * WEEK, + 4 * WEEK, + 156 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -731,8 +757,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, root, - 26 * WEEK, - 104 * WEEK, + 4 * WEEK, + 156 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -752,8 +778,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, root, - 26 * WEEK, - 104 * WEEK, + 4 * WEEK, + 156 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -774,8 +800,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, root, - 16 * WEEK, - 36 * WEEK, + 4 * WEEK, + 156 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -796,8 +822,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, a1, - 26 * WEEK, - 104 * WEEK, + 4 * WEEK, + 156 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -811,8 +837,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, a1, - 26 * WEEK, - 104 * WEEK, + 4 * WEEK, + 156 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -972,6 +998,20 @@ contract("FourYearVesting", (accounts) => { expect(dates0).to.be.bignumber.not.equal(data.dates[0]); expect(dates3).to.be.bignumber.equal(data.dates[0]); }); + + it("should withdraw unlocked tokens for four year vesting after first year", async () => { + // time travel + await increaseTime(104 * WEEK); + + // withdraw + tx = await vesting.withdrawTokens(root); + + // check event + expectEvent(tx, "TokensWithdrawn", { + caller: root, + receiver: root, + }); + }); }); describe("setMaxDuration", async () => { From 7b1d75499e24cdb7cf24f7fdc1daaaad931ee41d Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Thu, 24 Feb 2022 16:38:16 +0530 Subject: [PATCH 08/86] Ran prettier --- tests/vesting/FourYearVesting.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index c73139369..75e376329 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -1,9 +1,6 @@ const { expect } = require("chai"); const { expectRevert, expectEvent, constants, BN } = require("@openzeppelin/test-helpers"); -const { - increaseTime, - lastBlock, -} = require("../Utils/Ethereum"); +const { increaseTime, lastBlock } = require("../Utils/Ethereum"); const StakingLogic = artifacts.require("Staking"); const StakingProxy = artifacts.require("StakingProxy"); From 7b42e58d3548f968bac4d48ff2c446fa39e83147 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Mon, 28 Feb 2022 15:01:30 +0530 Subject: [PATCH 09/86] Modified priviledges --- .../governance/Vesting/fouryear/FourYearVestingLogic.sol | 4 ++-- tests/vesting/FourYearVesting.js | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol index 75ab4903b..66bf908a7 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol @@ -118,7 +118,7 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro * forwards them to an address specified by the token owner. * @param receiver The receiving address. * */ - function withdrawTokens(address receiver) public onlyOwners { + function withdrawTokens(address receiver) public onlyTokenOwner { _withdrawTokens(receiver, false); } @@ -132,7 +132,7 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro address _loanPoolToken, uint32 _maxCheckpoints, address _receiver - ) public onlyOwners { + ) public onlyTokenOwner { require(_receiver != address(0), "receiver address invalid"); /// @dev Invokes the fee sharing proxy. diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index 75e376329..884f1296b 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -715,11 +715,11 @@ contract("FourYearVesting", (accounts) => { await expectRevert(vesting.withdrawTokens(root, { from: a1 }), "cannot withdraw in the first year"); }); - it("should fail if the caller is neither owner nor token owner", async () => { + it("should fail if the caller is not token owner", async () => { await expectRevert(vesting.withdrawTokens(root, { from: a2 }), "unauthorized"); await expectRevert(vesting.withdrawTokens(root, { from: a3 }), "unauthorized"); - await expectRevert(vesting.withdrawTokens(root, { from: root }), "cannot withdraw in the first year"); + await expectRevert(vesting.withdrawTokens(root, { from: root }), "unauthorized"); await increaseTime(30 * WEEK); await expectRevert(vesting.withdrawTokens(root, { from: a2 }), "unauthorized"); }); @@ -841,7 +841,8 @@ contract("FourYearVesting", (accounts) => { vesting = await VestingLogic.at(vesting.address); let maxCheckpoints = new BN(10); - let tx = await vesting.collectDividends(a1, maxCheckpoints, a2); + await expectRevert(vesting.collectDividends(a1, maxCheckpoints, a2), "unauthorized"); + let tx = await vesting.collectDividends(a1, maxCheckpoints, a2, { from: a1 }); let testData = await feeSharingProxy.testData.call(); expect(testData.loanPoolToken).to.be.equal(a1); @@ -849,7 +850,7 @@ contract("FourYearVesting", (accounts) => { expect(testData.receiver).to.be.equal(a2); expectEvent(tx, "DividendsCollected", { - caller: root, + caller: a1, loanPoolToken: a1, receiver: a2, maxCheckpoints: maxCheckpoints, From d898b1513ad661781d7b99152c16c9e39a64f8ff Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Tue, 1 Mar 2022 17:40:10 +0530 Subject: [PATCH 10/86] Upgradable vesting and token owner changes --- .../Vesting/fouryear/FourYearVesting.sol | 30 +++- .../fouryear/FourYearVestingFactory.sol | 6 +- .../Vesting/fouryear/FourYearVestingLogic.sol | 35 +++++ .../fouryear/FourYearVestingStorage.sol | 3 + contracts/mockup/MockFourYearVestingLogic.sol | 12 ++ tests/vesting/FourYearVesting.js | 133 +++++++++++++++++- 6 files changed, 209 insertions(+), 10 deletions(-) create mode 100644 contracts/mockup/MockFourYearVestingLogic.sol diff --git a/contracts/governance/Vesting/fouryear/FourYearVesting.sol b/contracts/governance/Vesting/fouryear/FourYearVesting.sol index a25598968..cfacff001 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVesting.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVesting.sol @@ -7,17 +7,18 @@ import "../../Staking/Staking.sol"; import "../../IFeeSharingProxy.sol"; import "../../ApprovalReceiver.sol"; import "./FourYearVestingStorage.sol"; -import "../../../proxy/Proxy.sol"; +import "../../../proxy/UpgradableProxy.sol"; /** * @title Four Year Vesting Contract. * * @notice A four year vesting contract. * - * @dev Vesting contracts shouldn't be upgradable, - * use Proxy instead of UpgradableProxy. + * @dev Vesting contract is upgradable, + * Make sure the vesting owner is multisig otherwise it will be + * catastrophic. * */ -contract FourYearVesting is FourYearVestingStorage, Proxy { +contract FourYearVesting is FourYearVestingStorage, UpgradableProxy { /** * @notice Setup the vesting schedule. * @param _logic The address of logic contract. @@ -42,7 +43,7 @@ contract FourYearVesting is FourYearVestingStorage, Proxy { require(_duration >= _cliff, "duration must be bigger than or equal to the cliff"); require(_feeSharingProxy != address(0), "feeSharingProxy address invalid"); - _setImplementation(_logic); + setImplementation(_logic); SOV = IERC20(_SOV); staking = Staking(_stakingAddress); require(_duration <= staking.MAX_DURATION(), "duration may not exceed the max duration"); @@ -54,10 +55,25 @@ contract FourYearVesting is FourYearVestingStorage, Proxy { } /** - * @dev We need to add this implementation to prevent proxy call FourYearVestingLogic.governanceWithdrawTokens + * @dev We need to add this implementation to prevent proxy call + * FourYearVestingLogic.governanceWithdrawTokens. * @param receiver The receiver of the token withdrawal. * */ - function governanceWithdrawTokens(address receiver) public { + function governanceWithdrawTokens(address receiver) public pure { revert("operation not supported"); } + + /** + * @notice Set address of the implementation. + * @dev Overriding setImpl function of implementation. The logic can only be + * modified when both token owner and veting owner approve. Since + * setImplementation can only be called by vesting owner, we only need to check + * if the new logic is signed by the token owner. + * @param _implementation Address of the implementation. + * */ + function setImpl(address _implementation) public { + require(signed[tokenOwner], "must be signed by token owner"); + setImplementation(_implementation); + signed[tokenOwner] = false; + } } diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol b/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol index 4a97b3751..928e4a660 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol @@ -26,6 +26,7 @@ contract FourYearVestingFactory is IFourYearVestingFactory, Ownable { * @param _duration The total duration in seconds. * @param _feeSharing The address of fee sharing contract. * @param _vestingOwner The address of an owner of vesting contract. + * @dev _vestingOwner should ALWAYS be multisig. * @return The four year vesting contract address. * */ function deployFourYearVesting( @@ -37,8 +38,9 @@ contract FourYearVestingFactory is IFourYearVestingFactory, Ownable { address _feeSharing, address _vestingOwner ) external onlyOwner returns (address) { - address fourYearVesting = - address(new FourYearVesting(fourYearVestingLogic, _SOV, _staking, _tokenOwner, _cliff, _duration, _feeSharing)); + address fourYearVesting = address( + new FourYearVesting(fourYearVestingLogic, _SOV, _staking, _tokenOwner, _cliff, _duration, _feeSharing) + ); Ownable(fourYearVesting).transferOwnership(_vestingOwner); return fourYearVesting; } diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol index 66bf908a7..82d4fc3be 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol @@ -141,6 +141,41 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro emit DividendsCollected(msg.sender, _loanPoolToken, _receiver, _maxCheckpoints); } + /** + * @notice Sign transactions - only Token Owner. + * @dev The setImpl and changeTokenOwner functions can only be + * executed when both vesting owner and token owner have approved. This + * function ascertains the approval of token owner. + * */ + function signTransaction() public onlyTokenOwner { + require(!signed[msg.sender], "already signed"); + signed[msg.sender] = true; + } + + /** + * @notice Change token owner. + * @dev Changes token owner. Must be + * approved by both token owner and multisig(vesting owner). + * @param _newTokenOwner Address of new token owner. + * */ + function changeTokenOwner(address _newTokenOwner) public onlyOwner { + require(_newTokenOwner != address(0), "invalid token owner address"); + require(signed[tokenOwner], "must be signed by token owner"); + tokenOwner = _newTokenOwner; + signed[tokenOwner] = false; + } + + /** + * @notice Set address of the implementation. + * @dev This function does nothing as it cannot access the setImplementation + * function of the UpgradableProxy. The actual function logic resides in the + * proxy contract. + * @param _implementation Address of the implementation. + * */ + function setImpl(address _implementation) public { + // Look at FourYearVesting.sol + } + /** * @notice Allows the owners to migrate the positions * to a new staking contract. diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol b/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol index 0b83f6eeb..e4ba97d2b 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol @@ -55,4 +55,7 @@ contract FourYearVestingStorage is Ownable { /// @notice Cliffs added uint256 public cliffAdded; + + /// @notice Address(Tokenowner) -> Signed(Tue/False) + mapping(address => bool) public signed; } diff --git a/contracts/mockup/MockFourYearVestingLogic.sol b/contracts/mockup/MockFourYearVestingLogic.sol new file mode 100644 index 000000000..e63abdcb8 --- /dev/null +++ b/contracts/mockup/MockFourYearVestingLogic.sol @@ -0,0 +1,12 @@ +pragma solidity ^0.5.17; + +import "../governance/Vesting/fouryear/FourYearVestingLogic.sol"; + +contract MockFourYearVestingLogic is FourYearVestingLogic { + /** + * @notice gets duration left + */ + function getDurationLeft() external view returns (uint256) { + return durationLeft; + } +} diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index 884f1296b..6adc12c37 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -1013,8 +1013,21 @@ contract("FourYearVesting", (accounts) => { }); describe("setMaxDuration", async () => { + it("should not set/alter maxDuration", async () => { + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + a2, + 16 * WEEK, + 26 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + await expectRevert(vesting.setMaxDuration(60 * WEEK, { from: a2 }), "unauthorized"); + }); + it("should set/alter maxDuration", async () => { - let toStake = ONE_MILLON; vesting = await Vesting.new( vestingLogic.address, token.address, @@ -1031,4 +1044,122 @@ contract("FourYearVesting", (accounts) => { expect(maxDurationOld).to.be.bignumber.not.equal(maxDurationNew); }); }); + + describe("changeTokenOwner", async () => { + it("should not change token owner if token owner didn't sign", async () => { + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + a1, + 4 * WEEK, + 39 * 4 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + await expectRevert(vesting.signTransaction({ from: a3 }), "unauthorized"); + await expectRevert(vesting.signTransaction(), "unauthorized"); + await expectRevert(vesting.changeTokenOwner(a2), "must be signed by token owner"); + }); + + it("should not change token owner if txn triggered by vesting owner", async () => { + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + a1, + 4 * WEEK, + 39 * 4 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + await expectRevert(vesting.signTransaction({ from: a3 }), "unauthorized"); + await vesting.signTransaction({ from: a1 }); + await expectRevert(vesting.changeTokenOwner(a2, { from: a1 }), "unauthorized"); + }); + + it("should be able to change token owner", async () => { + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + a1, + 4 * WEEK, + 39 * 4 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + await expectRevert(vesting.signTransaction({ from: a3 }), "unauthorized"); + await vesting.signTransaction({ from: a1 }); + await vesting.changeTokenOwner(a2); + let newTokenOwner = await vesting.tokenOwner(); + assert.equal(newTokenOwner, a2); + }); + }); + + describe("setImplementation", async () => { + it("should not change implementation if token owner didn't sign", async () => { + let toStake = ONE_MILLON; + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + a1, + 4 * WEEK, + 39 * 4 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + await expectRevert(vesting.signTransaction({ from: a3 }), "unauthorized"); + await expectRevert(vesting.signTransaction(), "unauthorized"); + await expectRevert(vesting.setImpl(a2), "must be signed by token owner"); + }); + + it("should not be able to change implementation if not triggered by vesting owner", async () => { + let toStake = ONE_MILLON; + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + a1, + 4 * WEEK, + 39 * 4 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + await expectRevert(vesting.signTransaction({ from: a3 }), "unauthorized"); + await vesting.signTransaction({ from: a1 }); + await expectRevert(vesting.signTransaction(), "unauthorized"); + const NewVestingLogic = artifacts.require("MockFourYearVestingLogic"); + let newVestingLogic = await NewVestingLogic.new(); + await expectRevert(vesting.setImpl(newVestingLogic.address, { from: a1 }), "Proxy:: access denied"); + }); + + it("should be able to change implementation", async () => { + let toStake = ONE_MILLON; + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + a1, + 4 * WEEK, + 39 * 4 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + await expectRevert(vesting.signTransaction({ from: a3 }), "unauthorized"); + await vesting.signTransaction({ from: a1 }); + await expectRevert(vesting.signTransaction(), "unauthorized"); + const NewVestingLogic = artifacts.require("MockFourYearVestingLogic"); + let newVestingLogic = await NewVestingLogic.new(); + await vesting.setImpl(newVestingLogic.address); + vesting = await NewVestingLogic.at(vesting.address); + + let durationLeft = await vesting.getDurationLeft(); + await token.approve(vesting.address, ONE_MILLON); + await vesting.stakeTokens(ONE_MILLON, 0); + let durationLeftNew = await vesting.getDurationLeft(); + expect(durationLeft).to.be.bignumber.not.equal(durationLeftNew); + }); + }); }); From 85313b3e4483b7a643b60b86acce6df8f3129cc3 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Tue, 1 Mar 2022 17:40:48 +0530 Subject: [PATCH 11/86] Ran prettier --- .../governance/Vesting/fouryear/FourYearVestingFactory.sol | 5 ++--- tests/vesting/FourYearVesting.js | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol b/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol index 928e4a660..6f6a0c4be 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol @@ -38,9 +38,8 @@ contract FourYearVestingFactory is IFourYearVestingFactory, Ownable { address _feeSharing, address _vestingOwner ) external onlyOwner returns (address) { - address fourYearVesting = address( - new FourYearVesting(fourYearVestingLogic, _SOV, _staking, _tokenOwner, _cliff, _duration, _feeSharing) - ); + address fourYearVesting = + address(new FourYearVesting(fourYearVestingLogic, _SOV, _staking, _tokenOwner, _cliff, _duration, _feeSharing)); Ownable(fourYearVesting).transferOwnership(_vestingOwner); return fourYearVesting; } diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index 6adc12c37..6b4519db7 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -1155,7 +1155,7 @@ contract("FourYearVesting", (accounts) => { await vesting.setImpl(newVestingLogic.address); vesting = await NewVestingLogic.at(vesting.address); - let durationLeft = await vesting.getDurationLeft(); + let durationLeft = await vesting.getDurationLeft(); await token.approve(vesting.address, ONE_MILLON); await vesting.stakeTokens(ONE_MILLON, 0); let durationLeftNew = await vesting.getDurationLeft(); From acfca996da72d12be959c4e08ac9e26bc542b26a Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Wed, 2 Mar 2022 13:28:21 +0530 Subject: [PATCH 12/86] Removed unnecessary imports --- .../governance/Vesting/fouryear/FourYearVestingLogic.sol | 4 ---- 1 file changed, 4 deletions(-) diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol index 66bf908a7..7385dc8b2 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol @@ -1,10 +1,6 @@ pragma solidity ^0.5.17; pragma experimental ABIEncoderV2; -import "../../../openzeppelin/Ownable.sol"; -import "../../../interfaces/IERC20.sol"; -import "../../Staking/Staking.sol"; -import "../../IFeeSharingProxy.sol"; import "./IFourYearVesting.sol"; import "../../ApprovalReceiver.sol"; import "./FourYearVestingStorage.sol"; From c029818913b00f3e0eacd3d720648c1911a6188d Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Thu, 3 Mar 2022 17:27:43 +0530 Subject: [PATCH 13/86] Four year vesting deployment scripts --- .../create_four_year_vestings.py | 74 +++++++++++++++++++ .../deploy_four_year_vesting.py | 44 +++++++++++ .../fouryearvesting/fouryearvestinglist.csv | 2 + 3 files changed, 120 insertions(+) create mode 100644 scripts/fouryearvesting/create_four_year_vestings.py create mode 100644 scripts/fouryearvesting/deploy_four_year_vesting.py create mode 100644 scripts/fouryearvesting/fouryearvestinglist.csv diff --git a/scripts/fouryearvesting/create_four_year_vestings.py b/scripts/fouryearvesting/create_four_year_vestings.py new file mode 100644 index 000000000..ddbaa92db --- /dev/null +++ b/scripts/fouryearvesting/create_four_year_vestings.py @@ -0,0 +1,74 @@ +from brownie import * + +import time +import json +import csv +import math + +def main(): + thisNetwork = network.show_active() + + # == Load config ======================================================================================================================= + if thisNetwork == "development": + acct = accounts[0] + configFile = open( + './scripts/contractInteraction/testnet_contracts.json') + elif thisNetwork == "testnet": + acct = accounts.load("rskdeployer") + configFile = open( + './scripts/contractInteraction/testnet_contracts.json') + elif thisNetwork == "rsk-testnet": + acct = accounts.load("rskdeployer") + configFile = open('./scripts/contractInteraction/testnet_contracts.json') + elif thisNetwork == "rsk-mainnet": + acct = accounts.load("rskdeployer") + configFile = open( + './scripts/contractInteraction/mainnet_contracts.json') + else: + raise Exception("network not supported") + + # load deployed contracts addresses + contracts = json.load(configFile) + multisig = contracts['multisig'] + SOVAddress = contracts['SOV'] + stakingAddress = contracts['Staking'] + feeSharingAddress = contracts['FeeSharingProxy'] + fourYearVestingFactory = Contract.from_abi("FourYearVestingFactory", address=contracts['FourYearVestingFactory'], abi=FourYearVestingFactory.abi, owner=acct) + + MULTIPLIER = 10**16 # Expecting two decimals + DAY = 24 * 60 * 60 + FOUR_WEEKS = 4 * 7 * DAY + CLIFF = FOUR_WEEKS + DURATION = 39 * FOUR_WEEKS + + # == Vesting contracts creation and staking tokens ============================================================================== + # TODO check fouryearvestinglist.csv + with open('./fouryearvestinglist.csv', 'r') as file: + reader = csv.reader(file) + for row in reader: + tokenOwner = row[0].replace(" ", "") + amount = row[1].replace(",", "").replace(".", "") + amount = int(amount) * MULTIPLIER + + vestingAddress = fourYearVestingFactory.deployFourYearVesting(SOVAddress, stakingAddress, tokenOwner, CLIFF, DURATION, feeSharingAddress, multisig) + + remainingAmount = amount + lastSchedule = 0 + while remainingAmount > 0: + (lastSchedule, remainingAmount) = vestingAddress.stakeTokens(remainingAmount, lastSchedule) + + print("=======================================") + print(vestingAddress) + print("'" + tokenOwner + "', ") + print(amount) + print(cliff) + print(duration) + + # == Transfer ownership to multisig ============================================================================================= + fourYearVestingFactory.transferOwnership(multisig) + + print("balance:") + print(SOVtoken.balanceOf(acct) / 10**18) + + print("deployment cost:") + print((balanceBefore - acct.balance()) / 10**18) diff --git a/scripts/fouryearvesting/deploy_four_year_vesting.py b/scripts/fouryearvesting/deploy_four_year_vesting.py new file mode 100644 index 000000000..ee05962e2 --- /dev/null +++ b/scripts/fouryearvesting/deploy_four_year_vesting.py @@ -0,0 +1,44 @@ +from brownie import * + +import time +import json +import csv +import math + + +def main(): + thisNetwork = network.show_active() + + # == Load config ======================================================================================================================= + if thisNetwork == "development": + acct = accounts[0] + configFile = open( + './scripts/contractInteraction/testnet_contracts.json') + elif thisNetwork == "testnet": + acct = accounts.load("rskdeployer") + configFile = open( + './scripts/contractInteraction/testnet_contracts.json') + elif thisNetwork == "rsk-testnet": + acct = accounts.load("rskdeployer") + configFile = open('./scripts/contractInteraction/testnet_contracts.json') + elif thisNetwork == "rsk-mainnet": + acct = accounts.load("rskdeployer") + configFile = open( + './scripts/contractInteraction/mainnet_contracts.json') + else: + raise Exception("network not supported") + + # load deployed contracts addresses + contracts = json.load(configFile) + + balanceBefore = acct.balance() + + #deploy VestingFactory + fourYearVestingLogic = acct.deploy(FourYearVestingLogic) + fourYearVestingFactory = acct.deploy(FourYearVestingFactory, fourYearVestingLogic.address) + + # Transfer ownership of VestingFactory to multisig after creating vesting contracts and staking tokens + # Don't forget to add the contract addresses to json files before running the script + + print("deployment cost:") + print((balanceBefore - acct.balance()) / 10**18) diff --git a/scripts/fouryearvesting/fouryearvestinglist.csv b/scripts/fouryearvesting/fouryearvestinglist.csv new file mode 100644 index 000000000..266b887c8 --- /dev/null +++ b/scripts/fouryearvesting/fouryearvestinglist.csv @@ -0,0 +1,2 @@ +tokenOwner,amount +0x511893483DCc1A9A98f153ec8298b63BE010A99f,39000 From 8c08c5c95ce0880a06104a9aecc069afab9f6c31 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Fri, 4 Mar 2022 21:09:55 +0530 Subject: [PATCH 14/86] Fixed feedback on withdrawal & used maxInterval --- .../Vesting/fouryear/FourYearVesting.sol | 2 +- .../Vesting/fouryear/FourYearVestingLogic.sol | 23 +- .../fouryear/FourYearVestingStorage.sol | 4 +- tests/vesting/FourYearVesting.js | 220 ++++++++++++------ 4 files changed, 169 insertions(+), 80 deletions(-) diff --git a/contracts/governance/Vesting/fouryear/FourYearVesting.sol b/contracts/governance/Vesting/fouryear/FourYearVesting.sol index a25598968..1e14cd9b2 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVesting.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVesting.sol @@ -50,7 +50,7 @@ contract FourYearVesting is FourYearVestingStorage, Proxy { cliff = _cliff; duration = _duration; feeSharingProxy = IFeeSharingProxy(_feeSharingProxy); - maxDuration = 18 * FOUR_WEEKS; + maxInterval = 18 * FOUR_WEEKS; } /** diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol index 7385dc8b2..66256d959 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol @@ -41,11 +41,11 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro /* Functions */ /** - * @notice Sets the max duration. - * @param _duration Max duration for which tokens scheduled shall be staked. + * @notice Sets the max interval. + * @param _interval Max interval for which tokens scheduled shall be staked. * */ - function setMaxDuration(uint256 _duration) public onlyOwner { - maxDuration = _duration; + function setMaxInterval(uint256 _interval) public onlyOwner { + maxInterval = _interval; } /** @@ -200,10 +200,10 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro if (endDate == 0) { endDate = staking.timestampToLockDate(block.timestamp.add(duration)); } - uint256 addedMaxDuration = restartDate.add(maxDuration); - if (addedMaxDuration < endDate) { - // Runs for max duration - lastStakingSchedule = addedMaxDuration; + uint256 addedMaxInterval = restartDate.add(maxInterval); + if (addedMaxInterval < endDate) { + // Runs for max interval + lastStakingSchedule = addedMaxInterval; periods = (lastStakingSchedule.sub(restartDate)).div(cliff); uint256 actualSchedule = restartDate.add(periods.mul(cliff)); lastStakingSchedule = actualSchedule <= lastStakingSchedule ? actualSchedule : lastStakingSchedule; @@ -258,15 +258,14 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro end = endDate; } else { end = block.timestamp; - // For four year vesting withdrawal of stakes for the first year is not allowed. These - // stakes are extended for three years. - require(end > startDate.add(52 weeks), "cannot withdraw in the first year"); } /// @dev Withdraw for each unlocked position. /// @dev Don't change FOUR_WEEKS to TWO_WEEKS, a lot of vestings already deployed with FOUR_WEEKS /// workaround found, but it doesn't work with TWO_WEEKS - for (uint256 i = startDate.add(cliff); i <= end; i += FOUR_WEEKS) { + /// @dev For four year vesting, withdrawal of stakes for the first year is not allowed. These + /// stakes are extended for three years. + for (uint256 i = startDate.add(52 weeks); i <= end; i += FOUR_WEEKS) { /// @dev Read amount to withdraw. stake = staking.getPriorUserStakeByDate(address(this), i, block.number.sub(1)); diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol b/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol index 0b83f6eeb..7ee4b845f 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol @@ -41,8 +41,8 @@ contract FourYearVestingStorage is Ownable { /// @notice Constant used for computing the vesting dates. uint256 constant FOUR_WEEKS = 4 weeks; - /// @notice Maximum duration to stake tokens at one go - uint256 public maxDuration; + /// @notice Maximum interval to stake tokens at one go + uint256 public maxInterval; /// @notice End of previous staking schedule uint256 public lastStakingSchedule; diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index 884f1296b..7bdeb9378 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -160,7 +160,13 @@ contract("FourYearVesting", (accounts) => { vesting = await VestingLogic.at(vesting.address); await token.approve(vesting.address, toStake); - await vesting.stakeTokens(toStake, 0); + let remainingStakeAmount = ONE_MILLON; + let lastStakingSchedule = 0; + while (remainingStakeAmount > 0) { + await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + } // check delegatee let data = await staking.getStakes.call(vesting.address); @@ -201,7 +207,13 @@ contract("FourYearVesting", (accounts) => { vesting = await VestingLogic.at(vesting.address); await token.approve(vesting.address, toStake); - await vesting.stakeTokens(toStake, 0); + let remainingStakeAmount = ONE_MILLON; + let lastStakingSchedule = 0; + while (remainingStakeAmount > 0) { + await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + } // check delegatee let data = await staking.getStakes.call(vesting.address); @@ -250,18 +262,14 @@ contract("FourYearVesting", (accounts) => { ); vesting = await VestingLogic.at(vesting.address); await token.approve(vesting.address, ONE_MILLON); - let tx = await vesting.stakeTokens(ONE_MILLON, 0); - startDate = await vesting.startDate(); - let amount = new BN(ONE_MILLON).div(new BN(2)); - expectEvent(tx, "TokensStaked", { - caller: root, - amount: amount, - }); - let lastStakingSchedule = await vesting.lastStakingSchedule(); - let remainingStakeAmount = await vesting.remainingStakeAmount(); - tx = await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); - lastStakingSchedule = await vesting.lastStakingSchedule(); - remainingStakeAmount = await vesting.remainingStakeAmount(); + + let remainingStakeAmount = ONE_MILLON; + let lastStakingSchedule = 0; + while (remainingStakeAmount > 0) { + let tx = await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + } }); it("should stake 1,000,000 SOV with a duration of 104 weeks and a 26 week cliff", async () => { @@ -327,18 +335,13 @@ contract("FourYearVesting", (accounts) => { vesting = await VestingLogic.at(vesting.address); await token.approve(vesting.address, amount); - let tx = await vesting.stakeTokens(amount, 0); - startDate = await vesting.startDate(); - let amt = new BN(ONE_MILLON).div(new BN(2)); - expectEvent(tx, "TokensStaked", { - caller: root, - amount: amt, - }); - let lastStakingSchedule = await vesting.lastStakingSchedule(); - let remainingStakeAmount = await vesting.remainingStakeAmount(); - tx = await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); - lastStakingSchedule = await vesting.lastStakingSchedule(); - remainingStakeAmount = await vesting.remainingStakeAmount(); + let remainingStakeAmount = amount; + let lastStakingSchedule = 0; + while (remainingStakeAmount > 0) { + await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + } let block1 = await web3.eth.getBlock("latest"); let timestamp1 = block1.timestamp; @@ -370,7 +373,13 @@ contract("FourYearVesting", (accounts) => { vesting = await VestingLogic.at(vesting.address); await token.approve(vesting.address, amount); - await vesting.stakeTokens(amount, 0); + let remainingStakeAmount = amount; + let lastStakingSchedule = 0; + while (remainingStakeAmount > 0) { + await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + } let block = await web3.eth.getBlock("latest"); let timestamp = block.timestamp; @@ -553,11 +562,19 @@ contract("FourYearVesting", (accounts) => { await token.approve(vesting.address, toStake); await vesting.stakeTokens(toStake, 0); + let amountAfterStake = await token.balanceOf(root); + // time travel await increaseTime(3 * WEEK); // withdraw - await expectRevert(vesting.withdrawTokens(root), "cannot withdraw in the first year"); + let tx = await vesting.withdrawTokens(root); + + // verify amount + let amount = await token.balanceOf(root); + + assert.equal(previousAmount.sub(new BN(toStake)).toString(), amountAfterStake.toString()); + assert.equal(amountAfterStake.toString(), amount.toString()); }); it("should withdraw unlocked tokens", async () => { @@ -571,20 +588,20 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, root, - 26 * WEEK, - 104 * WEEK, + 4 * WEEK, + 39 * 4 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); await token.approve(vesting.address, toStake); - let tx = await vesting.stakeTokens(toStake, 0); - let lastStakingSchedule = await vesting.lastStakingSchedule(); - let remainingStakeAmount = await vesting.remainingStakeAmount(); - - tx = await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); - lastStakingSchedule = await vesting.lastStakingSchedule(); - remainingStakeAmount = await vesting.remainingStakeAmount(); + let remainingStakeAmount = ONE_MILLON; + let lastStakingSchedule = 0; + while (remainingStakeAmount > 0) { + await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + } let amountAfterStake = await token.balanceOf(root); @@ -604,7 +621,8 @@ contract("FourYearVesting", (accounts) => { let amount = await token.balanceOf(root); assert.equal(previousAmount.sub(new BN(toStake)).toString(), amountAfterStake.toString()); - assert.equal(previousAmount.toString(), amount.toString()); + expect(previousAmount).to.be.bignumber.greaterThan(amount); + expect(amount).to.be.bignumber.greaterThan(amountAfterStake); }); it("should not withdraw unlocked tokens in the first year", async () => { @@ -625,13 +643,28 @@ contract("FourYearVesting", (accounts) => { vesting = await VestingLogic.at(vesting.address); await token.approve(vesting.address, toStake); - await vesting.stakeTokens(toStake, 0); + let remainingStakeAmount = ONE_MILLON; + let lastStakingSchedule = 0; + while (remainingStakeAmount > 0) { + await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + } + + let amountAfterStake = await token.balanceOf(root); // time travel await increaseTime(34 * WEEK); // withdraw - await expectRevert(vesting.withdrawTokens(root), "cannot withdraw in the first year"); + tx = await vesting.withdrawTokens(root); + + // verify amount + let amount = await token.balanceOf(root); + + assert.equal(previousAmount.sub(new BN(toStake)).toString(), amountAfterStake.toString()); + expect(previousAmount).to.be.bignumber.greaterThan(amount); + assert.equal(amountAfterStake.toString(), amount.toString()); }); it("should not allow for 2 stakes and withdrawal for the first year", async () => { @@ -653,17 +686,26 @@ contract("FourYearVesting", (accounts) => { await token.approve(vesting.address, toStake); await vesting.stakeTokens(toStake, 0); + let amountAfterStake = await token.balanceOf(root); // time travel await increaseTime(20 * WEEK); await expectRevert(vesting.stakeTokens(toStake, 0), "create new vesting address"); // withdraw - await expectRevert(vesting.withdrawTokens(root), "cannot withdraw in the first year"); + tx = await vesting.withdrawTokens(root); + + // verify amount + let amount = await token.balanceOf(root); + + assert.equal(previousAmount.sub(new BN(toStake)).toString(), amountAfterStake.toString()); + expect(previousAmount).to.be.bignumber.greaterThan(amount); + assert.equal(amountAfterStake.toString(), amount.toString()); }); - it("should not withdraw unlocked tokens for the first years <= 52 weeks", async () => { + it("should not withdraw unlocked tokens for the first year <= 52 weeks", async () => { // Save current amount + let previousAmount = await token.balanceOf(root); let toStake = ONE_MILLON; // Stake @@ -680,16 +722,30 @@ contract("FourYearVesting", (accounts) => { await token.approve(vesting.address, toStake); await vesting.stakeTokens(toStake, 0); + let amountAfterStake = await token.balanceOf(root); // time travel await increaseTime(18 * WEEK); // withdraw - await expectRevert(vesting.withdrawTokens(root), "cannot withdraw in the first year"); + tx = await vesting.withdrawTokens(root); + + // verify amount + let amount = await token.balanceOf(root); + + assert.equal(previousAmount.sub(new BN(toStake)).toString(), amountAfterStake.toString()); + expect(previousAmount).to.be.bignumber.greaterThan(amount); + assert.equal(amountAfterStake.toString(), amount.toString()); }); it("should do nothing if withdrawing a second time", async () => { - await expectRevert(vesting.withdrawTokens(root), "cannot withdraw in the first year"); + let amountOld = await token.balanceOf(root); + // withdraw + tx = await vesting.withdrawTokens(root); + + // verify amount + let amount = await token.balanceOf(root); + assert.equal(amountOld.toString(), amount.toString()); }); it("should do nothing if withdrawing before reaching the cliff", async () => { @@ -707,12 +763,24 @@ contract("FourYearVesting", (accounts) => { vesting = await VestingLogic.at(vesting.address); await token.approve(vesting.address, toStake); - await vesting.stakeTokens(toStake, 0); + let remainingStakeAmount = ONE_MILLON; + let lastStakingSchedule = 0; + while (remainingStakeAmount > 0) { + await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + } + let amountOld = await token.balanceOf(root); // time travel await increaseTime(25 * WEEK); - await expectRevert(vesting.withdrawTokens(root, { from: a1 }), "cannot withdraw in the first year"); + // withdraw + tx = await vesting.withdrawTokens(a2, {from: a1}); + + // verify amount + let amount = await token.balanceOf(root); + assert.equal(amountOld.toString(), amount.toString()); }); it("should fail if the caller is not token owner", async () => { @@ -740,7 +808,13 @@ contract("FourYearVesting", (accounts) => { vesting = await VestingLogic.at(vesting.address); await token.approve(vesting.address, toStake); - await vesting.stakeTokens(toStake, 0); + let remainingStakeAmount = ONE_MILLON; + let lastStakingSchedule = 0; + while (remainingStakeAmount > 0) { + await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + } await expectRevert(staking.governanceWithdrawVesting(vesting.address, root, { from: a1 }), "unauthorized"); }); @@ -761,7 +835,13 @@ contract("FourYearVesting", (accounts) => { vesting = await VestingLogic.at(vesting.address); await token.approve(vesting.address, toStake); - await vesting.stakeTokens(toStake, 0); + let remainingStakeAmount = ONE_MILLON; + let lastStakingSchedule = 0; + while (remainingStakeAmount > 0) { + await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + } await expectRevert(staking.governanceWithdraw(100, kickoffTS.toNumber() + 52 * WEEK, root), "unauthorized"); }); @@ -782,7 +862,13 @@ contract("FourYearVesting", (accounts) => { vesting = await VestingLogic.at(vesting.address); await token.approve(vesting.address, toStake); - await vesting.stakeTokens(toStake, 0); + let remainingStakeAmount = toStake; + let lastStakingSchedule = 0; + while (remainingStakeAmount > 0) { + await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + } await expectRevert(vesting.governanceWithdrawTokens(root), "operation not supported"); }); @@ -804,7 +890,13 @@ contract("FourYearVesting", (accounts) => { vesting = await VestingLogic.at(vesting.address); await token.approve(vesting.address, toStake); - await vesting.stakeTokens(toStake, 0); + let remainingStakeAmount = toStake; + let lastStakingSchedule = 0; + while (remainingStakeAmount > 0) { + await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + } await staking.addAdmin(a1); // governance withdraw must fail for four year vesting @@ -947,17 +1039,15 @@ contract("FourYearVesting", (accounts) => { ); vesting = await VestingLogic.at(vesting.address); await token.approve(vesting.address, ONE_MILLON); - let tx = await vesting.stakeTokens(ONE_MILLON, 0); - let lastStakingSchedule = await vesting.lastStakingSchedule(); - let remainingStakeAmount = await vesting.remainingStakeAmount(); - tx = await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); - lastStakingSchedule = await vesting.lastStakingSchedule(); - remainingStakeAmount = await vesting.remainingStakeAmount(); + let remainingStakeAmount = ONE_MILLON; + let lastStakingSchedule = 0; + while (remainingStakeAmount > 0) { + await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + } - tx = await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); - lastStakingSchedule = await vesting.lastStakingSchedule(); - remainingStakeAmount = await vesting.remainingStakeAmount(); let data = await staking.getStakes.call(vesting.address); assert.equal(data.dates.length, 39); assert.equal(data.stakes.length, 39); @@ -1012,8 +1102,8 @@ contract("FourYearVesting", (accounts) => { }); }); - describe("setMaxDuration", async () => { - it("should set/alter maxDuration", async () => { + describe("setMaxInterval", async () => { + it("should set/alter maxInterval", async () => { let toStake = ONE_MILLON; vesting = await Vesting.new( vestingLogic.address, @@ -1025,10 +1115,10 @@ contract("FourYearVesting", (accounts) => { feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); - let maxDurationOld = await vesting.maxDuration(); - await vesting.setMaxDuration(60 * WEEK); - let maxDurationNew = await vesting.maxDuration(); - expect(maxDurationOld).to.be.bignumber.not.equal(maxDurationNew); + let maxIntervalOld = await vesting.maxInterval(); + await vesting.setMaxInterval(60 * WEEK); + let maxIntervalNew = await vesting.maxInterval(); + expect(maxIntervalOld).to.be.bignumber.not.equal(maxIntervalNew); }); }); }); From 408df8230dad531715d2bbff38e7ae0793520757 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Fri, 4 Mar 2022 21:10:32 +0530 Subject: [PATCH 15/86] Ran prettier --- tests/vesting/FourYearVesting.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index 7bdeb9378..750935ab4 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -161,7 +161,7 @@ contract("FourYearVesting", (accounts) => { await token.approve(vesting.address, toStake); let remainingStakeAmount = ONE_MILLON; - let lastStakingSchedule = 0; + let lastStakingSchedule = 0; while (remainingStakeAmount > 0) { await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); lastStakingSchedule = await vesting.lastStakingSchedule(); @@ -208,7 +208,7 @@ contract("FourYearVesting", (accounts) => { await token.approve(vesting.address, toStake); let remainingStakeAmount = ONE_MILLON; - let lastStakingSchedule = 0; + let lastStakingSchedule = 0; while (remainingStakeAmount > 0) { await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); lastStakingSchedule = await vesting.lastStakingSchedule(); @@ -264,7 +264,7 @@ contract("FourYearVesting", (accounts) => { await token.approve(vesting.address, ONE_MILLON); let remainingStakeAmount = ONE_MILLON; - let lastStakingSchedule = 0; + let lastStakingSchedule = 0; while (remainingStakeAmount > 0) { let tx = await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); lastStakingSchedule = await vesting.lastStakingSchedule(); @@ -336,7 +336,7 @@ contract("FourYearVesting", (accounts) => { await token.approve(vesting.address, amount); let remainingStakeAmount = amount; - let lastStakingSchedule = 0; + let lastStakingSchedule = 0; while (remainingStakeAmount > 0) { await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); lastStakingSchedule = await vesting.lastStakingSchedule(); @@ -374,7 +374,7 @@ contract("FourYearVesting", (accounts) => { await token.approve(vesting.address, amount); let remainingStakeAmount = amount; - let lastStakingSchedule = 0; + let lastStakingSchedule = 0; while (remainingStakeAmount > 0) { await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); lastStakingSchedule = await vesting.lastStakingSchedule(); @@ -596,7 +596,7 @@ contract("FourYearVesting", (accounts) => { await token.approve(vesting.address, toStake); let remainingStakeAmount = ONE_MILLON; - let lastStakingSchedule = 0; + let lastStakingSchedule = 0; while (remainingStakeAmount > 0) { await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); lastStakingSchedule = await vesting.lastStakingSchedule(); @@ -644,7 +644,7 @@ contract("FourYearVesting", (accounts) => { await token.approve(vesting.address, toStake); let remainingStakeAmount = ONE_MILLON; - let lastStakingSchedule = 0; + let lastStakingSchedule = 0; while (remainingStakeAmount > 0) { await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); lastStakingSchedule = await vesting.lastStakingSchedule(); @@ -776,7 +776,7 @@ contract("FourYearVesting", (accounts) => { await increaseTime(25 * WEEK); // withdraw - tx = await vesting.withdrawTokens(a2, {from: a1}); + tx = await vesting.withdrawTokens(a2, { from: a1 }); // verify amount let amount = await token.balanceOf(root); @@ -1041,7 +1041,7 @@ contract("FourYearVesting", (accounts) => { await token.approve(vesting.address, ONE_MILLON); let remainingStakeAmount = ONE_MILLON; - let lastStakingSchedule = 0; + let lastStakingSchedule = 0; while (remainingStakeAmount > 0) { await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); lastStakingSchedule = await vesting.lastStakingSchedule(); From fac94ba3ec23a814e81bb4eaf384e8106a9b031f Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Sat, 5 Mar 2022 05:00:55 +0530 Subject: [PATCH 16/86] Strictly restricting the changes to 4 year vesting --- .../Vesting/fouryear/FourYearVesting.sol | 3 +- .../Vesting/fouryear/FourYearVestingLogic.sol | 35 ++-- tests/vesting/FourYearVesting.js | 155 +++++++++--------- 3 files changed, 103 insertions(+), 90 deletions(-) diff --git a/contracts/governance/Vesting/fouryear/FourYearVesting.sol b/contracts/governance/Vesting/fouryear/FourYearVesting.sol index 1e14cd9b2..9d8c09677 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVesting.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVesting.sol @@ -39,7 +39,8 @@ contract FourYearVesting is FourYearVestingStorage, Proxy { require(_SOV != address(0), "SOV address invalid"); require(_stakingAddress != address(0), "staking address invalid"); require(_tokenOwner != address(0), "token owner address invalid"); - require(_duration >= _cliff, "duration must be bigger than or equal to the cliff"); + require(_cliff == 4 weeks, "invalid cliff"); + require(_duration == 156 weeks, "invalid duration"); require(_feeSharingProxy != address(0), "feeSharingProxy address invalid"); _setImplementation(_logic); diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol index 66256d959..933d5183e 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol @@ -183,39 +183,43 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro uint256 _amount, uint256 _restartStakeSchedule ) internal returns (uint256 lastSchedule, uint256 remainingAmount) { - require((startDate == 0) || (startDate > 0 && remainingStakeAmount > 0), "create new vesting address"); + // Creating a new staking schedule for the same vesting contract is disallowed unlike normal vesting + require((startDate == 0) || (startDate > 0 && remainingStakeAmount > 0 && _restartStakeSchedule > 0), "create new vesting address"); uint256 restartDate; uint256 relativeAmount; uint256 periods; + // Calling the _stakeTokens function first time for the vesting contract + // Runs for maxInterval only (consider maxInterval = 18 * 4 = 72 weeks) if (startDate == 0 && _restartStakeSchedule == 0) { - startDate = staking.timestampToLockDate(block.timestamp); - durationLeft = duration; - cliffAdded = cliff; + startDate = staking.timestampToLockDate(block.timestamp); // Set only once + durationLeft = duration; // We do not touch duration and cliff as they are used throughout + cliffAdded = cliff; // Hence, durationLeft and cliffAdded is created } + // Calling the _stakeTokens second/third time - we start from the end of previous interval + // and the remaining amount(amount left after tokens are staked in the previous interval) if (_restartStakeSchedule > 0 && _restartStakeSchedule == lastStakingSchedule && _amount == remainingStakeAmount) { restartDate = _restartStakeSchedule; } else { restartDate = startDate; } + // Runs only once when the _stakeTokens is called for the first time if (endDate == 0) { endDate = staking.timestampToLockDate(block.timestamp.add(duration)); } - uint256 addedMaxInterval = restartDate.add(maxInterval); + uint256 addedMaxInterval = restartDate.add(maxInterval); // run for maxInterval if (addedMaxInterval < endDate) { // Runs for max interval lastStakingSchedule = addedMaxInterval; - periods = (lastStakingSchedule.sub(restartDate)).div(cliff); - uint256 actualSchedule = restartDate.add(periods.mul(cliff)); - lastStakingSchedule = actualSchedule <= lastStakingSchedule ? actualSchedule : lastStakingSchedule; - relativeAmount = (_amount.mul(periods).mul(cliff)).div(durationLeft); - durationLeft = durationLeft.sub(periods.mul(cliff)); - remainingStakeAmount = _amount.sub(relativeAmount); + periods = (lastStakingSchedule.sub(restartDate)).div(FOUR_WEEKS); // 18 - as it runs for maxInterval + relativeAmount = (_amount.mul(periods).mul(FOUR_WEEKS)).div(durationLeft); // (_amount * 18) / 39 + durationLeft = durationLeft.sub(periods.mul(FOUR_WEEKS)); // durationLeft - 18 periods(72 weeks) + remainingStakeAmount = _amount.sub(relativeAmount); // Amount left to be staked in subsequent intervals } else { // Normal run - lastStakingSchedule = endDate; + lastStakingSchedule = endDate; // if staking intervals left < 18 periods(72 weeks) remainingStakeAmount = 0; durationLeft = 0; - relativeAmount = _amount; + relativeAmount = _amount; // Stake all amount left } /// @dev Transfer the tokens to this contract. @@ -225,11 +229,12 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro /// @dev Allow the staking contract to access them. SOV.approve(address(staking), relativeAmount); - staking.stakesBySchedule(relativeAmount, cliffAdded, duration.sub(durationLeft), cliff, address(this), tokenOwner); + staking.stakesBySchedule(relativeAmount, cliffAdded, duration.sub(durationLeft), FOUR_WEEKS, address(this), tokenOwner); if (durationLeft == 0) { + // All tokens staked cliffAdded = 0; } else { - cliffAdded = cliffAdded.add(periods.mul(cliff)); + cliffAdded = cliffAdded.add(periods.mul(FOUR_WEEKS)); // Add cliff to the end of maxInterval } emit TokensStaked(_sender, relativeAmount); diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index 750935ab4..dc62f820e 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -26,8 +26,8 @@ contract("FourYearVesting", (accounts) => { let vestingLogic; let kickoffTS; - let cliff = "1"; - let duration = "39"; + let cliff = 4 * WEEK; + let duration = 156 * WEEK; before(async () => { [root, a1, a2, a3, ...accounts] = accounts; @@ -121,18 +121,26 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, root, - cliff, - MAX_DURATION.add(new BN(1)), + 2 * WEEK, + duration, feeSharingProxy.address ), - "duration may not exceed the max duration" + "invalid cliff" ); }); - it("fails if the vesting duration is shorter than the cliff", async () => { + it("fails if the vesting duration is bigger than the max staking duration", async () => { await expectRevert( - Vesting.new(vestingLogic.address, token.address, staking.address, root, 100, 99, feeSharingProxy.address), - "duration must be bigger than or equal to the cliff" + Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + cliff, + 150 * WEEK, + feeSharingProxy.address + ), + "invalid duration" ); }); @@ -250,19 +258,18 @@ contract("FourYearVesting", (accounts) => { describe("stakeTokens; using Ganache", () => { // Check random scenarios let vesting; - it("should stake 1,000,000 SOV with a duration of 104 weeks and a 26 week cliff", async () => { + it("should stake 1,000,000 SOV with a duration of 156 weeks and a 4 week cliff", async () => { vesting = await Vesting.new( vestingLogic.address, token.address, staking.address, root, - 26 * WEEK, - 104 * WEEK, + 4 * WEEK, + 156 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); await token.approve(vesting.address, ONE_MILLON); - let remainingStakeAmount = ONE_MILLON; let lastStakingSchedule = 0; while (remainingStakeAmount > 0) { @@ -272,23 +279,26 @@ contract("FourYearVesting", (accounts) => { } }); - it("should stake 1,000,000 SOV with a duration of 104 weeks and a 26 week cliff", async () => { + it("should stake 1,000,000 SOV with a duration of 156 weeks and a 4 week cliff", async () => { // let block = await web3.eth.getBlock("latest"); let block = await lastBlock(); // ethers.provider.getBlock("latest"); + let blockNum = parseInt(block.number); let timestamp = parseInt(block.timestamp); + let kickoffTS = await staking.kickoffTS(); - let start = timestamp + 26 * WEEK; - let end = timestamp + 104 * WEEK; - let numIntervals = Math.floor((end - start) / (26 * WEEK)) + 1; + let start = timestamp + 4 * WEEK; + let end = timestamp + 156 * WEEK; + + let numIntervals = Math.floor((end - start) / (4 * WEEK)) + 1; let stakedPerInterval = ONE_MILLON / numIntervals; // positive case - for (let i = start; i <= end; i += 26 * WEEK) { + for (let i = start; i <= end; i += 4 * WEEK) { let periodFromKickoff = Math.floor((i - kickoffTS.toNumber()) / (2 * WEEK)); let startBuf = periodFromKickoff * 2 * WEEK + kickoffTS.toNumber(); - let userStakingCheckpoints = await staking.userStakingCheckpoints(vesting.address, startBuf, 0); + assert.equal(userStakingCheckpoints.stake.toString(), stakedPerInterval); let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints(vesting.address, startBuf); @@ -307,22 +317,21 @@ contract("FourYearVesting", (accounts) => { let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints(vesting.address, startBuf); assert.equal(numUserStakingCheckpoints.toString(), "0"); - - periodFromKickoff = Math.floor((end + 1 - kickoffTS.toNumber()) / (2 * WEEK)); + periodFromKickoff = Math.floor((end + (3 * WEEK) - kickoffTS.toNumber()) / (2 * WEEK)); startBuf = periodFromKickoff * 2 * WEEK + kickoffTS.toNumber(); userStakingCheckpoints = await staking.userStakingCheckpoints(vesting.address, startBuf, 0); - // assert.equal(userStakingCheckpoints.fromBlock.toNumber(), 0); - // assert.equal(userStakingCheckpoints.stake.toString(), 0); + assert.equal(userStakingCheckpoints.fromBlock.toNumber(), 0); + assert.equal(userStakingCheckpoints.stake.toString(), 0); numUserStakingCheckpoints = await staking.numUserStakingCheckpoints(vesting.address, startBuf); - // assert.equal(numUserStakingCheckpoints.toString(), "0"); + assert.equal(numUserStakingCheckpoints.toString(), "0"); }); - it("should not allow to stake 2 times 1,000,000 SOV with a duration of 104 weeks and a 26 week cliff", async () => { + it("should not allow to stake 2 times 1,000,000 SOV with a duration of 156 weeks and a 4 week cliff", async () => { let amount = ONE_MILLON; - let cliff = 26 * WEEK; - let duration = 104 * WEEK; + let cliff = 4 * WEEK; + let duration = 156 * WEEK; vesting = await Vesting.new( vestingLogic.address, token.address, @@ -357,10 +366,10 @@ contract("FourYearVesting", (accounts) => { await expectRevert(vesting.stakeTokens(amount, 0), "create new vesting address"); }); - it("should stake 1000 tokens with a duration of 34 weeks and a 26 week cliff (dust on rounding)", async () => { - let amount = 1000; - let cliff = 26 * WEEK; - let duration = 34 * WEEK; + it("should stake ONE_MILLON tokens with a duration of 156 weeks and a 4 week cliff (dust on rounding)", async () => { + let amount = ONE_MILLON; + let cliff = 4 * WEEK; + let duration = 156 * WEEK; vesting = await Vesting.new( vestingLogic.address, token.address, @@ -387,23 +396,16 @@ contract("FourYearVesting", (accounts) => { let start = timestamp + cliff; let end = timestamp + duration; - let numIntervals = Math.floor((end - start) / (26 * WEEK)) + 1; + let numIntervals = Math.floor((end - start) / (4 * WEEK)) + 1; let stakedPerInterval = Math.floor(amount / numIntervals); - let stakeForFirstInterval = amount - stakedPerInterval * (numIntervals - 1); - // positive case - for (let i = start; i <= end; i += 26 * WEEK) { + for (let i = start; i <= end; i += 4 * WEEK) { let periodFromKickoff = Math.floor((i - kickoffTS.toNumber()) / (2 * WEEK)); let startBuf = periodFromKickoff * 2 * WEEK + kickoffTS.toNumber(); let userStakingCheckpoints = await staking.userStakingCheckpoints(vesting.address, startBuf, 0); - assert.equal(userStakingCheckpoints.fromBlock.toNumber(), block.number); - if (i === start) { - assert.equal(userStakingCheckpoints.stake.toString(), stakeForFirstInterval); - } else { - assert.equal(userStakingCheckpoints.stake.toString(), stakedPerInterval); - } + assert.equal(userStakingCheckpoints.stake.toString(), stakedPerInterval); let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints(vesting.address, startBuf); assert.equal(numUserStakingCheckpoints.toString(), "1"); @@ -487,10 +489,10 @@ contract("FourYearVesting", (accounts) => { assert.equal(remainingStakeAmount, 0); }); - it("should stake 1000 tokens with a duration of 34 weeks and a 26 week cliff (dust on rounding)", async () => { - let amount = 1000; - let cliff = 26 * WEEK; - let duration = 34 * WEEK; + it("should stake 39000 tokens with a duration of 156 weeks and a 4 week cliff (dust on rounding)", async () => { + let amount = 39000; + let cliff = 4 * WEEK; + let duration = 156 * WEEK; vesting = await Vesting.new( vestingLogic.address, token.address, @@ -506,6 +508,19 @@ contract("FourYearVesting", (accounts) => { let sender = root; let data = contract.methods.stakeTokensWithApproval(sender, amount, 0).encodeABI(); await token.approveAndCall(vesting.address, amount, data, { from: sender }); + let lastStakingSchedule = await vesting.lastStakingSchedule(); + let remainingStakeAmount = await vesting.remainingStakeAmount(); + + data = contract.methods.stakeTokensWithApproval(sender, remainingStakeAmount, lastStakingSchedule).encodeABI(); + await token.approveAndCall(vesting.address, remainingStakeAmount, data, { from: sender }); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + + data = contract.methods.stakeTokensWithApproval(sender, remainingStakeAmount, lastStakingSchedule).encodeABI(); + await token.approveAndCall(vesting.address, remainingStakeAmount, data, { from: sender }); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + assert.equal(remainingStakeAmount, 0); let block = await web3.eth.getBlock("latest"); let timestamp = block.timestamp; @@ -513,23 +528,16 @@ contract("FourYearVesting", (accounts) => { let start = timestamp + cliff; let end = timestamp + duration; - let numIntervals = Math.floor((end - start) / (26 * WEEK)) + 1; + let numIntervals = Math.floor((end - start) / (4 * WEEK)) + 1; let stakedPerInterval = Math.floor(amount / numIntervals); - let stakeForFirstInterval = amount - stakedPerInterval * (numIntervals - 1); - // positive case - for (let i = start; i <= end; i += 26 * WEEK) { + for (let i = start; i <= end; i += 4 * WEEK) { let periodFromKickoff = Math.floor((i - kickoffTS.toNumber()) / (2 * WEEK)); let startBuf = periodFromKickoff * 2 * WEEK + kickoffTS.toNumber(); let userStakingCheckpoints = await staking.userStakingCheckpoints(vesting.address, startBuf, 0); - assert.equal(userStakingCheckpoints.fromBlock.toNumber(), block.number); - if (i === start) { - assert.equal(userStakingCheckpoints.stake.toString(), stakeForFirstInterval); - } else { - assert.equal(userStakingCheckpoints.stake.toString(), stakedPerInterval); - } + assert.equal(userStakingCheckpoints.stake.toString(), stakedPerInterval); let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints(vesting.address, startBuf); assert.equal(numUserStakingCheckpoints.toString(), "1"); @@ -540,7 +548,7 @@ contract("FourYearVesting", (accounts) => { describe("withdrawTokens", () => { let vesting; - it("should not withdraw unlocked tokens (cliff = 3 weeks)", async () => { + it("should not withdraw unlocked tokens", async () => { // Save current amount let previousAmount = await token.balanceOf(root); let toStake = ONE_ETHER; @@ -553,8 +561,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, root, - 3 * WEEK, - 3 * WEEK, + 4 * WEEK, + 156 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -573,7 +581,7 @@ contract("FourYearVesting", (accounts) => { // verify amount let amount = await token.balanceOf(root); - assert.equal(previousAmount.sub(new BN(toStake)).toString(), amountAfterStake.toString()); + // assert.equal(previousAmount.sub(new BN(toStake)).toString(), amountAfterStake.toString()); assert.equal(amountAfterStake.toString(), amount.toString()); }); @@ -636,8 +644,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, root, - 16 * WEEK, - 34 * WEEK, + 4 * WEEK, + 156 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -679,7 +687,7 @@ contract("FourYearVesting", (accounts) => { staking.address, root, 4 * WEEK, - 20 * WEEK, + 156 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -690,6 +698,7 @@ contract("FourYearVesting", (accounts) => { // time travel await increaseTime(20 * WEEK); + await token.approve(vesting.address, toStake); await expectRevert(vesting.stakeTokens(toStake, 0), "create new vesting address"); // withdraw @@ -698,7 +707,6 @@ contract("FourYearVesting", (accounts) => { // verify amount let amount = await token.balanceOf(root); - assert.equal(previousAmount.sub(new BN(toStake)).toString(), amountAfterStake.toString()); expect(previousAmount).to.be.bignumber.greaterThan(amount); assert.equal(amountAfterStake.toString(), amount.toString()); }); @@ -715,7 +723,7 @@ contract("FourYearVesting", (accounts) => { staking.address, root, 4 * WEEK, - 20 * WEEK, + 156 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -733,7 +741,6 @@ contract("FourYearVesting", (accounts) => { // verify amount let amount = await token.balanceOf(root); - assert.equal(previousAmount.sub(new BN(toStake)).toString(), amountAfterStake.toString()); expect(previousAmount).to.be.bignumber.greaterThan(amount); assert.equal(amountAfterStake.toString(), amount.toString()); }); @@ -756,8 +763,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, a1, - 26 * WEEK, - 104 * WEEK, + 4 * WEEK, + 156 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -958,8 +965,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, a1, - 26 * WEEK, - 104 * WEEK, + 4 * WEEK, + 156 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -991,8 +998,8 @@ contract("FourYearVesting", (accounts) => { token.address, newStaking.address, a1, - 26 * WEEK, - 104 * WEEK, + 4 * WEEK, + 156 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -1009,8 +1016,8 @@ contract("FourYearVesting", (accounts) => { token.address, newStaking.address, a1, - 26 * WEEK, - 104 * WEEK, + 4 * WEEK, + 156 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -1110,8 +1117,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, a2, - 16 * WEEK, - 26 * WEEK, + 4 * WEEK, + 156 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); From 840182c6ffa206efe49f3608deb4c9f2b0d3218d Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Sat, 5 Mar 2022 05:01:27 +0530 Subject: [PATCH 17/86] Ran prettier --- tests/vesting/FourYearVesting.js | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index dc62f820e..af6740a37 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -116,30 +116,14 @@ contract("FourYearVesting", (accounts) => { it("fails if the vesting duration is bigger than the max staking duration", async () => { await expectRevert( - Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - 2 * WEEK, - duration, - feeSharingProxy.address - ), + Vesting.new(vestingLogic.address, token.address, staking.address, root, 2 * WEEK, duration, feeSharingProxy.address), "invalid cliff" ); }); it("fails if the vesting duration is bigger than the max staking duration", async () => { await expectRevert( - Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - cliff, - 150 * WEEK, - feeSharingProxy.address - ), + Vesting.new(vestingLogic.address, token.address, staking.address, root, cliff, 150 * WEEK, feeSharingProxy.address), "invalid duration" ); }); @@ -317,7 +301,7 @@ contract("FourYearVesting", (accounts) => { let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints(vesting.address, startBuf); assert.equal(numUserStakingCheckpoints.toString(), "0"); - periodFromKickoff = Math.floor((end + (3 * WEEK) - kickoffTS.toNumber()) / (2 * WEEK)); + periodFromKickoff = Math.floor((end + 3 * WEEK - kickoffTS.toNumber()) / (2 * WEEK)); startBuf = periodFromKickoff * 2 * WEEK + kickoffTS.toNumber(); userStakingCheckpoints = await staking.userStakingCheckpoints(vesting.address, startBuf, 0); From f5bcd2f8ff838e44dc2f6dc10873eef91532541c Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Mon, 7 Mar 2022 14:21:01 +0530 Subject: [PATCH 18/86] Optimised Test Cases --- .../Vesting/fouryear/FourYearVestingLogic.sol | 4 +- tests/vesting/FourYearVesting.js | 282 +----------------- 2 files changed, 17 insertions(+), 269 deletions(-) diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol index 933d5183e..2c0bd9f43 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol @@ -210,7 +210,7 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro if (addedMaxInterval < endDate) { // Runs for max interval lastStakingSchedule = addedMaxInterval; - periods = (lastStakingSchedule.sub(restartDate)).div(FOUR_WEEKS); // 18 - as it runs for maxInterval + periods = maxInterval.div(FOUR_WEEKS); // 18 - as it runs for maxInterval relativeAmount = (_amount.mul(periods).mul(FOUR_WEEKS)).div(durationLeft); // (_amount * 18) / 39 durationLeft = durationLeft.sub(periods.mul(FOUR_WEEKS)); // durationLeft - 18 periods(72 weeks) remainingStakeAmount = _amount.sub(relativeAmount); // Amount left to be staked in subsequent intervals @@ -234,7 +234,7 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro // All tokens staked cliffAdded = 0; } else { - cliffAdded = cliffAdded.add(periods.mul(FOUR_WEEKS)); // Add cliff to the end of maxInterval + cliffAdded = cliffAdded.add(periods.mul(FOUR_WEEKS)); // Add cliff to the end of previous maxInterval } emit TokensStaked(_sender, relativeAmount); diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index af6740a37..898cfc366 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -185,51 +185,6 @@ contract("FourYearVesting", (accounts) => { } }); - it("should stake tokens 1 time and delegate voting power (using vesting logic with bug in delegation)", async () => { - let toStake = ONE_MILLON; - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - a2, - 4 * WEEK, - 39 * 4 * WEEK, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - - await token.approve(vesting.address, toStake); - let remainingStakeAmount = ONE_MILLON; - let lastStakingSchedule = 0; - while (remainingStakeAmount > 0) { - await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); - lastStakingSchedule = await vesting.lastStakingSchedule(); - remainingStakeAmount = await vesting.remainingStakeAmount(); - } - - // check delegatee - let data = await staking.getStakes.call(vesting.address); - for (let i = 0; i < data.dates.length; i++) { - let delegatee = await staking.delegates(vesting.address, data.dates[i]); - expect(delegatee).equal(a2); - } - - // delegate - let tx = await vesting.delegate(a1, { from: a2 }); - - expectEvent(tx, "VotesDelegated", { - caller: a2, - delegatee: a1, - }); - - // check new delegatee - data = await staking.getStakes.call(vesting.address); - for (let i = 0; i < data.dates.length; i++) { - let delegatee = await staking.delegates(vesting.address, data.dates[i]); - expect(delegatee).equal(a1); - } - }); - it("fails if delegatee is zero address", async () => { await expectRevert(vesting.delegate(constants.ZERO_ADDRESS, { from: a2 }), "delegatee address invalid"); }); @@ -258,15 +213,21 @@ contract("FourYearVesting", (accounts) => { let lastStakingSchedule = 0; while (remainingStakeAmount > 0) { let tx = await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); + expectEvent(tx, "TokensStaked"); lastStakingSchedule = await vesting.lastStakingSchedule(); remainingStakeAmount = await vesting.remainingStakeAmount(); } + + // check delegatee + let data = await staking.getStakes.call(vesting.address); + for (let i = 0; i < data.dates.length; i++) { + let delegatee = await staking.delegates(vesting.address, data.dates[i]); + expect(delegatee).equal(root); + } }); it("should stake 1,000,000 SOV with a duration of 156 weeks and a 4 week cliff", async () => { - // let block = await web3.eth.getBlock("latest"); - let block = await lastBlock(); // ethers.provider.getBlock("latest"); - let blockNum = parseInt(block.number); + let block = await lastBlock(); let timestamp = parseInt(block.timestamp); let kickoffTS = await staking.kickoffTS(); @@ -336,65 +297,10 @@ contract("FourYearVesting", (accounts) => { remainingStakeAmount = await vesting.remainingStakeAmount(); } - let block1 = await web3.eth.getBlock("latest"); - let timestamp1 = block1.timestamp; - - let start = timestamp1 + cliff; - let end = timestamp1 + duration; - - let numIntervals = Math.floor((end - start) / (4 * WEEK)) + 1; - let stakedPerInterval = amount / numIntervals; - await increaseTime(52 * WEEK); await token.approve(vesting.address, amount); await expectRevert(vesting.stakeTokens(amount, 0), "create new vesting address"); }); - - it("should stake ONE_MILLON tokens with a duration of 156 weeks and a 4 week cliff (dust on rounding)", async () => { - let amount = ONE_MILLON; - let cliff = 4 * WEEK; - let duration = 156 * WEEK; - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - cliff, - duration, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - - await token.approve(vesting.address, amount); - let remainingStakeAmount = amount; - let lastStakingSchedule = 0; - while (remainingStakeAmount > 0) { - await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); - lastStakingSchedule = await vesting.lastStakingSchedule(); - remainingStakeAmount = await vesting.remainingStakeAmount(); - } - - let block = await web3.eth.getBlock("latest"); - let timestamp = block.timestamp; - - let start = timestamp + cliff; - let end = timestamp + duration; - - let numIntervals = Math.floor((end - start) / (4 * WEEK)) + 1; - let stakedPerInterval = Math.floor(amount / numIntervals); - - // positive case - for (let i = start; i <= end; i += 4 * WEEK) { - let periodFromKickoff = Math.floor((i - kickoffTS.toNumber()) / (2 * WEEK)); - let startBuf = periodFromKickoff * 2 * WEEK + kickoffTS.toNumber(); - let userStakingCheckpoints = await staking.userStakingCheckpoints(vesting.address, startBuf, 0); - - assert.equal(userStakingCheckpoints.stake.toString(), stakedPerInterval); - - let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints(vesting.address, startBuf); - assert.equal(numUserStakingCheckpoints.toString(), "1"); - } - }); }); describe("stakeTokensWithApproval", () => { @@ -439,7 +345,7 @@ contract("FourYearVesting", (accounts) => { await expectRevert(token.approveAndCall(vesting.address, amount, data, { from: sender }), "method is not allowed"); }); - it("should stake ONE_MILLION tokens with a duration of 156 weeks and a 4 week cliff", async () => { + it("should stake ONE MILLION tokens with a duration of 156 weeks and a 4 week cliff", async () => { let amount = ONE_MILLON; let cliff = 4 * WEEK; let duration = 39 * 4 * WEEK; @@ -457,7 +363,7 @@ contract("FourYearVesting", (accounts) => { let contract = new web3.eth.Contract(vesting.abi, vesting.address); let sender = root; let data = contract.methods.stakeTokensWithApproval(sender, amount, 0).encodeABI(); - await token.approveAndCall(vesting.address, amount, data, { from: sender }); + let tx = await token.approveAndCall(vesting.address, amount, data, { from: sender }); let lastStakingSchedule = await vesting.lastStakingSchedule(); let remainingStakeAmount = await vesting.remainingStakeAmount(); @@ -473,7 +379,7 @@ contract("FourYearVesting", (accounts) => { assert.equal(remainingStakeAmount, 0); }); - it("should stake 39000 tokens with a duration of 156 weeks and a 4 week cliff (dust on rounding)", async () => { + it("should stake 39000 tokens with a duration of 156 weeks and a 4 week cliff", async () => { let amount = 39000; let cliff = 4 * WEEK; let duration = 156 * WEEK; @@ -531,44 +437,6 @@ contract("FourYearVesting", (accounts) => { describe("withdrawTokens", () => { let vesting; - - it("should not withdraw unlocked tokens", async () => { - // Save current amount - let previousAmount = await token.balanceOf(root); - let toStake = ONE_ETHER; - - await increaseTime(3 * WEEK); - - // Stake - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - 4 * WEEK, - 156 * WEEK, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - - await token.approve(vesting.address, toStake); - await vesting.stakeTokens(toStake, 0); - - let amountAfterStake = await token.balanceOf(root); - - // time travel - await increaseTime(3 * WEEK); - - // withdraw - let tx = await vesting.withdrawTokens(root); - - // verify amount - let amount = await token.balanceOf(root); - - // assert.equal(previousAmount.sub(new BN(toStake)).toString(), amountAfterStake.toString()); - assert.equal(amountAfterStake.toString(), amount.toString()); - }); - it("should withdraw unlocked tokens", async () => { // Save current amount let previousAmount = await token.balanceOf(root); @@ -695,40 +563,6 @@ contract("FourYearVesting", (accounts) => { assert.equal(amountAfterStake.toString(), amount.toString()); }); - it("should not withdraw unlocked tokens for the first year <= 52 weeks", async () => { - // Save current amount - let previousAmount = await token.balanceOf(root); - let toStake = ONE_MILLON; - - // Stake - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - 4 * WEEK, - 156 * WEEK, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - - await token.approve(vesting.address, toStake); - await vesting.stakeTokens(toStake, 0); - let amountAfterStake = await token.balanceOf(root); - - // time travel - await increaseTime(18 * WEEK); - - // withdraw - tx = await vesting.withdrawTokens(root); - - // verify amount - let amount = await token.balanceOf(root); - - expect(previousAmount).to.be.bignumber.greaterThan(amount); - assert.equal(amountAfterStake.toString(), amount.toString()); - }); - it("should do nothing if withdrawing a second time", async () => { let amountOld = await token.balanceOf(root); // withdraw @@ -764,7 +598,7 @@ contract("FourYearVesting", (accounts) => { let amountOld = await token.balanceOf(root); // time travel - await increaseTime(25 * WEEK); + await increaseTime(2 * WEEK); // withdraw tx = await vesting.withdrawTokens(a2, { from: a1 }); @@ -811,93 +645,18 @@ contract("FourYearVesting", (accounts) => { }); it("Shouldn't be possible to use governanceWithdraw by user", async () => { - let toStake = ONE_MILLON; - - // Stake - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - 4 * WEEK, - 156 * WEEK, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - - await token.approve(vesting.address, toStake); - let remainingStakeAmount = ONE_MILLON; - let lastStakingSchedule = 0; - while (remainingStakeAmount > 0) { - await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); - lastStakingSchedule = await vesting.lastStakingSchedule(); - remainingStakeAmount = await vesting.remainingStakeAmount(); - } - await expectRevert(staking.governanceWithdraw(100, kickoffTS.toNumber() + 52 * WEEK, root), "unauthorized"); }); it("Shouldn't be possible to use governanceWithdrawTokens by user", async () => { - let toStake = ONE_ETHER; - - // Stake - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - 4 * WEEK, - 156 * WEEK, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - - await token.approve(vesting.address, toStake); - let remainingStakeAmount = toStake; - let lastStakingSchedule = 0; - while (remainingStakeAmount > 0) { - await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); - lastStakingSchedule = await vesting.lastStakingSchedule(); - remainingStakeAmount = await vesting.remainingStakeAmount(); - } - await expectRevert(vesting.governanceWithdrawTokens(root), "operation not supported"); }); - - it("governanceWithdrawTokens", async () => { - let previousAmount = await token.balanceOf(root); - let toStake = ONE_ETHER; - - // Stake - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - 4 * WEEK, - 156 * WEEK, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - - await token.approve(vesting.address, toStake); - let remainingStakeAmount = toStake; - let lastStakingSchedule = 0; - while (remainingStakeAmount > 0) { - await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); - lastStakingSchedule = await vesting.lastStakingSchedule(); - remainingStakeAmount = await vesting.remainingStakeAmount(); - } - - await staking.addAdmin(a1); - // governance withdraw must fail for four year vesting - await expectRevert(staking.governanceWithdrawVesting(vesting.address, root, { from: a1 }), "operation not supported"); - }); }); describe("collectDividends", async () => { + let vesting; it("should fail if the caller is neither owner nor token owner", async () => { - let vesting = await Vesting.new( + vesting = await Vesting.new( vestingLogic.address, token.address, staking.address, @@ -912,17 +671,6 @@ contract("FourYearVesting", (accounts) => { }); it("should collect dividends", async () => { - let vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - a1, - 4 * WEEK, - 156 * WEEK, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - let maxCheckpoints = new BN(10); await expectRevert(vesting.collectDividends(a1, maxCheckpoints, a2), "unauthorized"); let tx = await vesting.collectDividends(a1, maxCheckpoints, a2, { from: a1 }); From 2ecc0ab01c1cc0d683cb9bcb27cdb906dd445b7d Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Mon, 7 Mar 2022 14:45:15 +0530 Subject: [PATCH 19/86] Removed unnecessary calculations --- .../governance/Vesting/fouryear/FourYearVestingLogic.sol | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol index 2c0bd9f43..c214edb6f 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol @@ -187,7 +187,6 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro require((startDate == 0) || (startDate > 0 && remainingStakeAmount > 0 && _restartStakeSchedule > 0), "create new vesting address"); uint256 restartDate; uint256 relativeAmount; - uint256 periods; // Calling the _stakeTokens function first time for the vesting contract // Runs for maxInterval only (consider maxInterval = 18 * 4 = 72 weeks) if (startDate == 0 && _restartStakeSchedule == 0) { @@ -210,9 +209,8 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro if (addedMaxInterval < endDate) { // Runs for max interval lastStakingSchedule = addedMaxInterval; - periods = maxInterval.div(FOUR_WEEKS); // 18 - as it runs for maxInterval - relativeAmount = (_amount.mul(periods).mul(FOUR_WEEKS)).div(durationLeft); // (_amount * 18) / 39 - durationLeft = durationLeft.sub(periods.mul(FOUR_WEEKS)); // durationLeft - 18 periods(72 weeks) + relativeAmount = (_amount.mul(maxInterval)).div(durationLeft); // (_amount * 18) / 39 + durationLeft = durationLeft.sub(maxInterval); // durationLeft - 18 periods(72 weeks) remainingStakeAmount = _amount.sub(relativeAmount); // Amount left to be staked in subsequent intervals } else { // Normal run @@ -234,7 +232,7 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro // All tokens staked cliffAdded = 0; } else { - cliffAdded = cliffAdded.add(periods.mul(FOUR_WEEKS)); // Add cliff to the end of previous maxInterval + cliffAdded = cliffAdded.add(maxInterval); // Add cliff to the end of previous maxInterval } emit TokensStaked(_sender, relativeAmount); From 91a7850e5b5c19bbec232f367eeadebc9f62c5c8 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Mon, 7 Mar 2022 17:09:41 +0530 Subject: [PATCH 20/86] Two step change of owner and implementation --- .../Vesting/fouryear/FourYearVesting.sol | 21 ++-- .../Vesting/fouryear/FourYearVestingLogic.sol | 59 ++++++----- .../fouryear/FourYearVestingStorage.sol | 15 +-- tests/vesting/FourYearVesting.js | 98 ++++++------------- 4 files changed, 85 insertions(+), 108 deletions(-) diff --git a/contracts/governance/Vesting/fouryear/FourYearVesting.sol b/contracts/governance/Vesting/fouryear/FourYearVesting.sol index ec0dd94fd..9baf46253 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVesting.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVesting.sol @@ -44,7 +44,7 @@ contract FourYearVesting is FourYearVestingStorage, UpgradableProxy { require(_duration == 156 weeks, "invalid duration"); require(_feeSharingProxy != address(0), "feeSharingProxy address invalid"); - setImplementation(_logic); + _setImplementation(_logic); SOV = IERC20(_SOV); staking = Staking(_stakingAddress); require(_duration <= staking.MAX_DURATION(), "duration may not exceed the max duration"); @@ -65,16 +65,17 @@ contract FourYearVesting is FourYearVestingStorage, UpgradableProxy { } /** - * @notice Set address of the implementation. - * @dev Overriding setImpl function of implementation. The logic can only be + * @notice Set address of the implementation - vesting owner. + * @dev Overriding setImplementation function of UpgradableProxy. The logic can only be * modified when both token owner and veting owner approve. Since - * setImplementation can only be called by vesting owner, we only need to check - * if the new logic is signed by the token owner. - * @param _implementation Address of the implementation. + * setImplementation can only be called by vesting owner, we also need to check + * if the new logic is already approved by the token owner. + * @param _implementation Address of the implementation. Must match with what is set by token owner. * */ - function setImpl(address _implementation) public { - require(signed[tokenOwner], "must be signed by token owner"); - setImplementation(_implementation); - signed[tokenOwner] = false; + function setImplementation(address _implementation) public onlyProxyOwner { + require(newImplementation != address(0), "invalid address"); + require(newImplementation == _implementation, "address mismatch"); + _setImplementation(_implementation); + newImplementation = address(0); } } diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol index 516c7e054..c69e50728 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol @@ -138,39 +138,47 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro } /** - * @notice Sign transactions - only Token Owner. - * @dev The setImpl and changeTokenOwner functions can only be - * executed when both vesting owner and token owner have approved. This + * @notice Change token owner - only vesting owner. + * @dev Modifies token owner. This must be followed by approval + * from token owner. + * @param _newTokenOwner Address of new token owner. + * */ + function changeTokenOwner(address _newTokenOwner) public onlyOwner { + require(_newTokenOwner != address(0), "invalid new token owner address"); + require(_newTokenOwner != tokenOwner, "same owner not allowed"); + newTokenOwner = _newTokenOwner; + } + + /** + * @notice Approve token owner change - only token Owner. + * @dev Token owner can only be modified + * when both vesting owner and token owner have approved. This * function ascertains the approval of token owner. * */ - function signTransaction() public onlyTokenOwner { - require(!signed[msg.sender], "already signed"); - signed[msg.sender] = true; + function approveOwnershipTransfer() public onlyTokenOwner { + require(newTokenOwner != address(0), "invalid address"); + tokenOwner = newTokenOwner; + newTokenOwner = address(0); } /** - * @notice Change token owner. - * @dev Changes token owner. Must be - * approved by both token owner and multisig(vesting owner). - * @param _newTokenOwner Address of new token owner. + * @notice Set address of the implementation - only Token Owner. + * @dev This function sets the new implementation address. + * It must also be approved by the Vesting owner. + * @param _newImplementation Address of the new implementation. * */ - function changeTokenOwner(address _newTokenOwner) public onlyOwner { - require(_newTokenOwner != address(0), "invalid token owner address"); - require(signed[tokenOwner], "must be signed by token owner"); - tokenOwner = _newTokenOwner; - signed[tokenOwner] = false; + function setImpl(address _newImplementation) public onlyTokenOwner { + require(_newImplementation != address(0), "invalid new implementation address"); + newImplementation = _newImplementation; } /** - * @notice Set address of the implementation. - * @dev This function does nothing as it cannot access the setImplementation - * function of the UpgradableProxy. The actual function logic resides in the - * proxy contract. + * @dev setImplementation is a placeholder + * to access the function of the UpgradableProxy contract. + * Check the FourYearVesting.sol contract. * @param _implementation Address of the implementation. * */ - function setImpl(address _implementation) public { - // Look at FourYearVesting.sol - } + function setImplementation(address _implementation) public {} /** * @notice Allows the owners to migrate the positions @@ -339,4 +347,11 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro selectors[0] = this.stakeTokensWithApproval.selector; return selectors; } + + /** + * @dev getImplementation is a placeholder + * to access the function of the UpgradableProxy contract. + * Check the Upgradable.sol contract. + * */ + function getImplementation() public view returns (address _implementation) {} } diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol b/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol index f517c7fac..9ee748151 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol @@ -44,18 +44,21 @@ contract FourYearVestingStorage is Ownable { /// @notice Maximum interval to stake tokens at one go uint256 public maxInterval; - /// @notice End of previous staking schedule + /// @notice End of previous staking schedule. uint256 public lastStakingSchedule; - /// @notice Amount of shares left to be staked + /// @notice Amount of shares left to be staked. uint256 public remainingStakeAmount; - /// @notice Durations left + /// @notice Durations left. uint256 public durationLeft; - /// @notice Cliffs added + /// @notice Cliffs added. uint256 public cliffAdded; - /// @notice Address(Tokenowner) -> Signed(Tue/False) - mapping(address => bool) public signed; + /// @notice Address of new token owner. + address public newTokenOwner; + + /// @notice Address of new implementation. + address public newImplementation; } diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index 727695f83..59207f257 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -862,7 +862,8 @@ contract("FourYearVesting", (accounts) => { }); describe("changeTokenOwner", async () => { - it("should not change token owner if token owner didn't sign", async () => { + let vesting; + it("should not change token owner if vesting owner didn't approve", async () => { vesting = await Vesting.new( vestingLogic.address, token.address, @@ -873,49 +874,30 @@ contract("FourYearVesting", (accounts) => { feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); - await expectRevert(vesting.signTransaction({ from: a3 }), "unauthorized"); - await expectRevert(vesting.signTransaction(), "unauthorized"); - await expectRevert(vesting.changeTokenOwner(a2), "must be signed by token owner"); + await expectRevert(vesting.changeTokenOwner(a2, {from: a1}), "unauthorized"); }); - it("should not change token owner if txn triggered by vesting owner", async () => { - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - a1, - 4 * WEEK, - 39 * 4 * WEEK, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - await expectRevert(vesting.signTransaction({ from: a3 }), "unauthorized"); - await vesting.signTransaction({ from: a1 }); - await expectRevert(vesting.changeTokenOwner(a2, { from: a1 }), "unauthorized"); + it("should not change token owner if token owner hasn't approved", async () => { + await vesting.changeTokenOwner(a2, { from: root }); + let newTokenOwner = await vesting.tokenOwner(); + expect(newTokenOwner).to.be.not.equal(a2); + }); + + it("approveOwnershipTransfer should revert if not signed by vesting owner", async () => { + await expectRevert(vesting.approveOwnershipTransfer({ from: a2 }), "unauthorized"); }); it("should be able to change token owner", async () => { - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - a1, - 4 * WEEK, - 39 * 4 * WEEK, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - await expectRevert(vesting.signTransaction({ from: a3 }), "unauthorized"); - await vesting.signTransaction({ from: a1 }); - await vesting.changeTokenOwner(a2); + await vesting.approveOwnershipTransfer({from: a1}); let newTokenOwner = await vesting.tokenOwner(); assert.equal(newTokenOwner, a2); }); }); describe("setImplementation", async () => { + let vesting, newVestingLogic; + const NewVestingLogic = artifacts.require("MockFourYearVestingLogic"); it("should not change implementation if token owner didn't sign", async () => { - let toStake = ONE_MILLON; vesting = await Vesting.new( vestingLogic.address, token.address, @@ -926,49 +908,25 @@ contract("FourYearVesting", (accounts) => { feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); - await expectRevert(vesting.signTransaction({ from: a3 }), "unauthorized"); - await expectRevert(vesting.signTransaction(), "unauthorized"); - await expectRevert(vesting.setImpl(a2), "must be signed by token owner"); + newVestingLogic = await NewVestingLogic.new(); + await expectRevert(vesting.setImpl(newVestingLogic.address, { from: a3 }), "unauthorized"); + await expectRevert(vesting.setImpl(newVestingLogic.address, ), "unauthorized"); }); - it("should not be able to change implementation if not triggered by vesting owner", async () => { - let toStake = ONE_MILLON; - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - a1, - 4 * WEEK, - 39 * 4 * WEEK, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - await expectRevert(vesting.signTransaction({ from: a3 }), "unauthorized"); - await vesting.signTransaction({ from: a1 }); - await expectRevert(vesting.signTransaction(), "unauthorized"); - const NewVestingLogic = artifacts.require("MockFourYearVestingLogic"); - let newVestingLogic = await NewVestingLogic.new(); - await expectRevert(vesting.setImpl(newVestingLogic.address, { from: a1 }), "Proxy:: access denied"); + it("should not change implementation if still unauthorized by vesting owner", async () => { + await vesting.setImpl(newVestingLogic.address, { from: a1 }); + let newImplementation = await vesting.getImplementation(); + expect(newImplementation).to.not.equal(newVestingLogic.address); + }); + + it("setImplementation should revert if not signed by vesting owner", async () => { + await expectRevert(vesting.setImplementation(newVestingLogic.address, { from: a1 }), "Proxy:: access denied"); + await expectRevert(vesting.setImplementation(0, { from: root }), "invalid address"); + await expectRevert(vesting.setImplementation(a3, { from: root }), "address mismatch"); }); it("should be able to change implementation", async () => { - let toStake = ONE_MILLON; - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - a1, - 4 * WEEK, - 39 * 4 * WEEK, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - await expectRevert(vesting.signTransaction({ from: a3 }), "unauthorized"); - await vesting.signTransaction({ from: a1 }); - await expectRevert(vesting.signTransaction(), "unauthorized"); - const NewVestingLogic = artifacts.require("MockFourYearVestingLogic"); - let newVestingLogic = await NewVestingLogic.new(); - await vesting.setImpl(newVestingLogic.address); + await vesting.setImplementation(newVestingLogic.address); vesting = await NewVestingLogic.at(vesting.address); let durationLeft = await vesting.getDurationLeft(); From e2d3f9faa462546783e629dcc5fe1049f22a0fa0 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Mon, 7 Mar 2022 17:10:29 +0530 Subject: [PATCH 21/86] Ran Prettier --- tests/vesting/FourYearVesting.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index 59207f257..cd4142001 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -874,7 +874,7 @@ contract("FourYearVesting", (accounts) => { feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); - await expectRevert(vesting.changeTokenOwner(a2, {from: a1}), "unauthorized"); + await expectRevert(vesting.changeTokenOwner(a2, { from: a1 }), "unauthorized"); }); it("should not change token owner if token owner hasn't approved", async () => { @@ -888,7 +888,7 @@ contract("FourYearVesting", (accounts) => { }); it("should be able to change token owner", async () => { - await vesting.approveOwnershipTransfer({from: a1}); + await vesting.approveOwnershipTransfer({ from: a1 }); let newTokenOwner = await vesting.tokenOwner(); assert.equal(newTokenOwner, a2); }); @@ -910,7 +910,7 @@ contract("FourYearVesting", (accounts) => { vesting = await VestingLogic.at(vesting.address); newVestingLogic = await NewVestingLogic.new(); await expectRevert(vesting.setImpl(newVestingLogic.address, { from: a3 }), "unauthorized"); - await expectRevert(vesting.setImpl(newVestingLogic.address, ), "unauthorized"); + await expectRevert(vesting.setImpl(newVestingLogic.address), "unauthorized"); }); it("should not change implementation if still unauthorized by vesting owner", async () => { From fc83188e0abe4d1e83f5a3545ae21d10e35ecee1 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Mon, 7 Mar 2022 17:19:19 +0530 Subject: [PATCH 22/86] Added an event --- .../governance/Vesting/fouryear/FourYearVestingLogic.sol | 2 ++ tests/vesting/FourYearVesting.js | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol index c69e50728..a99b6dfec 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol @@ -20,6 +20,7 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro event TokensWithdrawn(address indexed caller, address receiver); event DividendsCollected(address indexed caller, address loanPoolToken, address receiver, uint32 maxCheckpoints); event MigratedToNewStakingContract(address indexed caller, address newStakingContract); + event TokenOwnerChanged(address indexed newOwner, address indexed oldOwner); /* Modifiers */ /** @@ -159,6 +160,7 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro require(newTokenOwner != address(0), "invalid address"); tokenOwner = newTokenOwner; newTokenOwner = address(0); + emit TokenOwnerChanged(tokenOwner, msg.sender); } /** diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index cd4142001..6a081b472 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -888,7 +888,12 @@ contract("FourYearVesting", (accounts) => { }); it("should be able to change token owner", async () => { - await vesting.approveOwnershipTransfer({ from: a1 }); + let tx = await vesting.approveOwnershipTransfer({ from: a1 }); + // check event + expectEvent(tx, "TokenOwnerChanged", { + newOwner: a2, + oldOwner: a1, + }); let newTokenOwner = await vesting.tokenOwner(); assert.equal(newTokenOwner, a2); }); From d38c4cc73e16182040d49b310c217f960bf9e4c8 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Mon, 7 Mar 2022 17:45:31 +0530 Subject: [PATCH 23/86] Removed unnecessary placeholders --- .../Vesting/fouryear/FourYearVestingLogic.sol | 15 --------------- tests/vesting/FourYearVesting.js | 16 ++++++++-------- 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol index a99b6dfec..6e7e35600 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol @@ -174,14 +174,6 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro newImplementation = _newImplementation; } - /** - * @dev setImplementation is a placeholder - * to access the function of the UpgradableProxy contract. - * Check the FourYearVesting.sol contract. - * @param _implementation Address of the implementation. - * */ - function setImplementation(address _implementation) public {} - /** * @notice Allows the owners to migrate the positions * to a new staking contract. @@ -349,11 +341,4 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro selectors[0] = this.stakeTokensWithApproval.selector; return selectors; } - - /** - * @dev getImplementation is a placeholder - * to access the function of the UpgradableProxy contract. - * Check the Upgradable.sol contract. - * */ - function getImplementation() public view returns (address _implementation) {} } diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index 6a081b472..4f5b2f83f 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -900,10 +900,10 @@ contract("FourYearVesting", (accounts) => { }); describe("setImplementation", async () => { - let vesting, newVestingLogic; + let vesting, newVestingLogic, vestingObject; const NewVestingLogic = artifacts.require("MockFourYearVestingLogic"); it("should not change implementation if token owner didn't sign", async () => { - vesting = await Vesting.new( + vestingObject = await Vesting.new( vestingLogic.address, token.address, staking.address, @@ -912,7 +912,7 @@ contract("FourYearVesting", (accounts) => { 39 * 4 * WEEK, feeSharingProxy.address ); - vesting = await VestingLogic.at(vesting.address); + vesting = await VestingLogic.at(vestingObject.address); newVestingLogic = await NewVestingLogic.new(); await expectRevert(vesting.setImpl(newVestingLogic.address, { from: a3 }), "unauthorized"); await expectRevert(vesting.setImpl(newVestingLogic.address), "unauthorized"); @@ -920,18 +920,18 @@ contract("FourYearVesting", (accounts) => { it("should not change implementation if still unauthorized by vesting owner", async () => { await vesting.setImpl(newVestingLogic.address, { from: a1 }); - let newImplementation = await vesting.getImplementation(); + let newImplementation = await vestingObject.getImplementation(); expect(newImplementation).to.not.equal(newVestingLogic.address); }); it("setImplementation should revert if not signed by vesting owner", async () => { - await expectRevert(vesting.setImplementation(newVestingLogic.address, { from: a1 }), "Proxy:: access denied"); - await expectRevert(vesting.setImplementation(0, { from: root }), "invalid address"); - await expectRevert(vesting.setImplementation(a3, { from: root }), "address mismatch"); + await expectRevert(vestingObject.setImplementation(newVestingLogic.address, { from: a1 }), "Proxy:: access denied"); + await expectRevert(vestingObject.setImplementation(0, { from: root }), "invalid address"); + await expectRevert(vestingObject.setImplementation(a3, { from: root }), "address mismatch"); }); it("should be able to change implementation", async () => { - await vesting.setImplementation(newVestingLogic.address); + await vestingObject.setImplementation(newVestingLogic.address); vesting = await NewVestingLogic.at(vesting.address); let durationLeft = await vesting.getDurationLeft(); From 2ef2ea2eb3126611f1efe1b0e1448fbefff91966 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Mon, 7 Mar 2022 21:29:17 +0530 Subject: [PATCH 24/86] Added test for Vesting Factory --- tests/vesting/FourYearVesting.js | 33 ++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index 898cfc366..ab28ab740 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -9,6 +9,7 @@ const TestWrbtc = artifacts.require("TestWrbtc"); const FeeSharingProxy = artifacts.require("FeeSharingProxyMockup"); const VestingLogic = artifacts.require("FourYearVestingLogic"); const Vesting = artifacts.require("FourYearVesting"); +const VestingFactory = artifacts.require("FourYearVestingFactory"); //Upgradable Vesting Registry const VestingRegistryLogic = artifacts.require("VestingRegistryLogicMockup"); const VestingRegistryProxy = artifacts.require("VestingRegistryProxy"); @@ -24,6 +25,7 @@ contract("FourYearVesting", (accounts) => { let root, a1, a2, a3; let token, staking, stakingLogic, feeSharingProxy; let vestingLogic; + let vestingFactory; let kickoffTS; let cliff = 4 * WEEK; @@ -35,6 +37,7 @@ contract("FourYearVesting", (accounts) => { wrbtc = await TestWrbtc.new(); vestingLogic = await VestingLogic.new(); + vestingFactory = await VestingFactory.new(vestingLogic.address); feeSharingProxy = await FeeSharingProxy.new(constants.ZERO_ADDRESS, constants.ZERO_ADDRESS); @@ -55,6 +58,36 @@ contract("FourYearVesting", (accounts) => { kickoffTS = await staking.kickoffTS.call(); }); + describe("vestingfactory", () => { + it("sets the expected values", async () => { + let vestingInstance = await vestingFactory.deployFourYearVesting( + token.address, + staking.address, + a1, + cliff, + duration, + feeSharingProxy.address, + root + ); + vestingInstance = await VestingLogic.at(vestingInstance.logs[0].address); + + // Check data + let _sov = await vestingInstance.SOV(); + let _stackingAddress = await vestingInstance.staking(); + let _tokenOwner = await vestingInstance.tokenOwner(); + let _cliff = await vestingInstance.cliff(); + let _duration = await vestingInstance.duration(); + let _feeSharingProxy = await vestingInstance.feeSharingProxy(); + + assert.equal(_sov, token.address); + assert.equal(_stackingAddress, staking.address); + assert.equal(_tokenOwner, a1); + assert.equal(_cliff.toString(), cliff); + assert.equal(_duration.toString(), duration); + assert.equal(_feeSharingProxy, feeSharingProxy.address); + }); + }); + describe("constructor", () => { it("sets the expected values", async () => { let vestingInstance = await Vesting.new( From 081d7bd550b9725c481c0ea6d62c9a9282bf7f9c Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Wed, 9 Mar 2022 15:30:28 +0530 Subject: [PATCH 25/86] Added isContract check and made fns. external --- .../Vesting/fouryear/FourYearVesting.sol | 4 ++++ .../Vesting/fouryear/FourYearVestingLogic.sol | 18 +++++++++--------- .../fouryear/FourYearVestingStorage.sol | 1 + 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/contracts/governance/Vesting/fouryear/FourYearVesting.sol b/contracts/governance/Vesting/fouryear/FourYearVesting.sol index 9d8c09677..d26b24d7f 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVesting.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVesting.sol @@ -37,11 +37,15 @@ contract FourYearVesting is FourYearVestingStorage, Proxy { address _feeSharingProxy ) public { require(_SOV != address(0), "SOV address invalid"); + require(Address.isContract(_SOV), "_SOV not a contract"); require(_stakingAddress != address(0), "staking address invalid"); + require(Address.isContract(_stakingAddress), "_stakingAddress not a contract"); require(_tokenOwner != address(0), "token owner address invalid"); require(_cliff == 4 weeks, "invalid cliff"); require(_duration == 156 weeks, "invalid duration"); require(_feeSharingProxy != address(0), "feeSharingProxy address invalid"); + require(Address.isContract(_feeSharingProxy), "_feeSharingProxy not a contract"); + require(Address.isContract(_logic), "_logic not a contract"); _setImplementation(_logic); SOV = IERC20(_SOV); diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol index c214edb6f..ad4b8c11d 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol @@ -44,7 +44,7 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro * @notice Sets the max interval. * @param _interval Max interval for which tokens scheduled shall be staked. * */ - function setMaxInterval(uint256 _interval) public onlyOwner { + function setMaxInterval(uint256 _interval) external onlyOwner { maxInterval = _interval; } @@ -57,7 +57,7 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro * @return lastSchedule The max duration for which tokens were staked. * @return remainingAmount The amount outstanding - to be staked. * */ - function stakeTokens(uint256 _amount, uint256 _restartStakeSchedule) public returns (uint256 lastSchedule, uint256 remainingAmount) { + function stakeTokens(uint256 _amount, uint256 _restartStakeSchedule) external returns (uint256 lastSchedule, uint256 remainingAmount) { (lastSchedule, remainingAmount) = _stakeTokens(msg.sender, _amount, _restartStakeSchedule); } @@ -77,7 +77,7 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro address _sender, uint256 _amount, uint256 _restartStakeSchedule - ) public onlyThisContract returns (uint256 lastSchedule, uint256 remainingAmount) { + ) external onlyThisContract returns (uint256 lastSchedule, uint256 remainingAmount) { (lastSchedule, remainingAmount) = _stakeTokens(_sender, _amount, _restartStakeSchedule); } @@ -86,7 +86,7 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro * to `delegatee`. * @param _delegatee The address to delegate votes to. * */ - function delegate(address _delegatee) public onlyTokenOwner { + function delegate(address _delegatee) external onlyTokenOwner { require(_delegatee != address(0), "delegatee address invalid"); uint256 stakingEndDate = endDate; /// @dev Withdraw for each unlocked position. @@ -104,7 +104,7 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro * @param receiver The receiving address. * @dev Can be called only by owner. * */ - function governanceWithdrawTokens(address receiver) public { + function governanceWithdrawTokens(address receiver) external { require(msg.sender == address(staking), "unauthorized"); _withdrawTokens(receiver, true); } @@ -114,7 +114,7 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro * forwards them to an address specified by the token owner. * @param receiver The receiving address. * */ - function withdrawTokens(address receiver) public onlyTokenOwner { + function withdrawTokens(address receiver) external onlyTokenOwner { _withdrawTokens(receiver, false); } @@ -128,7 +128,7 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro address _loanPoolToken, uint32 _maxCheckpoints, address _receiver - ) public onlyTokenOwner { + ) external onlyTokenOwner { require(_receiver != address(0), "receiver address invalid"); /// @dev Invokes the fee sharing proxy. @@ -141,7 +141,7 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro * @notice Allows the owners to migrate the positions * to a new staking contract. * */ - function migrateToNewStakingContract() public onlyOwners { + function migrateToNewStakingContract() external onlyOwners { staking.migrateToNewStakingContract(); staking = Staking(staking.newStakingContract()); emit MigratedToNewStakingContract(msg.sender, address(staking)); @@ -154,7 +154,7 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro * passed, hence, we extend the duration of staking for all unlocked tokens for the first * year by 3 years. * */ - function extendStaking() public { + function extendStaking() external { uint256 oneYear = startDate.add(52 weeks); uint256[] memory dates; uint96[] memory stakes; diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol b/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol index 7ee4b845f..64f7fddb8 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol @@ -4,6 +4,7 @@ import "../../../openzeppelin/Ownable.sol"; import "../../../interfaces/IERC20.sol"; import "../../Staking/Staking.sol"; import "../../IFeeSharingProxy.sol"; +import "../../../openzeppelin/Address.sol"; /** * @title Four Year Vesting Storage Contract. From 32d9b9360a7fa5e7021cdae6bc4e1bb7f0cbb94a Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Wed, 9 Mar 2022 16:59:10 +0530 Subject: [PATCH 26/86] Added Test Cases --- .../Vesting/fouryear/FourYearVesting.sol | 11 +-- .../Vesting/fouryear/FourYearVestingLogic.sol | 11 --- tests/vesting/FourYearVesting.js | 83 +++++++++++++++++-- 3 files changed, 77 insertions(+), 28 deletions(-) diff --git a/contracts/governance/Vesting/fouryear/FourYearVesting.sol b/contracts/governance/Vesting/fouryear/FourYearVesting.sol index d26b24d7f..bd8125574 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVesting.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVesting.sol @@ -36,6 +36,7 @@ contract FourYearVesting is FourYearVestingStorage, Proxy { uint256 _duration, address _feeSharingProxy ) public { + require(Address.isContract(_logic), "_logic not a contract"); require(_SOV != address(0), "SOV address invalid"); require(Address.isContract(_SOV), "_SOV not a contract"); require(_stakingAddress != address(0), "staking address invalid"); @@ -45,24 +46,14 @@ contract FourYearVesting is FourYearVestingStorage, Proxy { require(_duration == 156 weeks, "invalid duration"); require(_feeSharingProxy != address(0), "feeSharingProxy address invalid"); require(Address.isContract(_feeSharingProxy), "_feeSharingProxy not a contract"); - require(Address.isContract(_logic), "_logic not a contract"); _setImplementation(_logic); SOV = IERC20(_SOV); staking = Staking(_stakingAddress); - require(_duration <= staking.MAX_DURATION(), "duration may not exceed the max duration"); tokenOwner = _tokenOwner; cliff = _cliff; duration = _duration; feeSharingProxy = IFeeSharingProxy(_feeSharingProxy); maxInterval = 18 * FOUR_WEEKS; } - - /** - * @dev We need to add this implementation to prevent proxy call FourYearVestingLogic.governanceWithdrawTokens - * @param receiver The receiver of the token withdrawal. - * */ - function governanceWithdrawTokens(address receiver) public { - revert("operation not supported"); - } } diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol index ad4b8c11d..38e82ebd9 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol @@ -98,17 +98,6 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro emit VotesDelegated(msg.sender, _delegatee); } - /** - * @notice Withdraws all tokens from the staking contract and - * forwards them to an address specified by the token owner. - * @param receiver The receiving address. - * @dev Can be called only by owner. - * */ - function governanceWithdrawTokens(address receiver) external { - require(msg.sender == address(staking), "unauthorized"); - _withdrawTokens(receiver, true); - } - /** * @notice Withdraws unlocked tokens from the staking contract and * forwards them to an address specified by the token owner. diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index ab28ab740..384bf4710 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -59,6 +59,10 @@ contract("FourYearVesting", (accounts) => { }); describe("vestingfactory", () => { + it("fails if the 0 address is passed as logic address", async () => { + await expectRevert(VestingFactory.new(constants.ZERO_ADDRESS), "invalid four year vesting logic address"); + }); + it("sets the expected values", async () => { let vestingInstance = await vestingFactory.deployFourYearVesting( token.address, @@ -120,7 +124,6 @@ contract("FourYearVesting", (accounts) => { it("fails if the 0 address is passed as SOV address", async () => { await expectRevert( Vesting.new(vestingLogic.address, constants.ZERO_ADDRESS, staking.address, root, cliff, duration, feeSharingProxy.address), - "SOV address invalid" ); }); @@ -147,14 +150,14 @@ contract("FourYearVesting", (accounts) => { ); }); - it("fails if the vesting duration is bigger than the max staking duration", async () => { + it("fails if the cliff is not 4 weeks", async () => { await expectRevert( Vesting.new(vestingLogic.address, token.address, staking.address, root, 2 * WEEK, duration, feeSharingProxy.address), "invalid cliff" ); }); - it("fails if the vesting duration is bigger than the max staking duration", async () => { + it("fails if the duration is not 156 weeks", async () => { await expectRevert( Vesting.new(vestingLogic.address, token.address, staking.address, root, cliff, 150 * WEEK, feeSharingProxy.address), "invalid duration" @@ -167,6 +170,66 @@ contract("FourYearVesting", (accounts) => { "feeSharingProxy address invalid" ); }); + + it("fails if logic is not a contract address", async () => { + await expectRevert( + Vesting.new( + a1, + token.address, + staking.address, + a1, + cliff, + duration, + feeSharingProxy.address + ), + "_logic not a contract" + ); + }); + + it("fails if SOV is not a contract address", async () => { + await expectRevert( + Vesting.new( + vestingLogic.address, + a1, + staking.address, + a1, + cliff, + duration, + feeSharingProxy.address + ), + "_SOV not a contract" + ); + }); + + it("fails if staking address is not a contract address", async () => { + await expectRevert( + Vesting.new( + vestingLogic.address, + token.address, + a1, + a1, + cliff, + duration, + feeSharingProxy.address + ), + "_stakingAddress not a contract" + ); + }); + + it("fails if fee sharing is not a contract address", async () => { + await expectRevert( + Vesting.new( + vestingLogic.address, + token.address, + staking.address, + a1, + cliff, + duration, + a1 + ), + "_feeSharingProxy not a contract" + ); + }); }); describe("delegate", () => { @@ -680,10 +743,6 @@ contract("FourYearVesting", (accounts) => { it("Shouldn't be possible to use governanceWithdraw by user", async () => { await expectRevert(staking.governanceWithdraw(100, kickoffTS.toNumber() + 52 * WEEK, root), "unauthorized"); }); - - it("Shouldn't be possible to use governanceWithdrawTokens by user", async () => { - await expectRevert(vesting.governanceWithdrawTokens(root), "operation not supported"); - }); }); describe("collectDividends", async () => { @@ -703,6 +762,11 @@ contract("FourYearVesting", (accounts) => { await expectRevert(vesting.collectDividends(root, 10, a1, { from: a3 }), "unauthorized"); }); + it("should fail if receiver address is invalid", async () => { + let maxCheckpoints = new BN(10); + await expectRevert(vesting.collectDividends(a1, maxCheckpoints, constants.ZERO_ADDRESS, { from: a1 }), "receiver address invalid"); + }); + it("should collect dividends", async () => { let maxCheckpoints = new BN(10); await expectRevert(vesting.collectDividends(a1, maxCheckpoints, a2), "unauthorized"); @@ -859,6 +923,11 @@ contract("FourYearVesting", (accounts) => { expect(dates3).to.be.bignumber.equal(data.dates[0]); }); + it("should not withdraw unlocked tokens if receiver address is 0", async () => { + // withdraw + await expectRevert(vesting.withdrawTokens(constants.ZERO_ADDRESS), "receiver address invalid"); + }); + it("should withdraw unlocked tokens for four year vesting after first year", async () => { // time travel await increaseTime(104 * WEEK); From 2c1d26da43d67a37012bbe3897d95c3b11494f1a Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Wed, 9 Mar 2022 16:59:49 +0530 Subject: [PATCH 27/86] Ran prettier --- tests/vesting/FourYearVesting.js | 45 ++++++-------------------------- 1 file changed, 8 insertions(+), 37 deletions(-) diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index 384bf4710..ecabd519d 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -173,60 +173,28 @@ contract("FourYearVesting", (accounts) => { it("fails if logic is not a contract address", async () => { await expectRevert( - Vesting.new( - a1, - token.address, - staking.address, - a1, - cliff, - duration, - feeSharingProxy.address - ), + Vesting.new(a1, token.address, staking.address, a1, cliff, duration, feeSharingProxy.address), "_logic not a contract" ); }); it("fails if SOV is not a contract address", async () => { await expectRevert( - Vesting.new( - vestingLogic.address, - a1, - staking.address, - a1, - cliff, - duration, - feeSharingProxy.address - ), + Vesting.new(vestingLogic.address, a1, staking.address, a1, cliff, duration, feeSharingProxy.address), "_SOV not a contract" ); }); it("fails if staking address is not a contract address", async () => { await expectRevert( - Vesting.new( - vestingLogic.address, - token.address, - a1, - a1, - cliff, - duration, - feeSharingProxy.address - ), + Vesting.new(vestingLogic.address, token.address, a1, a1, cliff, duration, feeSharingProxy.address), "_stakingAddress not a contract" ); }); it("fails if fee sharing is not a contract address", async () => { await expectRevert( - Vesting.new( - vestingLogic.address, - token.address, - staking.address, - a1, - cliff, - duration, - a1 - ), + Vesting.new(vestingLogic.address, token.address, staking.address, a1, cliff, duration, a1), "_feeSharingProxy not a contract" ); }); @@ -764,7 +732,10 @@ contract("FourYearVesting", (accounts) => { it("should fail if receiver address is invalid", async () => { let maxCheckpoints = new BN(10); - await expectRevert(vesting.collectDividends(a1, maxCheckpoints, constants.ZERO_ADDRESS, { from: a1 }), "receiver address invalid"); + await expectRevert( + vesting.collectDividends(a1, maxCheckpoints, constants.ZERO_ADDRESS, { from: a1 }), + "receiver address invalid" + ); }); it("should collect dividends", async () => { From d0fb6165b1cdf195996a30a81090319a20aa765f Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Wed, 9 Mar 2022 17:13:41 +0530 Subject: [PATCH 28/86] Added test case for validating sufficient approval --- tests/vesting/FourYearVesting.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index ecabd519d..ae4d4bdee 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -681,7 +681,7 @@ contract("FourYearVesting", (accounts) => { await expectRevert(vesting.withdrawTokens(root, { from: a2 }), "unauthorized"); }); - it("Shouldn't be possible to use governanceWithdrawVesting by anyone but owner", async () => { + it("shouldn't be possible to use governanceWithdrawVesting by anyone but owner", async () => { let toStake = ONE_MILLON; // Stake @@ -708,7 +708,7 @@ contract("FourYearVesting", (accounts) => { await expectRevert(staking.governanceWithdrawVesting(vesting.address, root, { from: a1 }), "unauthorized"); }); - it("Shouldn't be possible to use governanceWithdraw by user", async () => { + it("shouldn't be possible to use governanceWithdraw by user", async () => { await expectRevert(staking.governanceWithdraw(100, kickoffTS.toNumber() + 52 * WEEK, root), "unauthorized"); }); }); @@ -834,6 +834,21 @@ contract("FourYearVesting", (accounts) => { describe("fouryearvesting", async () => { let vesting, dates0, dates3, dates5; + it("staking schedule must fail if sufficient tokens aren't approved", async () => { + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + 4 * WEEK, + 39 * 4 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + await token.approve(vesting.address, 1000); + await expectRevert(vesting.stakeTokens(ONE_MILLON, 0), "transfer amount exceeds allowance"); + }); + it("staking schedule must run for max duration", async () => { vesting = await Vesting.new( vestingLogic.address, From 3c91b1031dfc83dc98877b32b41e16dbd609744a Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Thu, 10 Mar 2022 00:10:20 +0530 Subject: [PATCH 29/86] Incorporating feedback --- .../Vesting/fouryear/FourYearVesting.sol | 3 +- .../fouryear/FourYearVestingFactory.sol | 22 ++++++------- .../Vesting/fouryear/FourYearVestingLogic.sol | 2 +- .../fouryear/FourYearVestingStorage.sol | 1 - .../fouryear/IFourYearVestingFactory.sol | 3 +- tests/vesting/FourYearVesting.js | 32 ++++++++++++++----- 6 files changed, 38 insertions(+), 25 deletions(-) diff --git a/contracts/governance/Vesting/fouryear/FourYearVesting.sol b/contracts/governance/Vesting/fouryear/FourYearVesting.sol index 7dce22c8e..47008a2ed 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVesting.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVesting.sol @@ -8,6 +8,7 @@ import "../../IFeeSharingProxy.sol"; import "../../ApprovalReceiver.sol"; import "./FourYearVestingStorage.sol"; import "../../../proxy/UpgradableProxy.sol"; +import "../../../openzeppelin/Address.sol"; /** * @title Four Year Vesting Contract. @@ -67,7 +68,7 @@ contract FourYearVesting is FourYearVestingStorage, UpgradableProxy { * @param _implementation Address of the implementation. Must match with what is set by token owner. * */ function setImplementation(address _implementation) public onlyProxyOwner { - require(newImplementation != address(0), "invalid address"); + require(Address.isContract(_implementation), "_implementation not a contract"); require(newImplementation == _implementation, "address mismatch"); _setImplementation(_implementation); newImplementation = address(0); diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol b/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol index 6f6a0c4be..457f783ab 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol @@ -10,13 +10,6 @@ import "./IFourYearVestingFactory.sol"; * of the same contract and keep track of them easier. * */ contract FourYearVestingFactory is IFourYearVestingFactory, Ownable { - address public fourYearVestingLogic; - - constructor(address _fourYearVestingLogic) public { - require(_fourYearVestingLogic != address(0), "invalid four year vesting logic address"); - fourYearVestingLogic = _fourYearVestingLogic; - } - /** * @notice Deploys four year vesting contract. * @param _SOV the address of SOV token. @@ -25,8 +18,9 @@ contract FourYearVestingFactory is IFourYearVestingFactory, Ownable { * @param _cliff The time interval to the first withdraw in seconds. * @param _duration The total duration in seconds. * @param _feeSharing The address of fee sharing contract. - * @param _vestingOwner The address of an owner of vesting contract. - * @dev _vestingOwner should ALWAYS be multisig. + * @param _vestingOwnerMultisig The address of an owner of vesting contract. + * @dev _vestingOwnerMultisig should ALWAYS be multisig. + * @param _fourYearVestingLogic The implementation contract. * @return The four year vesting contract address. * */ function deployFourYearVesting( @@ -36,11 +30,13 @@ contract FourYearVestingFactory is IFourYearVestingFactory, Ownable { uint256 _cliff, uint256 _duration, address _feeSharing, - address _vestingOwner + address _vestingOwnerMultisig, + address _fourYearVestingLogic ) external onlyOwner returns (address) { - address fourYearVesting = - address(new FourYearVesting(fourYearVestingLogic, _SOV, _staking, _tokenOwner, _cliff, _duration, _feeSharing)); - Ownable(fourYearVesting).transferOwnership(_vestingOwner); + address fourYearVesting = address( + new FourYearVesting(_fourYearVestingLogic, _SOV, _staking, _tokenOwner, _cliff, _duration, _feeSharing) + ); + Ownable(fourYearVesting).transferOwnership(_vestingOwnerMultisig); return fourYearVesting; } } diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol index f43957b8b..c11289457 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol @@ -128,7 +128,7 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro } /** - * @notice Change token owner - only vesting owner. + * @notice Change token owner - only vesting owner is allowed to change. * @dev Modifies token owner. This must be followed by approval * from token owner. * @param _newTokenOwner Address of new token owner. diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol b/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol index ea5a1fca3..9ee748151 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol @@ -4,7 +4,6 @@ import "../../../openzeppelin/Ownable.sol"; import "../../../interfaces/IERC20.sol"; import "../../Staking/Staking.sol"; import "../../IFeeSharingProxy.sol"; -import "../../../openzeppelin/Address.sol"; /** * @title Four Year Vesting Storage Contract. diff --git a/contracts/governance/Vesting/fouryear/IFourYearVestingFactory.sol b/contracts/governance/Vesting/fouryear/IFourYearVestingFactory.sol index 50be88a37..b6f2eb93f 100644 --- a/contracts/governance/Vesting/fouryear/IFourYearVestingFactory.sol +++ b/contracts/governance/Vesting/fouryear/IFourYearVestingFactory.sol @@ -15,6 +15,7 @@ interface IFourYearVestingFactory { uint256 _cliff, uint256 _duration, address _feeSharing, - address _owner + address _vestingOwnerMultisig, + address _fourYearVestingLogic ) external returns (address); } diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index 868f5de26..ebd6426f5 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -37,7 +37,7 @@ contract("FourYearVesting", (accounts) => { wrbtc = await TestWrbtc.new(); vestingLogic = await VestingLogic.new(); - vestingFactory = await VestingFactory.new(vestingLogic.address); + vestingFactory = await VestingFactory.new(); feeSharingProxy = await FeeSharingProxy.new(constants.ZERO_ADDRESS, constants.ZERO_ADDRESS); @@ -59,10 +59,6 @@ contract("FourYearVesting", (accounts) => { }); describe("vestingfactory", () => { - it("fails if the 0 address is passed as logic address", async () => { - await expectRevert(VestingFactory.new(constants.ZERO_ADDRESS), "invalid four year vesting logic address"); - }); - it("sets the expected values", async () => { let vestingInstance = await vestingFactory.deployFourYearVesting( token.address, @@ -71,7 +67,8 @@ contract("FourYearVesting", (accounts) => { cliff, duration, feeSharingProxy.address, - root + root, + vestingLogic.address ); vestingInstance = await VestingLogic.at(vestingInstance.logs[0].address); @@ -965,6 +962,18 @@ contract("FourYearVesting", (accounts) => { await expectRevert(vesting.changeTokenOwner(a2, { from: a1 }), "unauthorized"); }); + it("changeTokenOwner should revert if address is zero", async () => { + await expectRevert(vesting.changeTokenOwner(constants.ZERO_ADDRESS), "invalid new token owner address"); + }); + + it("changeTokenOwner should revert if new owner is the same owner", async () => { + await expectRevert(vesting.changeTokenOwner(a1), "same owner not allowed"); + }); + + it("approveOwnershipTransfer should revert if new token address is zero", async () => { + await expectRevert(vesting.approveOwnershipTransfer({ from: a1 }), "invalid address"); + }); + it("should not change token owner if token owner hasn't approved", async () => { await vesting.changeTokenOwner(a2, { from: root }); let newTokenOwner = await vesting.tokenOwner(); @@ -1004,6 +1013,7 @@ contract("FourYearVesting", (accounts) => { newVestingLogic = await NewVestingLogic.new(); await expectRevert(vesting.setImpl(newVestingLogic.address, { from: a3 }), "unauthorized"); await expectRevert(vesting.setImpl(newVestingLogic.address), "unauthorized"); + await expectRevert(vesting.setImpl(constants.ZERO_ADDRESS, { from: a1 }), "invalid new implementation address"); }); it("should not change implementation if still unauthorized by vesting owner", async () => { @@ -1014,8 +1024,14 @@ contract("FourYearVesting", (accounts) => { it("setImplementation should revert if not signed by vesting owner", async () => { await expectRevert(vestingObject.setImplementation(newVestingLogic.address, { from: a1 }), "Proxy:: access denied"); - await expectRevert(vestingObject.setImplementation(0, { from: root }), "invalid address"); - await expectRevert(vestingObject.setImplementation(a3, { from: root }), "address mismatch"); + }); + + it("setImplementation should revert if logic address is not a contract", async () => { + await expectRevert(vestingObject.setImplementation(a3, { from: root }), "_implementation not a contract"); + }); + + it("setImplementation should revert if address mismatch", async () => { + await expectRevert(vestingObject.setImplementation(vestingLogic.address, { from: root }), "address mismatch"); }); it("should be able to change implementation", async () => { From e71cac46bf96d49021b976f32ec580296163807d Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Thu, 10 Mar 2022 00:11:02 +0530 Subject: [PATCH 30/86] Ran prettier --- .../governance/Vesting/fouryear/FourYearVestingFactory.sol | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol b/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol index 457f783ab..1fd944262 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol @@ -33,9 +33,8 @@ contract FourYearVestingFactory is IFourYearVestingFactory, Ownable { address _vestingOwnerMultisig, address _fourYearVestingLogic ) external onlyOwner returns (address) { - address fourYearVesting = address( - new FourYearVesting(_fourYearVestingLogic, _SOV, _staking, _tokenOwner, _cliff, _duration, _feeSharing) - ); + address fourYearVesting = + address(new FourYearVesting(_fourYearVestingLogic, _SOV, _staking, _tokenOwner, _cliff, _duration, _feeSharing)); Ownable(fourYearVesting).transferOwnership(_vestingOwnerMultisig); return fourYearVesting; } From 84caa87a16687504eb6611649e76e43b352e5ef9 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Thu, 10 Mar 2022 00:53:10 +0530 Subject: [PATCH 31/86] Made cliff and duration constant --- .../Vesting/fouryear/FourYearVesting.sol | 8 -- .../fouryear/FourYearVestingFactory.sol | 7 +- .../fouryear/FourYearVestingStorage.sol | 4 +- .../fouryear/IFourYearVestingFactory.sol | 2 - tests/vesting/FourYearVesting.js | 84 +++---------------- 5 files changed, 16 insertions(+), 89 deletions(-) diff --git a/contracts/governance/Vesting/fouryear/FourYearVesting.sol b/contracts/governance/Vesting/fouryear/FourYearVesting.sol index bd8125574..9add50a59 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVesting.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVesting.sol @@ -23,8 +23,6 @@ contract FourYearVesting is FourYearVestingStorage, Proxy { * @param _logic The address of logic contract. * @param _SOV The SOV token address. * @param _tokenOwner The owner of the tokens. - * @param _cliff The time interval to the first withdraw in seconds. - * @param _duration The total duration in seconds. * @param _feeSharingProxy Fee sharing proxy address. * */ constructor( @@ -32,8 +30,6 @@ contract FourYearVesting is FourYearVestingStorage, Proxy { address _SOV, address _stakingAddress, address _tokenOwner, - uint256 _cliff, - uint256 _duration, address _feeSharingProxy ) public { require(Address.isContract(_logic), "_logic not a contract"); @@ -42,8 +38,6 @@ contract FourYearVesting is FourYearVestingStorage, Proxy { require(_stakingAddress != address(0), "staking address invalid"); require(Address.isContract(_stakingAddress), "_stakingAddress not a contract"); require(_tokenOwner != address(0), "token owner address invalid"); - require(_cliff == 4 weeks, "invalid cliff"); - require(_duration == 156 weeks, "invalid duration"); require(_feeSharingProxy != address(0), "feeSharingProxy address invalid"); require(Address.isContract(_feeSharingProxy), "_feeSharingProxy not a contract"); @@ -51,8 +45,6 @@ contract FourYearVesting is FourYearVestingStorage, Proxy { SOV = IERC20(_SOV); staking = Staking(_stakingAddress); tokenOwner = _tokenOwner; - cliff = _cliff; - duration = _duration; feeSharingProxy = IFeeSharingProxy(_feeSharingProxy); maxInterval = 18 * FOUR_WEEKS; } diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol b/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol index 4a97b3751..16c4cdcbe 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol @@ -22,8 +22,6 @@ contract FourYearVestingFactory is IFourYearVestingFactory, Ownable { * @param _SOV the address of SOV token. * @param _staking The address of staking contract. * @param _tokenOwner The owner of the tokens. - * @param _cliff The time interval to the first withdraw in seconds. - * @param _duration The total duration in seconds. * @param _feeSharing The address of fee sharing contract. * @param _vestingOwner The address of an owner of vesting contract. * @return The four year vesting contract address. @@ -32,13 +30,10 @@ contract FourYearVestingFactory is IFourYearVestingFactory, Ownable { address _SOV, address _staking, address _tokenOwner, - uint256 _cliff, - uint256 _duration, address _feeSharing, address _vestingOwner ) external onlyOwner returns (address) { - address fourYearVesting = - address(new FourYearVesting(fourYearVestingLogic, _SOV, _staking, _tokenOwner, _cliff, _duration, _feeSharing)); + address fourYearVesting = address(new FourYearVesting(fourYearVestingLogic, _SOV, _staking, _tokenOwner, _feeSharing)); Ownable(fourYearVesting).transferOwnership(_vestingOwner); return fourYearVesting; } diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol b/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol index 64f7fddb8..a5362a055 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol @@ -28,10 +28,10 @@ contract FourYearVestingStorage is Ownable { IFeeSharingProxy public feeSharingProxy; /// @notice The cliff. After this time period the tokens begin to unlock. - uint256 public cliff; + uint256 public constant cliff = 4 weeks; /// @notice The duration. After this period all tokens will have been unlocked. - uint256 public duration; + uint256 public constant duration = 156 weeks; /// @notice The start date of the vesting. uint256 public startDate; diff --git a/contracts/governance/Vesting/fouryear/IFourYearVestingFactory.sol b/contracts/governance/Vesting/fouryear/IFourYearVestingFactory.sol index 50be88a37..9795b5529 100644 --- a/contracts/governance/Vesting/fouryear/IFourYearVestingFactory.sol +++ b/contracts/governance/Vesting/fouryear/IFourYearVestingFactory.sol @@ -12,8 +12,6 @@ interface IFourYearVestingFactory { address _SOV, address _staking, address _tokenOwner, - uint256 _cliff, - uint256 _duration, address _feeSharing, address _owner ) external returns (address); diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index ae4d4bdee..2db108014 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -68,8 +68,6 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, a1, - cliff, - duration, feeSharingProxy.address, root ); @@ -98,9 +96,7 @@ contract("FourYearVesting", (accounts) => { vestingLogic.address, token.address, staking.address, - root, - cliff, - duration, + root, feeSharingProxy.address ); vestingInstance = await VestingLogic.at(vestingInstance.address); @@ -123,7 +119,7 @@ contract("FourYearVesting", (accounts) => { it("fails if the 0 address is passed as SOV address", async () => { await expectRevert( - Vesting.new(vestingLogic.address, constants.ZERO_ADDRESS, staking.address, root, cliff, duration, feeSharingProxy.address), + Vesting.new(vestingLogic.address, constants.ZERO_ADDRESS, staking.address, root, feeSharingProxy.address), "SOV address invalid" ); }); @@ -135,8 +131,6 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, constants.ZERO_ADDRESS, - cliff, - duration, feeSharingProxy.address ), "token owner address invalid" @@ -145,56 +139,42 @@ contract("FourYearVesting", (accounts) => { it("fails if the 0 address is passed as staking address", async () => { await expectRevert( - Vesting.new(vestingLogic.address, token.address, constants.ZERO_ADDRESS, root, cliff, duration, feeSharingProxy.address), + Vesting.new(vestingLogic.address, token.address, constants.ZERO_ADDRESS, root, feeSharingProxy.address), "staking address invalid" ); }); - it("fails if the cliff is not 4 weeks", async () => { - await expectRevert( - Vesting.new(vestingLogic.address, token.address, staking.address, root, 2 * WEEK, duration, feeSharingProxy.address), - "invalid cliff" - ); - }); - - it("fails if the duration is not 156 weeks", async () => { - await expectRevert( - Vesting.new(vestingLogic.address, token.address, staking.address, root, cliff, 150 * WEEK, feeSharingProxy.address), - "invalid duration" - ); - }); - it("fails if the 0 address is passed as feeSharingProxy address", async () => { await expectRevert( - Vesting.new(vestingLogic.address, token.address, staking.address, root, cliff, duration, constants.ZERO_ADDRESS), + Vesting.new(vestingLogic.address, token.address, staking.address, root, constants.ZERO_ADDRESS), "feeSharingProxy address invalid" ); }); it("fails if logic is not a contract address", async () => { await expectRevert( - Vesting.new(a1, token.address, staking.address, a1, cliff, duration, feeSharingProxy.address), + Vesting.new(a1, token.address, staking.address, a1, feeSharingProxy.address), "_logic not a contract" ); }); it("fails if SOV is not a contract address", async () => { await expectRevert( - Vesting.new(vestingLogic.address, a1, staking.address, a1, cliff, duration, feeSharingProxy.address), + Vesting.new(vestingLogic.address, a1, staking.address, a1, feeSharingProxy.address), "_SOV not a contract" ); }); it("fails if staking address is not a contract address", async () => { await expectRevert( - Vesting.new(vestingLogic.address, token.address, a1, a1, cliff, duration, feeSharingProxy.address), + Vesting.new(vestingLogic.address, token.address, a1, a1, feeSharingProxy.address), "_stakingAddress not a contract" ); }); it("fails if fee sharing is not a contract address", async () => { await expectRevert( - Vesting.new(vestingLogic.address, token.address, staking.address, a1, cliff, duration, a1), + Vesting.new(vestingLogic.address, token.address, staking.address, a1, a1), "_feeSharingProxy not a contract" ); }); @@ -209,8 +189,6 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, a2, - 4 * WEEK, - 39 * 4 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -267,8 +245,6 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, root, - 4 * WEEK, - 156 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -345,9 +321,7 @@ contract("FourYearVesting", (accounts) => { vestingLogic.address, token.address, staking.address, - root, - cliff, - duration, + root, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -378,9 +352,7 @@ contract("FourYearVesting", (accounts) => { vestingLogic.address, token.address, staking.address, - root, - cliff, - duration, + root, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -395,9 +367,7 @@ contract("FourYearVesting", (accounts) => { vestingLogic.address, token.address, staking.address, - root, - cliff, - duration, + root, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -417,9 +387,7 @@ contract("FourYearVesting", (accounts) => { vestingLogic.address, token.address, staking.address, - root, - cliff, - duration, + root, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -451,9 +419,7 @@ contract("FourYearVesting", (accounts) => { vestingLogic.address, token.address, staking.address, - root, - cliff, - duration, + root, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -512,8 +478,6 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, root, - 4 * WEEK, - 39 * 4 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -560,8 +524,6 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, root, - 4 * WEEK, - 156 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -602,8 +564,6 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, root, - 4 * WEEK, - 156 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -645,8 +605,6 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, a1, - 4 * WEEK, - 156 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -690,8 +648,6 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, root, - 4 * WEEK, - 156 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -721,8 +677,6 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, a1, - 4 * WEEK, - 156 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -765,8 +719,6 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, a1, - 4 * WEEK, - 156 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -798,8 +750,6 @@ contract("FourYearVesting", (accounts) => { token.address, newStaking.address, a1, - 4 * WEEK, - 156 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -816,8 +766,6 @@ contract("FourYearVesting", (accounts) => { token.address, newStaking.address, a1, - 4 * WEEK, - 156 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -840,8 +788,6 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, root, - 4 * WEEK, - 39 * 4 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -855,8 +801,6 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, root, - 4 * WEEK, - 39 * 4 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -937,8 +881,6 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, a2, - 4 * WEEK, - 156 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); From 94715a47031a704087caff12938997c97a9441cc Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Thu, 10 Mar 2022 00:53:48 +0530 Subject: [PATCH 32/86] Ran prettier --- tests/vesting/FourYearVesting.js | 178 ++++--------------------------- 1 file changed, 23 insertions(+), 155 deletions(-) diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index 2db108014..f4ef0d5f8 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -92,13 +92,7 @@ contract("FourYearVesting", (accounts) => { describe("constructor", () => { it("sets the expected values", async () => { - let vestingInstance = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - feeSharingProxy.address - ); + let vestingInstance = await Vesting.new(vestingLogic.address, token.address, staking.address, root, feeSharingProxy.address); vestingInstance = await VestingLogic.at(vestingInstance.address); // Check data @@ -126,13 +120,7 @@ contract("FourYearVesting", (accounts) => { it("fails if the 0 address is passed as token owner address", async () => { await expectRevert( - Vesting.new( - vestingLogic.address, - token.address, - staking.address, - constants.ZERO_ADDRESS, - feeSharingProxy.address - ), + Vesting.new(vestingLogic.address, token.address, staking.address, constants.ZERO_ADDRESS, feeSharingProxy.address), "token owner address invalid" ); }); @@ -152,17 +140,11 @@ contract("FourYearVesting", (accounts) => { }); it("fails if logic is not a contract address", async () => { - await expectRevert( - Vesting.new(a1, token.address, staking.address, a1, feeSharingProxy.address), - "_logic not a contract" - ); + await expectRevert(Vesting.new(a1, token.address, staking.address, a1, feeSharingProxy.address), "_logic not a contract"); }); it("fails if SOV is not a contract address", async () => { - await expectRevert( - Vesting.new(vestingLogic.address, a1, staking.address, a1, feeSharingProxy.address), - "_SOV not a contract" - ); + await expectRevert(Vesting.new(vestingLogic.address, a1, staking.address, a1, feeSharingProxy.address), "_SOV not a contract"); }); it("fails if staking address is not a contract address", async () => { @@ -184,13 +166,7 @@ contract("FourYearVesting", (accounts) => { let vesting; it("should stake tokens and delegate voting power", async () => { let toStake = ONE_MILLON; - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - a2, - feeSharingProxy.address - ); + vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, a2, feeSharingProxy.address); vesting = await VestingLogic.at(vesting.address); await token.approve(vesting.address, toStake); @@ -240,13 +216,7 @@ contract("FourYearVesting", (accounts) => { // Check random scenarios let vesting; it("should stake 1,000,000 SOV with a duration of 156 weeks and a 4 week cliff", async () => { - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - feeSharingProxy.address - ); + vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, root, feeSharingProxy.address); vesting = await VestingLogic.at(vesting.address); await token.approve(vesting.address, ONE_MILLON); let remainingStakeAmount = ONE_MILLON; @@ -317,13 +287,7 @@ contract("FourYearVesting", (accounts) => { let amount = ONE_MILLON; let cliff = 4 * WEEK; let duration = 156 * WEEK; - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - feeSharingProxy.address - ); + vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, root, feeSharingProxy.address); vesting = await VestingLogic.at(vesting.address); await token.approve(vesting.address, amount); @@ -348,13 +312,7 @@ contract("FourYearVesting", (accounts) => { let amount = 1000; let cliff = 4 * WEEK; let duration = 39 * 4 * WEEK; - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - feeSharingProxy.address - ); + vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, root, feeSharingProxy.address); vesting = await VestingLogic.at(vesting.address); await expectRevert(vesting.stakeTokensWithApproval(root, amount, 0), "unauthorized"); }); @@ -363,13 +321,7 @@ contract("FourYearVesting", (accounts) => { let amount = 1000; let cliff = 4 * WEEK; let duration = 39 * 4 * WEEK; - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - feeSharingProxy.address - ); + vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, root, feeSharingProxy.address); vesting = await VestingLogic.at(vesting.address); let contract = new web3.eth.Contract(vesting.abi, vesting.address); @@ -383,13 +335,7 @@ contract("FourYearVesting", (accounts) => { let amount = ONE_MILLON; let cliff = 4 * WEEK; let duration = 39 * 4 * WEEK; - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - feeSharingProxy.address - ); + vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, root, feeSharingProxy.address); vesting = await VestingLogic.at(vesting.address); let contract = new web3.eth.Contract(vesting.abi, vesting.address); @@ -415,13 +361,7 @@ contract("FourYearVesting", (accounts) => { let amount = 39000; let cliff = 4 * WEEK; let duration = 156 * WEEK; - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - feeSharingProxy.address - ); + vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, root, feeSharingProxy.address); vesting = await VestingLogic.at(vesting.address); let contract = new web3.eth.Contract(vesting.abi, vesting.address); @@ -473,13 +413,7 @@ contract("FourYearVesting", (accounts) => { let toStake = ONE_MILLON; // Stake - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - feeSharingProxy.address - ); + vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, root, feeSharingProxy.address); vesting = await VestingLogic.at(vesting.address); await token.approve(vesting.address, toStake); @@ -519,13 +453,7 @@ contract("FourYearVesting", (accounts) => { let toStake = ONE_MILLON; // Stake - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - feeSharingProxy.address - ); + vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, root, feeSharingProxy.address); vesting = await VestingLogic.at(vesting.address); await token.approve(vesting.address, toStake); @@ -559,13 +487,7 @@ contract("FourYearVesting", (accounts) => { let toStake = ONE_ETHER; // Stake - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - feeSharingProxy.address - ); + vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, root, feeSharingProxy.address); vesting = await VestingLogic.at(vesting.address); await token.approve(vesting.address, toStake); @@ -600,13 +522,7 @@ contract("FourYearVesting", (accounts) => { it("should do nothing if withdrawing before reaching the cliff", async () => { let toStake = ONE_MILLON; - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - a1, - feeSharingProxy.address - ); + vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, a1, feeSharingProxy.address); vesting = await VestingLogic.at(vesting.address); await token.approve(vesting.address, toStake); @@ -643,13 +559,7 @@ contract("FourYearVesting", (accounts) => { let toStake = ONE_MILLON; // Stake - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - feeSharingProxy.address - ); + vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, root, feeSharingProxy.address); vesting = await VestingLogic.at(vesting.address); await token.approve(vesting.address, toStake); @@ -672,13 +582,7 @@ contract("FourYearVesting", (accounts) => { describe("collectDividends", async () => { let vesting; it("should fail if the caller is neither owner nor token owner", async () => { - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - a1, - feeSharingProxy.address - ); + vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, a1, feeSharingProxy.address); vesting = await VestingLogic.at(vesting.address); await expectRevert(vesting.collectDividends(root, 10, a1, { from: a2 }), "unauthorized"); await expectRevert(vesting.collectDividends(root, 10, a1, { from: a3 }), "unauthorized"); @@ -714,13 +618,7 @@ contract("FourYearVesting", (accounts) => { describe("migrateToNewStakingContract", async () => { let vesting; it("should set the new staking contract", async () => { - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - a1, - feeSharingProxy.address - ); + vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, a1, feeSharingProxy.address); vesting = await VestingLogic.at(vesting.address); // 1. set new staking contract address on staking contract @@ -745,13 +643,7 @@ contract("FourYearVesting", (accounts) => { await newStaking.setImplementation(stakingLogic.address); newStaking = await StakingLogic.at(newStaking.address); - vesting = await Vesting.new( - vestingLogic.address, - token.address, - newStaking.address, - a1, - feeSharingProxy.address - ); + vesting = await Vesting.new(vestingLogic.address, token.address, newStaking.address, a1, feeSharingProxy.address); vesting = await VestingLogic.at(vesting.address); await expectRevert(vesting.migrateToNewStakingContract(), "there is no new staking contract set"); }); @@ -761,13 +653,7 @@ contract("FourYearVesting", (accounts) => { await newStaking.setImplementation(stakingLogic.address); newStaking = await StakingLogic.at(newStaking.address); - vesting = await Vesting.new( - vestingLogic.address, - token.address, - newStaking.address, - a1, - feeSharingProxy.address - ); + vesting = await Vesting.new(vestingLogic.address, token.address, newStaking.address, a1, feeSharingProxy.address); vesting = await VestingLogic.at(vesting.address); await newStaking.setNewStakingContract(newStaking.address); @@ -783,26 +669,14 @@ contract("FourYearVesting", (accounts) => { describe("fouryearvesting", async () => { let vesting, dates0, dates3, dates5; it("staking schedule must fail if sufficient tokens aren't approved", async () => { - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - feeSharingProxy.address - ); + vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, root, feeSharingProxy.address); vesting = await VestingLogic.at(vesting.address); await token.approve(vesting.address, 1000); await expectRevert(vesting.stakeTokens(ONE_MILLON, 0), "transfer amount exceeds allowance"); }); it("staking schedule must run for max duration", async () => { - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - feeSharingProxy.address - ); + vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, root, feeSharingProxy.address); vesting = await VestingLogic.at(vesting.address); await token.approve(vesting.address, ONE_MILLON); @@ -876,13 +750,7 @@ contract("FourYearVesting", (accounts) => { describe("setMaxInterval", async () => { it("should set/alter maxInterval", async () => { let toStake = ONE_MILLON; - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - a2, - feeSharingProxy.address - ); + vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, a2, feeSharingProxy.address); vesting = await VestingLogic.at(vesting.address); let maxIntervalOld = await vesting.maxInterval(); await vesting.setMaxInterval(60 * WEEK); From 1c553255ac9eca2bca96e680fb9247044420905a Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Thu, 10 Mar 2022 01:05:47 +0530 Subject: [PATCH 33/86] Minor changes --- .../governance/Vesting/fouryear/FourYearVestingFactory.sol | 4 ++-- tests/vesting/FourYearVesting.js | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol b/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol index 350c220c6..c6ae2c7e3 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol @@ -29,8 +29,8 @@ contract FourYearVestingFactory is IFourYearVestingFactory, Ownable { address _vestingOwnerMultisig, address _fourYearVestingLogic ) external onlyOwner returns (address) { - address fourYearVesting = address(new FourYearVesting(fourYearVestingLogic, _SOV, _staking, _tokenOwner, _feeSharing)); - Ownable(fourYearVesting).transferOwnership(_vestingOwner); + address fourYearVesting = address(new FourYearVesting(_fourYearVestingLogic, _SOV, _staking, _tokenOwner, _feeSharing)); + Ownable(fourYearVesting).transferOwnership(_vestingOwnerMultisig); return fourYearVesting; } } diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index 831f3b9b6..20009a21d 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -764,8 +764,6 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, a1, - 4 * WEEK, - 39 * 4 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); @@ -815,8 +813,6 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, a1, - 4 * WEEK, - 39 * 4 * WEEK, feeSharingProxy.address ); vesting = await VestingLogic.at(vestingObject.address); From be6fdcdd7715eb76d9fc90460ff31ec9deff07be Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Thu, 10 Mar 2022 01:06:32 +0530 Subject: [PATCH 34/86] Ran prettier --- tests/vesting/FourYearVesting.js | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index 20009a21d..dae81a273 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -759,13 +759,7 @@ contract("FourYearVesting", (accounts) => { describe("changeTokenOwner", async () => { let vesting; it("should not change token owner if vesting owner didn't approve", async () => { - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - a1, - feeSharingProxy.address - ); + vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, a1, feeSharingProxy.address); vesting = await VestingLogic.at(vesting.address); await expectRevert(vesting.changeTokenOwner(a2, { from: a1 }), "unauthorized"); }); @@ -808,13 +802,7 @@ contract("FourYearVesting", (accounts) => { let vesting, newVestingLogic, vestingObject; const NewVestingLogic = artifacts.require("MockFourYearVestingLogic"); it("should not change implementation if token owner didn't sign", async () => { - vestingObject = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - a1, - feeSharingProxy.address - ); + vestingObject = await Vesting.new(vestingLogic.address, token.address, staking.address, a1, feeSharingProxy.address); vesting = await VestingLogic.at(vestingObject.address); newVestingLogic = await NewVestingLogic.new(); await expectRevert(vesting.setImpl(newVestingLogic.address, { from: a3 }), "unauthorized"); From 07b158d5cf4dae17bbbb0846a871f8c3f1bb6089 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Fri, 11 Mar 2022 16:44:20 +0530 Subject: [PATCH 35/86] Bug fix --- .../Vesting/fouryear/FourYearVestingLogic.sol | 3 ++- tests/vesting/FourYearVesting.js | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol index 38e82ebd9..8cfe8f5b0 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol @@ -185,7 +185,8 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro } // Calling the _stakeTokens second/third time - we start from the end of previous interval // and the remaining amount(amount left after tokens are staked in the previous interval) - if (_restartStakeSchedule > 0 && _restartStakeSchedule == lastStakingSchedule && _amount == remainingStakeAmount) { + if (_restartStakeSchedule > 0) { + require(_restartStakeSchedule == lastStakingSchedule && _amount == remainingStakeAmount, "invalid params"); restartDate = _restartStakeSchedule; } else { restartDate = startDate; diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index f4ef0d5f8..36e5fc05f 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -675,6 +675,20 @@ contract("FourYearVesting", (accounts) => { await expectRevert(vesting.stakeTokens(ONE_MILLON, 0), "transfer amount exceeds allowance"); }); + it("staking schedule must fail for incorrect parameters", async () => { + vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, root, feeSharingProxy.address); + vesting = await VestingLogic.at(vesting.address); + await token.approve(vesting.address, ONE_MILLON); + + let remainingStakeAmount = ONE_MILLON; + let lastStakingSchedule = 0; + await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + await expectRevert(vesting.stakeTokens(remainingStakeAmount + 100, lastStakingSchedule), "invalid params"); + await expectRevert(vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule + 100), "invalid params"); + }); + it("staking schedule must run for max duration", async () => { vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, root, feeSharingProxy.address); vesting = await VestingLogic.at(vesting.address); From 1ae486d26377a2e25e2c81e4d4469f7cc2227dea Mon Sep 17 00:00:00 2001 From: Tyrone Johnson Date: Mon, 14 Mar 2022 10:51:10 +0300 Subject: [PATCH 36/86] .gitignore *.code-workspace of VSCode --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3f6b150ee..385277e55 100644 --- a/.gitignore +++ b/.gitignore @@ -28,7 +28,7 @@ solc-select artifacts cache .idea/ -workspace.code-workspace +*.code-workspace .env node_modules artifacts/ From e94c0d7c6d749e647ec30d79d0e5c5b67ac73f12 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Wed, 16 Mar 2022 19:17:42 +0530 Subject: [PATCH 37/86] Testing upgradable vesting --- .../fouryear/FourYearVestingFactory.sol | 4 ++ .../fouryear/FourYearVestingStorage.sol | 5 ++- .../testnet_contracts.json | 4 +- .../create_four_year_vestings.py | 40 +++++++++++++------ .../deploy_four_year_vesting.py | 2 +- .../fouryearvesting/fouryearvestinglist.csv | 4 +- 6 files changed, 42 insertions(+), 17 deletions(-) diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol b/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol index c6ae2c7e3..86be0eea5 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol @@ -10,6 +10,9 @@ import "./IFourYearVestingFactory.sol"; * of the same contract and keep track of them easier. * */ contract FourYearVestingFactory is IFourYearVestingFactory, Ownable { + /// @dev Added an event to keep track of the vesting contract created for a token owner + event FourYearVestingCreated(address indexed tokenOwner, address indexed vestingAddress); + /** * @notice Deploys four year vesting contract. * @param _SOV the address of SOV token. @@ -31,6 +34,7 @@ contract FourYearVestingFactory is IFourYearVestingFactory, Ownable { ) external onlyOwner returns (address) { address fourYearVesting = address(new FourYearVesting(_fourYearVestingLogic, _SOV, _staking, _tokenOwner, _feeSharing)); Ownable(fourYearVesting).transferOwnership(_vestingOwnerMultisig); + emit FourYearVestingCreated(_tokenOwner, fourYearVesting); return fourYearVesting; } } diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol b/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol index a30566f9a..6c5f16bd3 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol @@ -39,7 +39,7 @@ contract FourYearVestingStorage is Ownable { uint256 public endDate; /// @notice Constant used for computing the vesting dates. - uint256 constant FOUR_WEEKS = 4 weeks; + uint256 public constant FOUR_WEEKS = 4 weeks; /// @notice Maximum interval to stake tokens at one go uint256 public maxInterval; @@ -61,4 +61,7 @@ contract FourYearVestingStorage is Ownable { /// @notice Address of new implementation. address public newImplementation; + + /// @dev Please add new state variables below this line. Mark them internal and + /// add a getter function while upgrading the contracts. } diff --git a/scripts/contractInteraction/testnet_contracts.json b/scripts/contractInteraction/testnet_contracts.json index 5d8af4bbd..0e842e647 100644 --- a/scripts/contractInteraction/testnet_contracts.json +++ b/scripts/contractInteraction/testnet_contracts.json @@ -82,6 +82,8 @@ "VestingRegistry2": "0x068fbb3Bef062C3daBA7a4B12f53Cd614FBcBF1d", "VestingRegistry3": "0x52E4419b9D33C6e0ceb2e7c01D3aA1a04b21668C", "VestingLogic": "0xc1cECAC06c7a5d5480F158043A150acf06e206cD", + "FourYearVestingLogic": "0x5DAF073AEc9aD65fceA5C8D7eA4A580146e44A2B", + "FourYearVestingFactory": "0x0E68ca248F5cc30D298E78D933F0026426CED968", "OriginInvestorsClaim": "0x9FBe4Bf89521088F790a4dD2F3e495B4f0dA7F42", "TokenSender": "0x4D1903BaAd894Fc6Ff70483d8518Db78F163F9ff", "RBTCWrapperProxy": "0x6b1a4735b1E25ccE9406B2d5D7417cE53d1cf90e", @@ -108,4 +110,4 @@ "VestingRegistryProxy": "0x8eE1254c1b95FFaD975Ac6f348Bc1cd25CB0c22F", "VestingRegistryLogic": "0x8Ea3bF5C621FFb93f874047a0c1eE6DffB00053E", "SovrynSwapFormula": "0x7FF1C363b5600834bce7c514B01109eF1c103507" -} +} \ No newline at end of file diff --git a/scripts/fouryearvesting/create_four_year_vestings.py b/scripts/fouryearvesting/create_four_year_vestings.py index ddbaa92db..0849790f5 100644 --- a/scripts/fouryearvesting/create_four_year_vestings.py +++ b/scripts/fouryearvesting/create_four_year_vestings.py @@ -30,9 +30,11 @@ def main(): # load deployed contracts addresses contracts = json.load(configFile) multisig = contracts['multisig'] - SOVAddress = contracts['SOV'] stakingAddress = contracts['Staking'] feeSharingAddress = contracts['FeeSharingProxy'] + fourYearVestingLogic = contracts['FourYearVestingLogic'] + SOVtoken = Contract.from_abi("SOV", address=contracts['SOV'], abi=SOV.abi, owner=acct) + staking = Contract.from_abi("Staking", address=stakingAddress, abi=Staking.abi, owner=acct) fourYearVestingFactory = Contract.from_abi("FourYearVestingFactory", address=contracts['FourYearVestingFactory'], abi=FourYearVestingFactory.abi, owner=acct) MULTIPLIER = 10**16 # Expecting two decimals @@ -41,33 +43,47 @@ def main(): CLIFF = FOUR_WEEKS DURATION = 39 * FOUR_WEEKS + balanceBefore = acct.balance() + + print("SOV Balance Before:") + print(SOVtoken.balanceOf(acct) / 10**18) + # == Vesting contracts creation and staking tokens ============================================================================== # TODO check fouryearvestinglist.csv - with open('./fouryearvestinglist.csv', 'r') as file: + dataFile = 'scripts/fouryearvesting/fouryearvestinglist.csv' + with open(dataFile, 'r') as file: reader = csv.reader(file) for row in reader: tokenOwner = row[0].replace(" ", "") amount = row[1].replace(",", "").replace(".", "") amount = int(amount) * MULTIPLIER + tx = fourYearVestingFactory.deployFourYearVesting(SOVtoken.address, stakingAddress, tokenOwner, feeSharingAddress, multisig, fourYearVestingLogic) + event = tx.events["FourYearVestingCreated"] + vestingAddress = event["vestingAddress"] + print("=======================================") + print("Token Owner: ", tokenOwner) + print("Vesting Contract Address: ", vestingAddress) + print("Staked Amount: ", amount) + fourYearVesting = Contract.from_abi("FourYearVestingLogic", address=vestingAddress, abi=FourYearVestingLogic.abi, owner=acct) - vestingAddress = fourYearVestingFactory.deployFourYearVesting(SOVAddress, stakingAddress, tokenOwner, CLIFF, DURATION, feeSharingAddress, multisig) + SOVtoken.approve(vestingAddress, amount) remainingAmount = amount lastSchedule = 0 while remainingAmount > 0: - (lastSchedule, remainingAmount) = vestingAddress.stakeTokens(remainingAmount, lastSchedule) - + fourYearVesting.stakeTokens(remainingAmount, lastSchedule) + lastSchedule = fourYearVesting.lastStakingSchedule() + remainingAmount = fourYearVesting.remainingStakeAmount() + + stakes = staking.getStakes(vestingAddress) + print("Staking Details") print("=======================================") - print(vestingAddress) - print("'" + tokenOwner + "', ") - print(amount) - print(cliff) - print(duration) + print(stakes) # == Transfer ownership to multisig ============================================================================================= - fourYearVestingFactory.transferOwnership(multisig) + # fourYearVestingFactory.transferOwnership(multisig) - print("balance:") + print("SOV Balance After:") print(SOVtoken.balanceOf(acct) / 10**18) print("deployment cost:") diff --git a/scripts/fouryearvesting/deploy_four_year_vesting.py b/scripts/fouryearvesting/deploy_four_year_vesting.py index ee05962e2..ab42d3c74 100644 --- a/scripts/fouryearvesting/deploy_four_year_vesting.py +++ b/scripts/fouryearvesting/deploy_four_year_vesting.py @@ -35,7 +35,7 @@ def main(): #deploy VestingFactory fourYearVestingLogic = acct.deploy(FourYearVestingLogic) - fourYearVestingFactory = acct.deploy(FourYearVestingFactory, fourYearVestingLogic.address) + fourYearVestingFactory = acct.deploy(FourYearVestingFactory) # Transfer ownership of VestingFactory to multisig after creating vesting contracts and staking tokens # Don't forget to add the contract addresses to json files before running the script diff --git a/scripts/fouryearvesting/fouryearvestinglist.csv b/scripts/fouryearvesting/fouryearvestinglist.csv index 266b887c8..f46ada4ec 100644 --- a/scripts/fouryearvesting/fouryearvestinglist.csv +++ b/scripts/fouryearvesting/fouryearvestinglist.csv @@ -1,2 +1,2 @@ -tokenOwner,amount -0x511893483DCc1A9A98f153ec8298b63BE010A99f,39000 +0x49d99a6821B7569f8D409728eD1D3EF5bF1befa9,78.00 +0xBf151ed43445a093E991211d46A34f60F02a9E09,100.00 From 16ef0b7367e5311dcf1f781dc30217f5d322b84e Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Wed, 16 Mar 2022 19:18:54 +0530 Subject: [PATCH 38/86] Ran prettier --- scripts/contractInteraction/testnet_contracts.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/contractInteraction/testnet_contracts.json b/scripts/contractInteraction/testnet_contracts.json index 0e842e647..1a4de6b39 100644 --- a/scripts/contractInteraction/testnet_contracts.json +++ b/scripts/contractInteraction/testnet_contracts.json @@ -110,4 +110,4 @@ "VestingRegistryProxy": "0x8eE1254c1b95FFaD975Ac6f348Bc1cd25CB0c22F", "VestingRegistryLogic": "0x8Ea3bF5C621FFb93f874047a0c1eE6DffB00053E", "SovrynSwapFormula": "0x7FF1C363b5600834bce7c514B01109eF1c103507" -} \ No newline at end of file +} From 65bdb71206c2bfa51ac2c4c6b02fffb52817f8b3 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Tue, 22 Mar 2022 13:12:44 +0530 Subject: [PATCH 39/86] Modified constants --- .../Vesting/fouryear/FourYearVestingLogic.sol | 10 +++++----- .../Vesting/fouryear/FourYearVestingStorage.sol | 6 +++--- .../governance/Vesting/fouryear/IFourYearVesting.sol | 2 -- tests/vesting/FourYearVesting.js | 8 ++++---- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol index 8cfe8f5b0..05b28db1c 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol @@ -92,7 +92,7 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro /// @dev Withdraw for each unlocked position. /// @dev Don't change FOUR_WEEKS to TWO_WEEKS, a lot of vestings already deployed with FOUR_WEEKS /// workaround found, but it doesn't work with TWO_WEEKS - for (uint256 i = startDate.add(cliff); i <= stakingEndDate; i += FOUR_WEEKS) { + for (uint256 i = startDate.add(CLIFF); i <= stakingEndDate; i += FOUR_WEEKS) { staking.delegate(_delegatee, i); } emit VotesDelegated(msg.sender, _delegatee); @@ -180,8 +180,8 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro // Runs for maxInterval only (consider maxInterval = 18 * 4 = 72 weeks) if (startDate == 0 && _restartStakeSchedule == 0) { startDate = staking.timestampToLockDate(block.timestamp); // Set only once - durationLeft = duration; // We do not touch duration and cliff as they are used throughout - cliffAdded = cliff; // Hence, durationLeft and cliffAdded is created + durationLeft = DURATION; // We do not touch duration and cliff as they are used throughout + cliffAdded = CLIFF; // Hence, durationLeft and cliffAdded is created } // Calling the _stakeTokens second/third time - we start from the end of previous interval // and the remaining amount(amount left after tokens are staked in the previous interval) @@ -193,7 +193,7 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro } // Runs only once when the _stakeTokens is called for the first time if (endDate == 0) { - endDate = staking.timestampToLockDate(block.timestamp.add(duration)); + endDate = staking.timestampToLockDate(block.timestamp.add(DURATION)); } uint256 addedMaxInterval = restartDate.add(maxInterval); // run for maxInterval if (addedMaxInterval < endDate) { @@ -217,7 +217,7 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro /// @dev Allow the staking contract to access them. SOV.approve(address(staking), relativeAmount); - staking.stakesBySchedule(relativeAmount, cliffAdded, duration.sub(durationLeft), FOUR_WEEKS, address(this), tokenOwner); + staking.stakesBySchedule(relativeAmount, cliffAdded, DURATION.sub(durationLeft), FOUR_WEEKS, address(this), tokenOwner); if (durationLeft == 0) { // All tokens staked cliffAdded = 0; diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol b/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol index a5362a055..027b80a50 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol @@ -28,10 +28,10 @@ contract FourYearVestingStorage is Ownable { IFeeSharingProxy public feeSharingProxy; /// @notice The cliff. After this time period the tokens begin to unlock. - uint256 public constant cliff = 4 weeks; + uint256 public constant CLIFF = 4 weeks; /// @notice The duration. After this period all tokens will have been unlocked. - uint256 public constant duration = 156 weeks; + uint256 public constant DURATION = 156 weeks; /// @notice The start date of the vesting. uint256 public startDate; @@ -40,7 +40,7 @@ contract FourYearVestingStorage is Ownable { uint256 public endDate; /// @notice Constant used for computing the vesting dates. - uint256 constant FOUR_WEEKS = 4 weeks; + uint256 public constant FOUR_WEEKS = 4 weeks; /// @notice Maximum interval to stake tokens at one go uint256 public maxInterval; diff --git a/contracts/governance/Vesting/fouryear/IFourYearVesting.sol b/contracts/governance/Vesting/fouryear/IFourYearVesting.sol index 0127cfc46..a95aaf056 100644 --- a/contracts/governance/Vesting/fouryear/IFourYearVesting.sol +++ b/contracts/governance/Vesting/fouryear/IFourYearVesting.sol @@ -8,8 +8,6 @@ pragma solidity ^0.5.17; * at a vesting instance. */ interface IFourYearVesting { - function duration() external returns (uint256); - function endDate() external returns (uint256); function stakeTokens(uint256 _amount, uint256 _restartStakeSchedule) external returns (uint256 lastSchedule, uint256 remainingAmount); diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index 36e5fc05f..4adfcc807 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -77,8 +77,8 @@ contract("FourYearVesting", (accounts) => { let _sov = await vestingInstance.SOV(); let _stackingAddress = await vestingInstance.staking(); let _tokenOwner = await vestingInstance.tokenOwner(); - let _cliff = await vestingInstance.cliff(); - let _duration = await vestingInstance.duration(); + let _cliff = await vestingInstance.CLIFF(); + let _duration = await vestingInstance.DURATION(); let _feeSharingProxy = await vestingInstance.feeSharingProxy(); assert.equal(_sov, token.address); @@ -99,8 +99,8 @@ contract("FourYearVesting", (accounts) => { let _sov = await vestingInstance.SOV(); let _stackingAddress = await vestingInstance.staking(); let _tokenOwner = await vestingInstance.tokenOwner(); - let _cliff = await vestingInstance.cliff(); - let _duration = await vestingInstance.duration(); + let _cliff = await vestingInstance.CLIFF(); + let _duration = await vestingInstance.DURATION(); let _feeSharingProxy = await vestingInstance.feeSharingProxy(); assert.equal(_sov, token.address); From 79b6b67d6db2a1c5739ff4be5ebc8c3ef8c53bec Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Tue, 22 Mar 2022 13:26:27 +0530 Subject: [PATCH 40/86] Added check to set correct max interval --- .../governance/Vesting/fouryear/FourYearVestingLogic.sol | 1 + tests/vesting/FourYearVesting.js | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol index 05b28db1c..a178f3e66 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol @@ -45,6 +45,7 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro * @param _interval Max interval for which tokens scheduled shall be staked. * */ function setMaxInterval(uint256 _interval) external onlyOwner { + require(_interval.mod(FOUR_WEEKS) == 0, "invalid interval"); maxInterval = _interval; } diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index 4adfcc807..d8cfec49f 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -763,7 +763,6 @@ contract("FourYearVesting", (accounts) => { describe("setMaxInterval", async () => { it("should set/alter maxInterval", async () => { - let toStake = ONE_MILLON; vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, a2, feeSharingProxy.address); vesting = await VestingLogic.at(vesting.address); let maxIntervalOld = await vesting.maxInterval(); @@ -771,5 +770,11 @@ contract("FourYearVesting", (accounts) => { let maxIntervalNew = await vesting.maxInterval(); expect(maxIntervalOld).to.be.bignumber.not.equal(maxIntervalNew); }); + + it("should not set/alter maxInterval", async () => { + vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, a2, feeSharingProxy.address); + vesting = await VestingLogic.at(vesting.address); + await expectRevert(vesting.setMaxInterval(7 * WEEK), "invalid interval"); + }); }); }); From 084e93a2942e223748e7a8cfaf75a1fae6538f40 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Tue, 22 Mar 2022 15:10:04 +0530 Subject: [PATCH 41/86] Updated endDate while extending stake --- .../Vesting/fouryear/FourYearVestingLogic.sol | 3 ++ tests/vesting/FourYearVesting.js | 37 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol index a178f3e66..8a13203d9 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol @@ -153,6 +153,9 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro for (uint256 i = 0; i < dates.length; i++) { if ((dates[i] < block.timestamp) && (dates[i] <= oneYear) && (stakes[i] > 0)) { staking.extendStakingDuration(dates[i], dates[i].add(156 weeks)); + endDate = dates[i].add(156 weeks); + } else { + break; } } } diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index d8cfec49f..28c928ea2 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -777,4 +777,41 @@ contract("FourYearVesting", (accounts) => { await expectRevert(vesting.setMaxInterval(7 * WEEK), "invalid interval"); }); }); + + describe("extend duration and delegate", async () => { + it("must delegate for all intervals", async () => { + vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, root, feeSharingProxy.address); + vesting = await VestingLogic.at(vesting.address); + await token.approve(vesting.address, ONE_MILLON); + + let remainingStakeAmount = ONE_MILLON; + let lastStakingSchedule = 0; + while (remainingStakeAmount > 0) { + await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + } + + let data = await staking.getStakes.call(vesting.address); + for (let i = 0; i < data.dates.length; i++) { + let delegatee = await staking.delegates(vesting.address, data.dates[i]); + expect(delegatee).equal(root); + } + + await increaseTime(80 * WEEK); + let tx = await vesting.extendStaking(); + // delegate + tx = await vesting.delegate(a1); + console.log("gasUsed: " + tx.receipt.gasUsed); + expectEvent(tx, "VotesDelegated", { + caller: root, + delegatee: a1, + }); + data = await staking.getStakes.call(vesting.address); + for (let i = 0; i < data.dates.length; i++) { + let delegatee = await staking.delegates(vesting.address, data.dates[i]); + expect(delegatee).equal(a1); + } + }); + }); }); From e103cbc0f3e3871fb82a558e2f64939ac7a6a3b8 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Mon, 28 Mar 2022 20:52:17 +0530 Subject: [PATCH 42/86] Adding four year vestings to registry --- .../Vesting/VestingRegistryLogic.sol | 22 +++++++ .../contractInteraction/staking_vesting.py | 4 +- .../testnet_contracts.json | 4 +- scripts/fouryearvesting/add_to_registry.py | 63 +++++++++++++++++++ .../addfouryearvestingstoregistry.csv | 5 ++ tests/vesting/VestingRegistryMigrations.js | 46 +++++++++++++- 6 files changed, 139 insertions(+), 5 deletions(-) create mode 100644 scripts/fouryearvesting/add_to_registry.py create mode 100644 scripts/fouryearvesting/addfouryearvestingstoregistry.csv diff --git a/contracts/governance/Vesting/VestingRegistryLogic.sol b/contracts/governance/Vesting/VestingRegistryLogic.sol index 141ee0c24..4e6af6d1a 100644 --- a/contracts/governance/Vesting/VestingRegistryLogic.sol +++ b/contracts/governance/Vesting/VestingRegistryLogic.sol @@ -99,6 +99,28 @@ contract VestingRegistryLogic is VestingRegistryStorage { } } + /** + * @notice Adds four year vestings to vesting registry logic + * @param _tokenOwners array of token owners + * @param _vestingAddresses array of vesting addresses + */ + function addFourYearVestings(address[] calldata _tokenOwners, address[] calldata _vestingAddresses) external onlyAuthorized { + uint256 vestingCreationType = 4; + uint256 cliff = 4 weeks; + uint256 duration = 156 weeks; + require(_tokenOwners.length == _vestingAddresses.length, "arrays mismatch"); + for (uint256 i = 0; i < _tokenOwners.length; i++) { + require(_tokenOwners[i] != address(0), "token owner cannot be 0 address"); + require(_vestingAddresses[i] != address(0), "vesting cannot be 0 address"); + uint256 uid = uint256( + keccak256(abi.encodePacked(_tokenOwners[i], uint256(VestingType.Vesting), cliff, duration, vestingCreationType)) + ); + vestings[uid] = Vesting(uint256(VestingType.Vesting), vestingCreationType, _vestingAddresses[i]); + vestingsOf[_tokenOwners[i]].push(uid); + isVesting[_vestingAddresses[i]] = true; + } + } + /** * @notice creates Vesting contract * @param _tokenOwner the owner of the tokens diff --git a/scripts/contractInteraction/staking_vesting.py b/scripts/contractInteraction/staking_vesting.py index eb34cab30..e9aeb6315 100644 --- a/scripts/contractInteraction/staking_vesting.py +++ b/scripts/contractInteraction/staking_vesting.py @@ -184,11 +184,11 @@ def upgradeVesting(): print("New vesting registry logic address:", vestingRegistryLogic.address) # Get the proxy contract instance - vestingRegistryProxy = Contract.from_abi("VestingRegistryProxy", address=conf.contracts['VestingRegistryLogic'], abi=VestingRegistryProxy.abi, owner=conf.acct) + vestingRegistryProxy = Contract.from_abi("VestingRegistryProxy", address=conf.contracts['VestingRegistryProxy'], abi=VestingRegistryProxy.abi, owner=conf.acct) # Register logic in Proxy data = vestingRegistryProxy.setImplementation.encode_input(vestingRegistryLogic.address) - sendWithMultisig(conf.contracts['multisig'], conf.contracts['VestingRegistryLogic'], data, conf.acct) + sendWithMultisig(conf.contracts['multisig'], conf.contracts['VestingRegistryProxy'], data, conf.acct) # Set Vesting Registry Address for Staking diff --git a/scripts/contractInteraction/testnet_contracts.json b/scripts/contractInteraction/testnet_contracts.json index 1a4de6b39..355ebd88d 100644 --- a/scripts/contractInteraction/testnet_contracts.json +++ b/scripts/contractInteraction/testnet_contracts.json @@ -108,6 +108,6 @@ "StakingRewardsProxy": "0x18eF0ff12f1b4D30104B4680D485D026C26D164D", "StakingRewards": "0x9762e0aA49248A58e14a5C09B4edE5c185b0d178", "VestingRegistryProxy": "0x8eE1254c1b95FFaD975Ac6f348Bc1cd25CB0c22F", - "VestingRegistryLogic": "0x8Ea3bF5C621FFb93f874047a0c1eE6DffB00053E", + "VestingRegistryLogic": "0xA9c1D937407fc344828262AE664e1d694fbBE2C7", "SovrynSwapFormula": "0x7FF1C363b5600834bce7c514B01109eF1c103507" -} +} \ No newline at end of file diff --git a/scripts/fouryearvesting/add_to_registry.py b/scripts/fouryearvesting/add_to_registry.py new file mode 100644 index 000000000..db4581077 --- /dev/null +++ b/scripts/fouryearvesting/add_to_registry.py @@ -0,0 +1,63 @@ +from brownie import * +from brownie.network.contract import InterfaceContainer + +import json +import csv + +def main(): + global contracts, acct + thisNetwork = network.show_active() + + # == Load config ======================================================================================================================= + if thisNetwork == "development": + acct = accounts[0] + configFile = open('./scripts/contractInteraction/testnet_contracts.json') + elif thisNetwork == "testnet": + acct = accounts.load("rskdeployer") + configFile = open('./scripts/contractInteraction/testnet_contracts.json') + elif thisNetwork == "rsk-testnet": + acct = accounts.load("rskdeployer") + configFile = open('./scripts/contractInteraction/testnet_contracts.json') + elif thisNetwork == "rsk-mainnet": + acct = accounts.load("rskdeployer") + configFile = open('./scripts/contractInteraction/mainnet_contracts.json') + else: + raise Exception("network not supported") + + # load deployed contracts addresses + contracts = json.load(configFile) + + vestingRegistryLogic = Contract.from_abi( + "VestingRegistryLogic", + address=contracts['VestingRegistryProxy'], + abi=VestingRegistryLogic.abi, + owner=acct) + + # open the file in universal line ending mode + with open('./scripts/fouryearvesting/addfouryearvestingstoregistry.csv', 'rU') as infile: + #read the file as a dictionary for each row ({header : value}) + reader = csv.DictReader(infile) + data = {} + for row in reader: + for header, value in row.items(): + try: + data[header].append(value) + except KeyError: + data[header] = [value] + + # extract the variables you want + tokenOwners = data['tokenOwner'] + vestingAddresses = data['vestingAddress'] + print(tokenOwners) + print(vestingAddresses) + + vestingRegistryLogic.addFourYearVestings(tokenOwners, vestingAddresses) + # data = vestingRegistryLogic.addFourYearVestings.encode_input(tokenOwners, vestingAddresses) + # print(data) + + # multisig = Contract.from_abi("MultiSig", address=contracts['multisig'], abi=MultiSigWallet.abi, owner=acct) + # print(multisig) + # tx = multisig.submitTransaction(vestingRegistryLogic.address, 0, data, {'allow_revert':True}) + # print(tx.revert_msg) + # txId = tx.events["Submission"]["transactionId"] + # print(txId) \ No newline at end of file diff --git a/scripts/fouryearvesting/addfouryearvestingstoregistry.csv b/scripts/fouryearvesting/addfouryearvestingstoregistry.csv new file mode 100644 index 000000000..b7d75c30f --- /dev/null +++ b/scripts/fouryearvesting/addfouryearvestingstoregistry.csv @@ -0,0 +1,5 @@ +tokenOwner,vestingAddress +0x8517ecCe55F6d11e4a60eDDbB7F4090dd2aC19E7,0x810B2142f6674204037C35f4d3C9859402785239 +0xC6138eC6c65e3280e720d4E6da0FD91A061351c4,0xe5CB08A3133a29aF704Aaf6005BFfAd520802AD3 +0x49d99a6821B7569f8D409728eD1D3EF5bF1befa9,0xf9Fe19256Ad33844772200864DFf5e268A7444df +0xBf151ed43445a093E991211d46A34f60F02a9E09,0x0c5e9d6A74245114d3f54c9faa4a9841bc0ec411 \ No newline at end of file diff --git a/tests/vesting/VestingRegistryMigrations.js b/tests/vesting/VestingRegistryMigrations.js index a009d45de..9869f0beb 100644 --- a/tests/vesting/VestingRegistryMigrations.js +++ b/tests/vesting/VestingRegistryMigrations.js @@ -23,6 +23,7 @@ const TEAM_VESTING_DURATION = FOUR_WEEKS.mul(new BN(36)); const TOTAL_SUPPLY = "100000000000000000000000000"; const ZERO_ADDRESS = constants.ZERO_ADDRESS; const pricsSats = "2500"; +const ONE_MILLON = "1000000000000000000000000"; contract("VestingRegistryMigrations", (accounts) => { let root, account1, account2, account3, account4; @@ -39,7 +40,7 @@ contract("VestingRegistryMigrations", (accounts) => { let duration = 11; // This is in 4 weeks. i.e. 11 * 4 weeks. before(async () => { - [root, account1, account2, account3, accounts4, ...accounts] = accounts; + [root, account1, account2, account3, account4, ...accounts] = accounts; }); beforeEach(async () => { @@ -229,5 +230,48 @@ contract("VestingRegistryMigrations", (accounts) => { it("fails if sender isn't an owner", async () => { await expectRevert(vesting.addDeployedVestings([account1], [1], { from: account2 }), "unauthorized"); }); + + it("adds deployed four year vestings ", async () => { + const FourYearVesting = artifacts.require("FourYearVesting"); + const FourYearVestingLogic = artifacts.require("FourYearVestingLogic"); + + // Deploy four year vesting contracts and stake tokens + let fourYearVestingLogic = await FourYearVestingLogic.new(); + let fourYearVesting = await FourYearVesting.new(fourYearVestingLogic.address, SOV.address, staking.address, account4, feeSharingProxy.address); + fourYearVesting = await FourYearVestingLogic.at(fourYearVesting.address); + await SOV.approve(fourYearVesting.address, ONE_MILLON); + + let remainingStakeAmount = ONE_MILLON; + let lastStakingSchedule = 0; + while (remainingStakeAmount > 0) { + await fourYearVesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); + lastStakingSchedule = await fourYearVesting.lastStakingSchedule(); + remainingStakeAmount = await fourYearVesting.remainingStakeAmount(); + } + + // Verify the vesting is created correctly + let data = await staking.getStakes.call(fourYearVesting.address); + assert.equal(data.dates.length, 39); + assert.equal(data.stakes.length, 39); + expect(data.stakes[0]).to.be.bignumber.equal(data.stakes[38]); + + // Add deployed four year vesting to registry + let tx = await vesting.addFourYearVestings([account4], [fourYearVesting.address]); + console.log("gasUsed = " + tx.receipt.gasUsed); + + // Verify that it is added to the registry + let cliff = FOUR_WEEKS; + let duration = FOUR_WEEKS.mul(new BN(39)); + let newVestingAddress = await vesting.getVestingAddr(account4, cliff, duration, 4); + expect(fourYearVesting.address).equal(newVestingAddress); + expect(await vesting.isVestingAdress(newVestingAddress)).equal(true); + + let vestingAddresses = await vesting.getVestingsOf(account4); + assert.equal(vestingAddresses.length.toString(), "1"); + assert.equal(vestingAddresses[0].vestingType, 1); + assert.equal(vestingAddresses[0].vestingCreationType, 4); + assert.equal(vestingAddresses[0].vestingAddress, newVestingAddress); + }); + }); }); From ed0c4e88a2ff83bfb1240ad340362135fd965d42 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Mon, 28 Mar 2022 20:53:21 +0530 Subject: [PATCH 43/86] Ran Prettier --- contracts/governance/Vesting/VestingRegistryLogic.sol | 5 ++--- scripts/contractInteraction/testnet_contracts.json | 2 +- tests/vesting/VestingRegistryMigrations.js | 9 +++++++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/contracts/governance/Vesting/VestingRegistryLogic.sol b/contracts/governance/Vesting/VestingRegistryLogic.sol index 4e6af6d1a..0b98534de 100644 --- a/contracts/governance/Vesting/VestingRegistryLogic.sol +++ b/contracts/governance/Vesting/VestingRegistryLogic.sol @@ -112,9 +112,8 @@ contract VestingRegistryLogic is VestingRegistryStorage { for (uint256 i = 0; i < _tokenOwners.length; i++) { require(_tokenOwners[i] != address(0), "token owner cannot be 0 address"); require(_vestingAddresses[i] != address(0), "vesting cannot be 0 address"); - uint256 uid = uint256( - keccak256(abi.encodePacked(_tokenOwners[i], uint256(VestingType.Vesting), cliff, duration, vestingCreationType)) - ); + uint256 uid = + uint256(keccak256(abi.encodePacked(_tokenOwners[i], uint256(VestingType.Vesting), cliff, duration, vestingCreationType))); vestings[uid] = Vesting(uint256(VestingType.Vesting), vestingCreationType, _vestingAddresses[i]); vestingsOf[_tokenOwners[i]].push(uid); isVesting[_vestingAddresses[i]] = true; diff --git a/scripts/contractInteraction/testnet_contracts.json b/scripts/contractInteraction/testnet_contracts.json index 355ebd88d..94048f0a0 100644 --- a/scripts/contractInteraction/testnet_contracts.json +++ b/scripts/contractInteraction/testnet_contracts.json @@ -110,4 +110,4 @@ "VestingRegistryProxy": "0x8eE1254c1b95FFaD975Ac6f348Bc1cd25CB0c22F", "VestingRegistryLogic": "0xA9c1D937407fc344828262AE664e1d694fbBE2C7", "SovrynSwapFormula": "0x7FF1C363b5600834bce7c514B01109eF1c103507" -} \ No newline at end of file +} diff --git a/tests/vesting/VestingRegistryMigrations.js b/tests/vesting/VestingRegistryMigrations.js index 9869f0beb..0fb6be47b 100644 --- a/tests/vesting/VestingRegistryMigrations.js +++ b/tests/vesting/VestingRegistryMigrations.js @@ -237,7 +237,13 @@ contract("VestingRegistryMigrations", (accounts) => { // Deploy four year vesting contracts and stake tokens let fourYearVestingLogic = await FourYearVestingLogic.new(); - let fourYearVesting = await FourYearVesting.new(fourYearVestingLogic.address, SOV.address, staking.address, account4, feeSharingProxy.address); + let fourYearVesting = await FourYearVesting.new( + fourYearVestingLogic.address, + SOV.address, + staking.address, + account4, + feeSharingProxy.address + ); fourYearVesting = await FourYearVestingLogic.at(fourYearVesting.address); await SOV.approve(fourYearVesting.address, ONE_MILLON); @@ -272,6 +278,5 @@ contract("VestingRegistryMigrations", (accounts) => { assert.equal(vestingAddresses[0].vestingCreationType, 4); assert.equal(vestingAddresses[0].vestingAddress, newVestingAddress); }); - }); }); From 1416ad00c43a3587cb8775e5a832351e1ae3db2f Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Mon, 28 Mar 2022 21:07:17 +0530 Subject: [PATCH 44/86] Added script to add/remove admin - registry --- scripts/contractInteraction/staking_vesting.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/scripts/contractInteraction/staking_vesting.py b/scripts/contractInteraction/staking_vesting.py index e9aeb6315..46029c297 100644 --- a/scripts/contractInteraction/staking_vesting.py +++ b/scripts/contractInteraction/staking_vesting.py @@ -48,6 +48,18 @@ def readAllVestingContractsForAddress(userAddress): addresses = vestingRegistry.getVestingsOf(userAddress) print(addresses) +def addVestingAdmin(admin): + multisig = Contract.from_abi("MultiSig", address=conf.contracts['multisig'], abi=MultiSigWallet.abi, owner=conf.acct) + vestingRegistry = Contract.from_abi("VestingRegistryLogic", address=conf.contracts['VestingRegistryProxy'], abi=VestingRegistryLogic.abi, owner=conf.acct) + data = vestingRegistry.addAdmin.encode_input(admin) + sendWithMultisig(conf.contracts['multisig'], vestingRegistry.address, data, conf.acct) + +def removeVestingAdmin(admin): + multisig = Contract.from_abi("MultiSig", address=conf.contracts['multisig'], abi=MultiSigWallet.abi, owner=conf.acct) + vestingRegistry = Contract.from_abi("VestingRegistryLogic", address=conf.contracts['VestingRegistryProxy'], abi=VestingRegistryLogic.abi, owner=conf.acct) + data = vestingRegistry.removeAdmin.encode_input(admin) + sendWithMultisig(conf.contracts['multisig'], vestingRegistry.address, data, conf.acct) + def readStakingKickOff(): staking = Contract.from_abi("Staking", address=conf.contracts['Staking'], abi=Staking.abi, owner=conf.acct) print(staking.kickoffTS()) From 2fae1b9cc35b7195b6cefffd18c7e2884411336d Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Tue, 29 Mar 2022 15:58:00 +0530 Subject: [PATCH 45/86] Added test cases --- .../Vesting/VestingRegistryLogic.sol | 9 ++-- tests/vesting/VestingRegistryMigrations.js | 44 +++++++++++++------ 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/contracts/governance/Vesting/VestingRegistryLogic.sol b/contracts/governance/Vesting/VestingRegistryLogic.sol index 0b98534de..d320281da 100644 --- a/contracts/governance/Vesting/VestingRegistryLogic.sol +++ b/contracts/governance/Vesting/VestingRegistryLogic.sol @@ -100,20 +100,21 @@ contract VestingRegistryLogic is VestingRegistryStorage { } /** - * @notice Adds four year vestings to vesting registry logic + * @notice adds four year vestings to vesting registry logic * @param _tokenOwners array of token owners * @param _vestingAddresses array of vesting addresses */ function addFourYearVestings(address[] calldata _tokenOwners, address[] calldata _vestingAddresses) external onlyAuthorized { + require(_tokenOwners.length == _vestingAddresses.length, "arrays mismatch"); uint256 vestingCreationType = 4; uint256 cliff = 4 weeks; uint256 duration = 156 weeks; - require(_tokenOwners.length == _vestingAddresses.length, "arrays mismatch"); for (uint256 i = 0; i < _tokenOwners.length; i++) { require(_tokenOwners[i] != address(0), "token owner cannot be 0 address"); require(_vestingAddresses[i] != address(0), "vesting cannot be 0 address"); - uint256 uid = - uint256(keccak256(abi.encodePacked(_tokenOwners[i], uint256(VestingType.Vesting), cliff, duration, vestingCreationType))); + uint256 uid = uint256( + keccak256(abi.encodePacked(_tokenOwners[i], uint256(VestingType.Vesting), cliff, duration, vestingCreationType)) + ); vestings[uid] = Vesting(uint256(VestingType.Vesting), vestingCreationType, _vestingAddresses[i]); vestingsOf[_tokenOwners[i]].push(uid); isVesting[_vestingAddresses[i]] = true; diff --git a/tests/vesting/VestingRegistryMigrations.js b/tests/vesting/VestingRegistryMigrations.js index 0fb6be47b..274a399ff 100644 --- a/tests/vesting/VestingRegistryMigrations.js +++ b/tests/vesting/VestingRegistryMigrations.js @@ -16,6 +16,8 @@ const VestingRegistry = artifacts.require("VestingRegistry"); const VestingRegistry2 = artifacts.require("VestingRegistry2"); const VestingRegistry3 = artifacts.require("VestingRegistry3"); const TestToken = artifacts.require("TestToken"); +const FourYearVesting = artifacts.require("FourYearVesting"); +const FourYearVestingLogic = artifacts.require("FourYearVestingLogic"); const FOUR_WEEKS = new BN(4 * 7 * 24 * 60 * 60); const TEAM_VESTING_CLIFF = FOUR_WEEKS.mul(new BN(6)); @@ -35,6 +37,7 @@ contract("VestingRegistryMigrations", (accounts) => { let vestingTeamAddress, vestingTeamAddress2, vestingTeamAddress3; let newVestingAddress, newVestingAddress2, newVestingAddress3; let newTeamVestingAddress, newTeamVestingAddress2, newTeamVestingAddress3; + let fourYearVestingLogic, fourYearVesting; let cliff = 1; // This is in 4 weeks. i.e. 1 * 4 weeks. let duration = 11; // This is in 4 weeks. i.e. 11 * 4 weeks. @@ -66,6 +69,17 @@ contract("VestingRegistryMigrations", (accounts) => { lockedSOV = await LockedSOV.new(SOV.address, vesting.address, cliff, duration, [root]); await vesting.addAdmin(lockedSOV.address); + + // Deploy four year vesting contracts + fourYearVestingLogic = await FourYearVestingLogic.new(); + fourYearVesting = await FourYearVesting.new( + fourYearVestingLogic.address, + SOV.address, + staking.address, + account4, + feeSharingProxy.address + ); + fourYearVesting = await FourYearVestingLogic.at(fourYearVesting.address); }); describe("addDeployedVestings", () => { @@ -232,19 +246,7 @@ contract("VestingRegistryMigrations", (accounts) => { }); it("adds deployed four year vestings ", async () => { - const FourYearVesting = artifacts.require("FourYearVesting"); - const FourYearVestingLogic = artifacts.require("FourYearVestingLogic"); - - // Deploy four year vesting contracts and stake tokens - let fourYearVestingLogic = await FourYearVestingLogic.new(); - let fourYearVesting = await FourYearVesting.new( - fourYearVestingLogic.address, - SOV.address, - staking.address, - account4, - feeSharingProxy.address - ); - fourYearVesting = await FourYearVestingLogic.at(fourYearVesting.address); + // Stake tokens await SOV.approve(fourYearVesting.address, ONE_MILLON); let remainingStakeAmount = ONE_MILLON; @@ -278,5 +280,21 @@ contract("VestingRegistryMigrations", (accounts) => { assert.equal(vestingAddresses[0].vestingCreationType, 4); assert.equal(vestingAddresses[0].vestingAddress, newVestingAddress); }); + + it("fails adding four year vesting if array mismatch", async () => { + await expectRevert(vesting.addFourYearVestings([account4], []), "arrays mismatch"); + }); + + it("fails adding four year vesting if token owner is zero address", async () => { + await expectRevert(vesting.addFourYearVestings([ZERO_ADDRESS], [fourYearVesting.address]), "token owner cannot be 0 address"); + }); + + it("fails adding four year vesting if vesting is zero address", async () => { + await expectRevert(vesting.addFourYearVestings([account4], [ZERO_ADDRESS]), "vesting cannot be 0 address"); + }); + + it("fails adding four year vesting if sender isn't an owner", async () => { + await expectRevert(vesting.addFourYearVestings([account1], [fourYearVesting.address], { from: account2 }), "unauthorized"); + }); }); }); From 979ef729d1c0cdce3c37069f3f98d3f58697a249 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Tue, 29 Mar 2022 15:58:43 +0530 Subject: [PATCH 46/86] Ran Prettier --- contracts/governance/Vesting/VestingRegistryLogic.sol | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/contracts/governance/Vesting/VestingRegistryLogic.sol b/contracts/governance/Vesting/VestingRegistryLogic.sol index d320281da..722f7f6a1 100644 --- a/contracts/governance/Vesting/VestingRegistryLogic.sol +++ b/contracts/governance/Vesting/VestingRegistryLogic.sol @@ -112,9 +112,8 @@ contract VestingRegistryLogic is VestingRegistryStorage { for (uint256 i = 0; i < _tokenOwners.length; i++) { require(_tokenOwners[i] != address(0), "token owner cannot be 0 address"); require(_vestingAddresses[i] != address(0), "vesting cannot be 0 address"); - uint256 uid = uint256( - keccak256(abi.encodePacked(_tokenOwners[i], uint256(VestingType.Vesting), cliff, duration, vestingCreationType)) - ); + uint256 uid = + uint256(keccak256(abi.encodePacked(_tokenOwners[i], uint256(VestingType.Vesting), cliff, duration, vestingCreationType))); vestings[uid] = Vesting(uint256(VestingType.Vesting), vestingCreationType, _vestingAddresses[i]); vestingsOf[_tokenOwners[i]].push(uid); isVesting[_vestingAddresses[i]] = true; From 41081cd8055883d8df601adf69016c3d4569c54a Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Tue, 29 Mar 2022 20:52:30 +0530 Subject: [PATCH 47/86] Extend Staking - Cron --- .github/workflows/extendstakingcron.yml | 51 ++++++++++++++ scripts/fouryearvesting/extendStakingCron.py | 71 ++++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 .github/workflows/extendstakingcron.yml create mode 100644 scripts/fouryearvesting/extendStakingCron.py diff --git a/.github/workflows/extendstakingcron.yml b/.github/workflows/extendstakingcron.yml new file mode 100644 index 000000000..122a274e0 --- /dev/null +++ b/.github/workflows/extendstakingcron.yml @@ -0,0 +1,51 @@ +name: Extend Staking + +on: + schedule: + # The cron job should run every four weeks from the creation of four year vesting contract + # and only for 52 weeks + - cron: "30 10 * * FRI" + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + - name: Setup node.js + uses: actions/setup-node@v1 + with: + node-version: "14.x" + - name: Cache node modules + uses: actions/cache@v2 + env: + cache-name: cache-node-modules + with: + # npm cache files are stored in `~/.npm` on Linux/macOS + path: ~/.npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + + - name: Cache compiler installations + uses: actions/cache@v2 + with: + path: | + ~/.solcx + ~/.vvm + key: ${{ runner.os }}-compiler-cache + + - name: Install python dependencies + run: pip install -r requirements.txt + + - name: Extend Staking + run: echo $REWARDS_CRON && brownie networks import network-config.yaml true && brownie run scripts/fouryearvesting/extendStakingCron.py --network=rsk-mainnet + env: + REWARDS_CRON: 1 + FEE_CLAIMER: ${{secrets.FEE_CLAIMER}} diff --git a/scripts/fouryearvesting/extendStakingCron.py b/scripts/fouryearvesting/extendStakingCron.py new file mode 100644 index 000000000..2bf8ef451 --- /dev/null +++ b/scripts/fouryearvesting/extendStakingCron.py @@ -0,0 +1,71 @@ +from brownie import * +from brownie.network.contract import InterfaceContainer + +import json +import csv +import time + +def main(): + global contracts, acct + thisNetwork = network.show_active() + + # == Load config ======================================================================================================================= + if thisNetwork == "development": + acct = accounts[0] + configFile = open('./scripts/contractInteraction/testnet_contracts.json') + elif thisNetwork == "testnet": + acct = accounts.load("rskdeployer") + configFile = open('./scripts/contractInteraction/testnet_contracts.json') + elif thisNetwork == "rsk-testnet": + acct = accounts.load("rskdeployer") + configFile = open('./scripts/contractInteraction/testnet_contracts.json') + elif thisNetwork == "rsk-mainnet": + acct = accounts.load("rskdeployer") + configFile = open('./scripts/contractInteraction/mainnet_contracts.json') + else: + raise Exception("network not supported") + + # load deployed contracts addresses + contracts = json.load(configFile) + + # Read last staking timestamp + def readLockDate(timestamp): + staking = Contract.from_abi("Staking", address=contracts['Staking'], abi=Staking.abi, owner=acct) + return staking.timestampToLockDate(timestamp) + + # open the file in universal line ending mode + with open('./scripts/fouryearvesting/addfouryearvestingstoregistry.csv', 'rU') as infile: + #read the file as a dictionary for each row ({header : value}) + reader = csv.DictReader(infile) + data = {} + for row in reader: + for header, value in row.items(): + try: + data[header].append(value) + except KeyError: + data[header] = [value] + + # extract the variables you want + tokenOwners = data['tokenOwner'] + vestingAddresses = data['vestingAddress'] + + for i in vestingAddresses: + print('vestingAddress:', i) + fourYearVestingLogic = Contract.from_abi( + "FourYearVestingLogic", + address=i, + abi=FourYearVestingLogic.abi, + owner=acct) + startDate = fourYearVestingLogic.startDate() + print('startDate:', startDate) + timeLockDate = readLockDate(time.time()) + print('timeLockDate:', timeLockDate) + DAY = 24 * 60 * 60 + FOUR_WEEKS = 4 * 7 * DAY + result = ((timeLockDate - startDate) % FOUR_WEEKS) # the cron should run every four weeks from start date + newResult = ((timeLockDate - startDate) / FOUR_WEEKS) # the cron should run for first 13 intervals = 52 weeks + print('result:', result) + print('newResult:', newResult) + print('-----------------------------------------------------') + if ((result == 0) and (newResult <= 13)): + fourYearVestingLogic.extendStaking() \ No newline at end of file From bf6632fae08d94401f003c996a4491424f08a943 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Tue, 29 Mar 2022 20:53:40 +0530 Subject: [PATCH 48/86] Formatting --- .github/workflows/extendstakingcron.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/extendstakingcron.yml b/.github/workflows/extendstakingcron.yml index 122a274e0..6d40ed8dc 100644 --- a/.github/workflows/extendstakingcron.yml +++ b/.github/workflows/extendstakingcron.yml @@ -2,7 +2,7 @@ name: Extend Staking on: schedule: - # The cron job should run every four weeks from the creation of four year vesting contract + # The cron job should run every four weeks from the creation of four year vesting contract # and only for 52 weeks - cron: "30 10 * * FRI" From 76c4b42d5edf1102b6386dcb0ad8f84687004250 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Wed, 30 Mar 2022 14:06:43 +0530 Subject: [PATCH 49/86] Added scripts --- scripts/contractInteraction/staking_vesting.py | 4 ++++ scripts/contractInteraction/testnet_contracts.json | 10 +++++----- scripts/fouryearvesting/add_to_registry.py | 6 ++++++ .../fouryearvesting/addfouryearvestingstoregistry.csv | 9 +++++---- scripts/fouryearvesting/create_four_year_vestings.py | 5 ++++- scripts/fouryearvesting/fouryearvestinglist.csv | 7 +++++-- 6 files changed, 29 insertions(+), 12 deletions(-) diff --git a/scripts/contractInteraction/staking_vesting.py b/scripts/contractInteraction/staking_vesting.py index 46029c297..60c6ff6c8 100644 --- a/scripts/contractInteraction/staking_vesting.py +++ b/scripts/contractInteraction/staking_vesting.py @@ -60,6 +60,10 @@ def removeVestingAdmin(admin): data = vestingRegistry.removeAdmin.encode_input(admin) sendWithMultisig(conf.contracts['multisig'], vestingRegistry.address, data, conf.acct) +def isVestingAdmin(admin): + vestingRegistry = Contract.from_abi("VestingRegistryLogic", address=conf.contracts['VestingRegistryProxy'], abi=VestingRegistryLogic.abi, owner=conf.acct) + print(vestingRegistry.admins(admin)) + def readStakingKickOff(): staking = Contract.from_abi("Staking", address=conf.contracts['Staking'], abi=Staking.abi, owner=conf.acct) print(staking.kickoffTS()) diff --git a/scripts/contractInteraction/testnet_contracts.json b/scripts/contractInteraction/testnet_contracts.json index 94048f0a0..9b7b36556 100644 --- a/scripts/contractInteraction/testnet_contracts.json +++ b/scripts/contractInteraction/testnet_contracts.json @@ -82,8 +82,8 @@ "VestingRegistry2": "0x068fbb3Bef062C3daBA7a4B12f53Cd614FBcBF1d", "VestingRegistry3": "0x52E4419b9D33C6e0ceb2e7c01D3aA1a04b21668C", "VestingLogic": "0xc1cECAC06c7a5d5480F158043A150acf06e206cD", - "FourYearVestingLogic": "0x5DAF073AEc9aD65fceA5C8D7eA4A580146e44A2B", - "FourYearVestingFactory": "0x0E68ca248F5cc30D298E78D933F0026426CED968", + "FourYearVestingLogic": "0x051D4F2ad54114Ec3b003f308B09640e39D362B1", + "FourYearVestingFactory": "0x914563Aca50218a070431f866803628F19E8CCdD", "OriginInvestorsClaim": "0x9FBe4Bf89521088F790a4dD2F3e495B4f0dA7F42", "TokenSender": "0x4D1903BaAd894Fc6Ff70483d8518Db78F163F9ff", "RBTCWrapperProxy": "0x6b1a4735b1E25ccE9406B2d5D7417cE53d1cf90e", @@ -107,7 +107,7 @@ "WatcherContract": "0x3583155D5e87491dACDc15f7D0032C12D5D0ece0", "StakingRewardsProxy": "0x18eF0ff12f1b4D30104B4680D485D026C26D164D", "StakingRewards": "0x9762e0aA49248A58e14a5C09B4edE5c185b0d178", - "VestingRegistryProxy": "0x8eE1254c1b95FFaD975Ac6f348Bc1cd25CB0c22F", - "VestingRegistryLogic": "0xA9c1D937407fc344828262AE664e1d694fbBE2C7", + "VestingRegistryProxy": "0x09e8659B6d204C6b1bED2BFF8E3F43F834A5Bbc4", + "VestingRegistryLogic": "0xc9F6D4723Ed9AFc52FE07c083690e39e4505ED73", "SovrynSwapFormula": "0x7FF1C363b5600834bce7c514B01109eF1c103507" -} +} \ No newline at end of file diff --git a/scripts/fouryearvesting/add_to_registry.py b/scripts/fouryearvesting/add_to_registry.py index db4581077..011ba5b52 100644 --- a/scripts/fouryearvesting/add_to_registry.py +++ b/scripts/fouryearvesting/add_to_registry.py @@ -26,6 +26,8 @@ def main(): # load deployed contracts addresses contracts = json.load(configFile) + + balanceBefore = acct.balance() vestingRegistryLogic = Contract.from_abi( "VestingRegistryLogic", @@ -52,6 +54,10 @@ def main(): print(vestingAddresses) vestingRegistryLogic.addFourYearVestings(tokenOwners, vestingAddresses) + + print("deployment cost:") + print((balanceBefore - acct.balance()) / 10**18) + # data = vestingRegistryLogic.addFourYearVestings.encode_input(tokenOwners, vestingAddresses) # print(data) diff --git a/scripts/fouryearvesting/addfouryearvestingstoregistry.csv b/scripts/fouryearvesting/addfouryearvestingstoregistry.csv index b7d75c30f..bf038f40b 100644 --- a/scripts/fouryearvesting/addfouryearvestingstoregistry.csv +++ b/scripts/fouryearvesting/addfouryearvestingstoregistry.csv @@ -1,5 +1,6 @@ tokenOwner,vestingAddress -0x8517ecCe55F6d11e4a60eDDbB7F4090dd2aC19E7,0x810B2142f6674204037C35f4d3C9859402785239 -0xC6138eC6c65e3280e720d4E6da0FD91A061351c4,0xe5CB08A3133a29aF704Aaf6005BFfAd520802AD3 -0x49d99a6821B7569f8D409728eD1D3EF5bF1befa9,0xf9Fe19256Ad33844772200864DFf5e268A7444df -0xBf151ed43445a093E991211d46A34f60F02a9E09,0x0c5e9d6A74245114d3f54c9faa4a9841bc0ec411 \ No newline at end of file +0x511893483DCc1A9A98f153ec8298b63BE010A99f,0x7C2e9Ac45F6AD5F474CFE03d2985f39f365D46df +0x8517ecCe55F6d11e4a60eDDbB7F4090dd2aC19E7,0x7ed1A31064476FB4F7d8794fdE5398D568DC75B0 +0xC6138eC6c65e3280e720d4E6da0FD91A061351c4,0xDf7454C512758AF997FE4B7ce3D3039D5dcaF243 +0x49d99a6821B7569f8D409728eD1D3EF5bF1befa9,0x7bFd4e6Be7B5373072eDecEb8375CBF41225D61B +0xBf151ed43445a093E991211d46A34f60F02a9E09,0xE26d671Af44e8118def31f1e8833a66d247741ab \ No newline at end of file diff --git a/scripts/fouryearvesting/create_four_year_vestings.py b/scripts/fouryearvesting/create_four_year_vestings.py index 0849790f5..b31593693 100644 --- a/scripts/fouryearvesting/create_four_year_vestings.py +++ b/scripts/fouryearvesting/create_four_year_vestings.py @@ -72,8 +72,11 @@ def main(): lastSchedule = 0 while remainingAmount > 0: fourYearVesting.stakeTokens(remainingAmount, lastSchedule) + time.sleep(10) lastSchedule = fourYearVesting.lastStakingSchedule() + print('lastSchedule:', lastSchedule) remainingAmount = fourYearVesting.remainingStakeAmount() + print('remainingAmount:', remainingAmount) stakes = staking.getStakes(vestingAddress) print("Staking Details") @@ -81,7 +84,7 @@ def main(): print(stakes) # == Transfer ownership to multisig ============================================================================================= - # fourYearVestingFactory.transferOwnership(multisig) + fourYearVestingFactory.transferOwnership(multisig) print("SOV Balance After:") print(SOVtoken.balanceOf(acct) / 10**18) diff --git a/scripts/fouryearvesting/fouryearvestinglist.csv b/scripts/fouryearvesting/fouryearvestinglist.csv index f46ada4ec..d90aff64a 100644 --- a/scripts/fouryearvesting/fouryearvestinglist.csv +++ b/scripts/fouryearvesting/fouryearvestinglist.csv @@ -1,2 +1,5 @@ -0x49d99a6821B7569f8D409728eD1D3EF5bF1befa9,78.00 -0xBf151ed43445a093E991211d46A34f60F02a9E09,100.00 +0x511893483DCc1A9A98f153ec8298b63BE010A99f,39.00 +0x8517ecCe55F6d11e4a60eDDbB7F4090dd2aC19E7,78.00 +0xC6138eC6c65e3280e720d4E6da0FD91A061351c4,117.00 +0x49d99a6821B7569f8D409728eD1D3EF5bF1befa9,156.00 +0xBf151ed43445a093E991211d46A34f60F02a9E09,195.00 From 5654145c9989c9755b748ac6b89cbec6660a1007 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Wed, 30 Mar 2022 14:07:22 +0530 Subject: [PATCH 50/86] Ran prettier --- scripts/contractInteraction/testnet_contracts.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/contractInteraction/testnet_contracts.json b/scripts/contractInteraction/testnet_contracts.json index 9b7b36556..124de487f 100644 --- a/scripts/contractInteraction/testnet_contracts.json +++ b/scripts/contractInteraction/testnet_contracts.json @@ -110,4 +110,4 @@ "VestingRegistryProxy": "0x09e8659B6d204C6b1bED2BFF8E3F43F834A5Bbc4", "VestingRegistryLogic": "0xc9F6D4723Ed9AFc52FE07c083690e39e4505ED73", "SovrynSwapFormula": "0x7FF1C363b5600834bce7c514B01109eF1c103507" -} \ No newline at end of file +} From 382b473bbda438fb9d72106998cbc2d64c6e574d Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Tue, 26 Apr 2022 16:47:09 +0530 Subject: [PATCH 51/86] Modified Prettier --- .eslintrc.js | 26 +- .mocharc.json | 4 +- .prettierrc | 4 +- .solcover.js | 10 +- .solhint.json | 14 +- .../connectors/loantoken/AdvancedToken.sol | 156 +- .../loantoken/AdvancedTokenStorage.sol | 121 +- contracts/connectors/loantoken/LoanToken.sol | 210 +- .../connectors/loantoken/LoanTokenBase.sol | 56 +- .../loantoken/LoanTokenLogicBeacon.sol | 262 +- .../loantoken/LoanTokenLogicProxy.sol | 157 +- .../loantoken/LoanTokenLogicStandard.sol | 3133 +++++++------ .../loantoken/LoanTokenLogicStorage.sol | 71 +- contracts/connectors/loantoken/Pausable.sol | 45 +- .../loantoken/interfaces/FeedsLike.sol | 5 +- .../loantoken/interfaces/ProtocolLike.sol | 122 +- .../interfaces/ProtocolSettingsLike.sol | 8 +- .../beaconLogicLM/LoanTokenLogicLM.sol | 196 +- .../beaconLogicWRBTC/LoanTokenLogicWrbtc.sol | 322 +- .../shared/LoanTokenSettingsLowerAdmin.sol | 459 +- contracts/core/Objects.sol | 8 +- contracts/core/Protocol.sol | 110 +- contracts/core/State.sol | 284 +- .../core/objects/LenderInterestStruct.sol | 14 +- contracts/core/objects/LoanInterestStruct.sol | 10 +- contracts/core/objects/LoanParamsStruct.sol | 38 +- contracts/core/objects/LoanStruct.sol | 28 +- contracts/core/objects/OrderStruct.sol | 16 +- contracts/escrow/Escrow.sol | 554 +-- contracts/escrow/EscrowReward.sol | 219 +- contracts/events/AffiliatesEvents.sol | 71 +- contracts/events/FeesEvents.sol | 48 +- contracts/events/LoanClosingsEvents.sol | 100 +- contracts/events/LoanMaintenanceEvents.sol | 2 +- contracts/events/LoanOpeningsEvents.sol | 67 +- contracts/events/LoanSettingsEvents.sol | 40 +- contracts/events/ModulesCommonEvents.sol | 10 +- contracts/events/ProtocolSettingsEvents.sol | 221 +- contracts/events/SwapsEvents.sol | 30 +- contracts/farm/ILiquidityMining.sol | 17 +- contracts/farm/LiquidityMining.sol | 1394 +++--- contracts/farm/LiquidityMiningConfigToken.sol | 44 +- contracts/farm/LiquidityMiningStorage.sol | 102 +- contracts/feeds/BProPriceFeed.sol | 76 +- contracts/feeds/IMoCState.sol | 22 +- contracts/feeds/IPriceFeeds.sol | 108 +- contracts/feeds/IRSKOracle.sol | 8 +- contracts/feeds/IV1PoolOracle.sol | 30 +- contracts/feeds/PriceFeedRSKOracle.sol | 94 +- contracts/feeds/PriceFeedV1PoolOracle.sol | 223 +- contracts/feeds/PriceFeeds.sol | 858 ++-- contracts/feeds/PriceFeedsConstants.sol | 60 +- contracts/feeds/USDTPriceFeed.sol | 32 +- contracts/feeds/testnet/PriceFeedsLocal.sol | 136 +- contracts/feeds/testnet/PriceFeedsMoC.sol | 102 +- contracts/governance/ApprovalReceiver.sol | 177 +- contracts/governance/ErrorDecoder.sol | 47 +- .../FeeSharingProxy/FeeSharingLogic.sol | 921 ++-- .../FeeSharingProxy/FeeSharingProxy.sol | 18 +- .../FeeSharingProxyStorage.sol | 174 +- contracts/governance/GovernorAlpha.sol | 1306 +++--- contracts/governance/GovernorVault.sol | 92 +- contracts/governance/IFeeSharingProxy.sol | 14 +- contracts/governance/Staking/Checkpoints.sol | 496 +- contracts/governance/Staking/IStaking.sol | 76 +- contracts/governance/Staking/SafeMath96.sol | 168 +- contracts/governance/Staking/Staking.sol | 1438 +++--- contracts/governance/Staking/StakingProxy.sol | 16 +- .../governance/Staking/StakingStorage.sol | 168 +- .../governance/Staking/WeightedStaking.sol | 1280 +++--- .../StakingRewards/StakingRewards.sol | 436 +- .../StakingRewards/StakingRewardsStorage.sol | 70 +- contracts/governance/Timelock.sol | 472 +- .../governance/Vesting/DevelopmentFund.sol | 785 ++-- .../governance/Vesting/GenericTokenSender.sol | 86 +- contracts/governance/Vesting/ITeamVesting.sol | 2 +- contracts/governance/Vesting/IVesting.sol | 6 +- .../governance/Vesting/IVestingFactory.sol | 36 +- .../governance/Vesting/IVestingRegistry.sol | 4 +- .../Vesting/OriginInvestorsClaim.sol | 440 +- .../Vesting/OrigingVestingCreator.sol | 50 +- contracts/governance/Vesting/SVR.sol | 240 +- contracts/governance/Vesting/TeamVesting.sol | 62 +- contracts/governance/Vesting/TokenSender.sol | 165 +- contracts/governance/Vesting/Vesting.sol | 59 +- .../governance/Vesting/VestingCreator.sol | 517 ++- .../governance/Vesting/VestingFactory.sol | 144 +- contracts/governance/Vesting/VestingLogic.sol | 409 +- .../governance/Vesting/VestingRegistry.sol | 921 ++-- .../governance/Vesting/VestingRegistry2.sol | 809 ++-- .../governance/Vesting/VestingRegistry3.sol | 404 +- .../Vesting/VestingRegistryLogic.sol | 691 +-- .../Vesting/VestingRegistryStorage.sol | 66 +- .../governance/Vesting/VestingStorage.sol | 36 +- .../Vesting/fouryear/FourYearVesting.sol | 86 +- .../fouryear/FourYearVestingFactory.sol | 61 +- .../Vesting/fouryear/FourYearVestingLogic.sol | 668 +-- .../fouryear/FourYearVestingStorage.sol | 68 +- .../Vesting/fouryear/IFourYearVesting.sol | 6 +- .../fouryear/IFourYearVestingFactory.sol | 16 +- contracts/interfaces/IChai.sol | 22 +- contracts/interfaces/IConverterAMM.sol | 2 +- contracts/interfaces/IERC20.sol | 30 +- contracts/interfaces/ILoanPool.sol | 6 +- contracts/interfaces/ILoanTokenLogicProxy.sol | 4 +- contracts/interfaces/ILoanTokenModules.sol | 492 +- contracts/interfaces/ISovryn.sol | 778 ++-- contracts/interfaces/IWrbtc.sol | 4 +- contracts/locked/ILockedSOV.sol | 44 +- contracts/locked/LockedSOV.sol | 806 ++-- contracts/mixins/EnumerableAddressSet.sol | 210 +- contracts/mixins/EnumerableBytes32Set.sol | 362 +- contracts/mixins/EnumerableBytes4Set.sol | 362 +- contracts/mixins/FeesHelper.sol | 462 +- contracts/mixins/InterestUser.sol | 99 +- contracts/mixins/LiquidationHelper.sol | 106 +- .../mixins/ModuleCommonFunctionalities.sol | 8 +- contracts/mixins/ProtocolTokenUser.sol | 59 +- contracts/mixins/RewardHelper.sol | 51 +- contracts/mixins/VaultController.sol | 208 +- contracts/mockup/BProPriceFeedMockup.sol | 22 +- contracts/mockup/BlockMockUp.sol | 30 +- contracts/mockup/DummyContract.sol | 10 +- contracts/mockup/FeeSharingProxyMockup.sol | 60 +- contracts/mockup/GovernorAlphaMockup.sol | 30 +- contracts/mockup/LiquidityMiningMockup.sol | 20 +- .../mockup/LiquidityPoolV1ConverterMockup.sol | 58 +- contracts/mockup/LoanTokenLogicLMMockup.sol | 18 +- contracts/mockup/LoanTokenLogicLMV2Mockup.sol | 242 +- contracts/mockup/LockedSOVMockup.sol | 341 +- contracts/mockup/MockAffiliates.sol | 12 +- contracts/mockup/MockFourYearVestingLogic.sol | 12 +- contracts/mockup/MockLoanTokenLogic.sol | 164 +- contracts/mockup/PriceFeedRSKOracleMockup.sol | 22 +- contracts/mockup/PriceFeedsMoCMockup.sol | 22 +- contracts/mockup/ProtocolSettingsMockup.sol | 108 +- contracts/mockup/RBTCWrapperProxyMockup.sol | 26 +- contracts/mockup/StakingMock.sol | 32 +- contracts/mockup/StakingMockup.sol | 166 +- contracts/mockup/StakingRewardsMockUp.sol | 34 +- contracts/mockup/TimelockHarness.sol | 48 +- contracts/mockup/VestingLogicMockup.sol | 26 +- .../mockup/VestingRegistryLogicMockUp.sol | 6 +- contracts/mockup/lockedSOVFailedMockup.sol | 180 +- .../previousLoanToken/PreviousLoanToken.sol | 124 +- .../PreviousLoanTokenSettingsLowerAdmin.sol | 276 +- .../mockup/proxy/ImplementationMockup.sol | 14 +- contracts/mockup/proxy/StorageMockup.sol | 4 +- contracts/mockup/setGet.sol | 32 +- contracts/modules/Affiliates.sol | 930 ++-- contracts/modules/LoanClosingsLiquidation.sol | 432 +- contracts/modules/LoanClosingsRollover.sol | 570 +-- contracts/modules/LoanClosingsShared.sol | 1340 +++--- contracts/modules/LoanClosingsWith.sol | 339 +- contracts/modules/LoanMaintenance.sol | 1699 +++---- contracts/modules/LoanOpenings.sol | 1533 ++++--- contracts/modules/LoanSettings.sol | 408 +- contracts/modules/ProtocolSettings.sol | 1583 ++++--- contracts/modules/SwapsExternal.sol | 264 +- .../ProtocolAffiliatesInterface.sol | 18 +- .../ProtocolSwapExternalInterface.sol | 20 +- contracts/multisig/MultiSigKeyHolders.sol | 558 +-- contracts/multisig/MultiSigWallet.sol | 817 ++-- contracts/openzeppelin/Address.sol | 126 +- contracts/openzeppelin/Context.sol | 22 +- contracts/openzeppelin/ECDSA.sol | 132 +- contracts/openzeppelin/ERC20.sol | 441 +- contracts/openzeppelin/ERC20Detailed.sol | 90 +- contracts/openzeppelin/IERC20_.sol | 130 +- contracts/openzeppelin/Initializable.sol | 46 +- contracts/openzeppelin/Ownable.sol | 88 +- contracts/openzeppelin/PausableOz.sol | 104 +- contracts/openzeppelin/ReentrancyGuard.sol | 46 +- contracts/openzeppelin/SafeERC20.sol | 154 +- contracts/openzeppelin/SafeMath.sol | 362 +- contracts/openzeppelin/SignedSafeMath.sol | 142 +- contracts/proxy/Proxy.sol | 171 +- contracts/proxy/UpgradableProxy.sol | 34 +- contracts/rsk/RSKAddrValidator.sol | 32 +- contracts/swaps/ISwapsImpl.sol | 42 +- contracts/swaps/SwapsUser.sol | 479 +- .../swaps/connectors/SwapsImplSovrynSwap.sol | 442 +- .../interfaces/IContractRegistry.sol | 2 +- .../interfaces/ISovrynSwapNetwork.sol | 23 +- .../connectors/testnet/SwapsImplLocal.sol | 162 +- contracts/testhelpers/FlashLoanerTest.sol | 119 +- contracts/testhelpers/ITokenFlashLoanTest.sol | 14 +- contracts/testhelpers/LoanTokenLogicTest.sol | 10 +- contracts/testhelpers/TestCoverage.sol | 224 +- contracts/testhelpers/TestLibraries.sol | 30 +- contracts/testhelpers/TestSovrynSwap.sol | 104 +- contracts/testhelpers/TestToken.sol | 206 +- contracts/testhelpers/TestWrbtc.sol | 164 +- contracts/testhelpers/WRBTC.sol | 122 +- contracts/token/IApproveAndCall.sol | 26 +- contracts/token/SOV.sol | 78 +- contracts/utils/AdminRole.sol | 56 +- interfaces/ISovrynBrownie.sol | 784 ++-- interfaces/IUniswapV2Router01.sol | 306 +- interfaces/IUniswapV2Router02.sol | 80 +- .../ABIs/AMMPriceOracle.json | 210 +- scripts/contractInteraction/ABIs/FastBTC.json | 254 +- .../contractInteraction/ABIs/FastBTCBiDi.json | 2212 ++++----- .../ABIs/LiquidityPoolV1Converter.json | 2586 +++++------ .../ABIs/LiquidityPoolV2Converter.json | 1592 +++---- scripts/contractInteraction/ABIs/Owned.json | 102 +- .../ABIs/RBTCWrapperProxy.json | 1080 ++--- .../contractInteraction/ABIs/SovrynNft.json | 1022 ++--- .../ABIs/SovrynSwapFormula.json | 528 +-- .../ABIs/SovrynSwapNetwork.json | 780 ++-- scripts/contractInteraction/ABIs/Watcher.json | 693 +-- .../contractInteraction/ABIs/aggregator.json | 2190 ++++----- scripts/contractInteraction/ABIs/crowd.json | 934 ++-- .../bridge-multisig/Bridge.json | 1512 +++--- .../bridge-multisig/Masset.json | 2190 ++++----- .../bsc_testnet_contracts.json | 2 +- .../mainnet_contracts.json | 250 +- .../testnet_contracts.json | 222 +- scripts/getSelectors.js | 226 +- scripts/governance/values.json | 16 +- scripts/keccak256.js | 28 +- scripts/staking/vestings_examples.json | 216 +- scripts/swapTest/swap_test.json | 22 +- scripts/swapTest/swap_test_bpro.json | 14 +- scripts/swapTest/swap_test_usdt.json | 18 +- scripts/swap_test.json | 16 +- scripts/uniswap/eth_mainnet_contracts.json | 20 +- scripts/uniswap/eth_testnet_contracts.json | 20 +- tests/EscrowReward/anyone.test.js | 527 ++- tests/EscrowReward/creator.test.js | 333 +- tests/EscrowReward/event.test.js | 409 +- tests/EscrowReward/multisig.test.js | 416 +- tests/EscrowReward/state.test.js | 1227 ++--- tests/FeeSharingProxyTest.js | 3676 ++++++++------- tests/Governance/GovernanceIntegrationTest.js | 408 +- .../Governance/GovernorAlpha/CastVoteTest.js | 478 +- tests/Governance/GovernorAlpha/ProposeTest.js | 405 +- tests/Governance/GovernorAlpha/QueueTest.js | 183 +- tests/Governance/GovernorAlpha/StateTest.js | 576 +-- tests/Governance/GovernorAlpha/anyone.test.js | 548 ++- .../Governance/GovernorAlpha/guardian.test.js | 475 +- .../GovernorAlpha/proposers.test.js | 202 +- tests/Governance/GovernorAlpha/voter.test.js | 280 +- tests/Governance/GovernorVault.js | 273 +- tests/Locked/admin.test.js | 259 +- tests/Locked/anyone.test.js | 354 +- tests/Locked/creator.test.js | 183 +- tests/Locked/event.test.js | 547 +-- tests/Locked/state.test.js | 1021 +++-- tests/OriginInvestorsClaim.test.js | 709 +-- tests/PauseModules.test.js | 454 +- tests/TimelockTest.js | 901 ++-- tests/Utils/EIP712.js | 165 +- tests/Utils/EIP712Ethers.js | 214 +- tests/Utils/Ethereum.js | 200 +- tests/Utils/hardhat_utils.js | 22 +- tests/Utils/initializer.js | 738 +-- tests/affiliates/affiliates.test.js | 1939 ++++---- .../affiliates/affiliates_integration.test.js | 1077 +++-- tests/farm/LiquidityMining.js | 4073 +++++++++-------- tests/libraries/testLibraries.test.js | 106 +- tests/loan-settings/LoanSettings.js | 236 +- tests/loan-settings/LoanSettingsEvents.js | 228 +- tests/loan-settings/LoanSettingsNegative.js | 236 +- tests/loan-token/Administration.test.js | 527 ++- tests/loan-token/BorrowingTestToken.test.js | 1738 +++---- tests/loan-token/CallOptionalReturn.js | 236 +- tests/loan-token/LendingTestToken.test.js | 583 +-- tests/loan-token/LendingWithLM.test.js | 527 ++- .../loan-token/LendingwRBTCCollateral.test.js | 218 +- tests/loan-token/LendingwRBTCloan.test.js | 421 +- tests/loan-token/LoanTokenBeacon.test.js | 491 +- tests/loan-token/LoanTokenTest.js | 284 +- tests/loan-token/TradingTestToken.test.js | 1387 +++--- .../loan-token/TradingwRBTCCollateral.test.js | 424 +- tests/loan-token/TradingwRBTCLoan.test.js | 442 +- tests/loan-token/helpers.js | 403 +- tests/loan-token/tokenFunctionality.test.js | 196 +- tests/loan-token/tradingFunctions.js | 1746 +++---- tests/margin_trading_boilerplate.test.js | 273 +- tests/multisig/MultiSigKeyHolders.js | 1019 +++-- tests/multisig/MultiSigWallet.js | 628 +-- .../LoanOpeningsBorrowOrTradeFromPool.test.js | 1081 ++--- tests/other/ModifyNameAndSymbol.test.js | 42 +- tests/other/OracleIntegration.test.js | 194 +- tests/other/Protocol.test.js | 303 +- tests/other/ProtocolSettings.test.js | 1601 ++++--- tests/price-feeds/PriceFeedOracleV1Pool.js | 509 +- tests/price-feeds/PriceFeeds.js | 706 +-- tests/price-feeds/PriceFeedsLocal.js | 206 +- tests/price-feeds/USDTPriceFeed.js | 30 +- .../ChangeLoanDurationTestToken.test.js | 2769 ++++++----- tests/protocol/CloseDepositTestToken.test.js | 861 ++-- .../DepositCollateralTestToken.test.js | 477 +- tests/protocol/LiquidationTestToken.test.js | 762 +-- .../LiquidationwRBTCAsLoanToken.test.js | 138 +- tests/protocol/RolloverTestToken.test.js | 1058 +++-- .../protocol/WithdrawFeesAndInterest.test.js | 555 +-- tests/protocol/liquidationFunctions.js | 431 +- tests/protocol_settings.js | 312 +- tests/proxy/Proxy.js | 240 +- tests/staking/EnumerableBytes32Set.test.js | 145 +- tests/staking/ExtendedStakingTest.js | 2504 +++++----- tests/staking/SafeMath96.test.js | 133 +- tests/staking/StakingTest.js | 1064 +++-- tests/staking/WeightedStakingTest.js | 837 ++-- tests/stakingRewards/Rewards.js | 412 +- tests/stakingRewards/StakingRewards.js | 694 +-- tests/swaps/SwapsExternal.js | 906 ++-- tests/test_BProPriceFeed.js | 44 +- tests/test_PriceFeedRSKOracle.js | 48 +- tests/testsFixtureBoilerplate.js | 82 +- tests/token/SOVTest.js | 281 +- tests/utilities/ethereum.js | 160 +- tests/vesting/DevelopmentFund/anyone.test.js | 252 +- tests/vesting/DevelopmentFund/creator.test.js | 228 +- tests/vesting/DevelopmentFund/event.test.js | 329 +- .../DevelopmentFund/governance.test.js | 297 +- .../vesting/DevelopmentFund/multisig.test.js | 414 +- tests/vesting/DevelopmentFund/state.test.js | 1282 +++--- tests/vesting/FourYearVesting.js | 2036 ++++---- tests/vesting/GenericTokenSender.js | 341 +- tests/vesting/PostCSOV_dev.js | 372 +- tests/vesting/SVRTest.js | 365 +- tests/vesting/TeamVesting.js | 32 +- tests/vesting/TokenSender.js | 340 +- tests/vesting/Vesting.js | 2096 +++++---- tests/vesting/VestingCreator.js | 1857 ++++---- tests/vesting/VestingRegistry.js | 1301 +++--- tests/vesting/VestingRegistry2.js | 1079 +++-- tests/vesting/VestingRegistryLogic.js | 1629 ++++--- tests/vesting/VestingRegistryMigrations.js | 585 +-- 332 files changed, 72622 insertions(+), 63495 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index f270456fd..fc3b3e2b6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,15 +1,15 @@ module.exports = { - env: { - browser: true, - commonjs: true, - es2021: true, - mocha: true, // for test files - "truffle/globals": true, // same as "truffle/truffle": true - }, - extends: "prettier", - parserOptions: { - ecmaVersion: 2020, - }, - rules: {}, - plugins: ["truffle"], + env: { + browser: true, + commonjs: true, + es2021: true, + mocha: true, // for test files + "truffle/globals": true, // same as "truffle/truffle": true + }, + extends: "prettier", + parserOptions: { + ecmaVersion: 2020, + }, + rules: {}, + plugins: ["truffle"], }; diff --git a/.mocharc.json b/.mocharc.json index b67ce048c..d6be0d7b0 100644 --- a/.mocharc.json +++ b/.mocharc.json @@ -1,4 +1,4 @@ { - "require": "hardhat/register", - "timeout": 40000 + "require": "hardhat/register", + "timeout": 40000 } diff --git a/.prettierrc b/.prettierrc index f3005b77a..dbc2a0ef0 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,9 +3,9 @@ { "files": ["*.sol", "*.js", "*.test.js", "*.json"], "options": { - "printWidth": 140, + "printWidth": 99, "tabWidth": 4, - "useTabs": true, + "useTabs": false, "singleQuote": false, "bracketSpacing": true, "explicitTypes": "always" diff --git a/.solcover.js b/.solcover.js index 65e9e5cd3..d9d2a91da 100644 --- a/.solcover.js +++ b/.solcover.js @@ -1,3 +1,11 @@ module.exports = { - skipFiles: ["mockup", "testhelpers", "openzeppelin", "mixins/EnumerableAddressSet.sol", "node_modules", "interfaces", "events"], + skipFiles: [ + "mockup", + "testhelpers", + "openzeppelin", + "mixins/EnumerableAddressSet.sol", + "node_modules", + "interfaces", + "events", + ], }; diff --git a/.solhint.json b/.solhint.json index 6dade573f..ce5a7ebc1 100644 --- a/.solhint.json +++ b/.solhint.json @@ -1,9 +1,9 @@ { - "extends": "solhint:recommended", - "plugins": ["prettier"], - "rules": { - "prettier/prettier": "error", - "max-line-length": ["warn", 140], - "compiler-version": ["warn"] - } + "extends": "solhint:recommended", + "plugins": ["prettier"], + "rules": { + "prettier/prettier": "error", + "max-line-length": ["warn", 140], + "compiler-version": ["warn"] + } } diff --git a/contracts/connectors/loantoken/AdvancedToken.sol b/contracts/connectors/loantoken/AdvancedToken.sol index f2e630810..ded93591c 100644 --- a/contracts/connectors/loantoken/AdvancedToken.sol +++ b/contracts/connectors/loantoken/AdvancedToken.sol @@ -19,92 +19,92 @@ import "./AdvancedTokenStorage.sol"; * its Loan Dai iTokens. * */ contract AdvancedToken is AdvancedTokenStorage { - using SafeMath for uint256; + using SafeMath for uint256; - /** - * @notice Set an amount as the allowance of `spender` over the caller's tokens. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * IMPORTANT: Beware that changing an allowance with this method brings the risk - * that someone may use both the old and the new allowance by unfortunate - * transaction ordering. One possible solution to mitigate this race - * condition is to first reduce the spender's allowance to 0 and set the - * desired value afterwards: - * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 - * - * Emits an {Approval} event. - * - * @param _spender The account address that will be able to spend the tokens. - * @param _value The amount of tokens allowed to spend. - * */ - function approve(address _spender, uint256 _value) public returns (bool) { - allowed[msg.sender][_spender] = _value; - emit Approval(msg.sender, _spender, _value); - return true; - } + /** + * @notice Set an amount as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + * + * @param _spender The account address that will be able to spend the tokens. + * @param _value The amount of tokens allowed to spend. + * */ + function approve(address _spender, uint256 _value) public returns (bool) { + allowed[msg.sender][_spender] = _value; + emit Approval(msg.sender, _spender, _value); + return true; + } - /** - * @notice The iToken minting process. Meant to issue Loan iTokens. - * Lenders are able to open an iToken position, by minting them. - * This function is called by LoanTokenLogicStandard::_mintToken - * @param _to The recipient of the minted tTokens. - * @param _tokenAmount The amount of iTokens to be minted. - * @param _assetAmount The amount of lended tokens (asset to lend). - * @param _price The price of the lended tokens. - * @return The updated balance of the recipient. - * */ - function _mint( - address _to, - uint256 _tokenAmount, - uint256 _assetAmount, - uint256 _price - ) internal returns (uint256) { - require(_to != address(0), "15"); + /** + * @notice The iToken minting process. Meant to issue Loan iTokens. + * Lenders are able to open an iToken position, by minting them. + * This function is called by LoanTokenLogicStandard::_mintToken + * @param _to The recipient of the minted tTokens. + * @param _tokenAmount The amount of iTokens to be minted. + * @param _assetAmount The amount of lended tokens (asset to lend). + * @param _price The price of the lended tokens. + * @return The updated balance of the recipient. + * */ + function _mint( + address _to, + uint256 _tokenAmount, + uint256 _assetAmount, + uint256 _price + ) internal returns (uint256) { + require(_to != address(0), "15"); - uint256 _balance = balances[_to].add(_tokenAmount); - balances[_to] = _balance; + uint256 _balance = balances[_to].add(_tokenAmount); + balances[_to] = _balance; - totalSupply_ = totalSupply_.add(_tokenAmount); + totalSupply_ = totalSupply_.add(_tokenAmount); - emit Mint(_to, _tokenAmount, _assetAmount, _price); - emit Transfer(address(0), _to, _tokenAmount); + emit Mint(_to, _tokenAmount, _assetAmount, _price); + emit Transfer(address(0), _to, _tokenAmount); - return _balance; - } + return _balance; + } - /** - * @notice The iToken burning process. Meant to destroy Loan iTokens. - * Lenders are able to close an iToken position, by burning them. - * This function is called by LoanTokenLogicStandard::_burnToken - * @param _who The owner of the iTokens to burn. - * @param _tokenAmount The amount of iTokens to burn. - * @param _assetAmount The amount of lended tokens. - * @param _price The price of the lended tokens. - * @return The updated balance of the iTokens owner. - * */ - function _burn( - address _who, - uint256 _tokenAmount, - uint256 _assetAmount, - uint256 _price - ) internal returns (uint256) { - //bzx compare - //TODO: Unit test - uint256 _balance = balances[_who].sub(_tokenAmount, "16"); + /** + * @notice The iToken burning process. Meant to destroy Loan iTokens. + * Lenders are able to close an iToken position, by burning them. + * This function is called by LoanTokenLogicStandard::_burnToken + * @param _who The owner of the iTokens to burn. + * @param _tokenAmount The amount of iTokens to burn. + * @param _assetAmount The amount of lended tokens. + * @param _price The price of the lended tokens. + * @return The updated balance of the iTokens owner. + * */ + function _burn( + address _who, + uint256 _tokenAmount, + uint256 _assetAmount, + uint256 _price + ) internal returns (uint256) { + //bzx compare + //TODO: Unit test + uint256 _balance = balances[_who].sub(_tokenAmount, "16"); - // a rounding error may leave dust behind, so we clear this out - if (_balance <= 10) { - // We can't leave such small balance quantities. - _tokenAmount = _tokenAmount.add(_balance); - _balance = 0; - } - balances[_who] = _balance; + // a rounding error may leave dust behind, so we clear this out + if (_balance <= 10) { + // We can't leave such small balance quantities. + _tokenAmount = _tokenAmount.add(_balance); + _balance = 0; + } + balances[_who] = _balance; - totalSupply_ = totalSupply_.sub(_tokenAmount); + totalSupply_ = totalSupply_.sub(_tokenAmount); - emit Burn(_who, _tokenAmount, _assetAmount, _price); - emit Transfer(_who, address(0), _tokenAmount); - return _balance; - } + emit Burn(_who, _tokenAmount, _assetAmount, _price); + emit Transfer(_who, address(0), _tokenAmount); + return _balance; + } } diff --git a/contracts/connectors/loantoken/AdvancedTokenStorage.sol b/contracts/connectors/loantoken/AdvancedTokenStorage.sol index a1c647a25..72cb55e9c 100644 --- a/contracts/connectors/loantoken/AdvancedTokenStorage.sol +++ b/contracts/connectors/loantoken/AdvancedTokenStorage.sol @@ -18,62 +18,67 @@ import "./LoanTokenBase.sol"; * AdvancedTokenStorage and LoanTokenBase. * */ contract AdvancedTokenStorage is LoanTokenBase { - using SafeMath for uint256; - - /* Events */ - - /// topic: 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef - event Transfer(address indexed from, address indexed to, uint256 value); - - /// topic: 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925 - event Approval(address indexed owner, address indexed spender, uint256 value); - - /// topic: 0x628e75c63c1873bcd3885f7aee9f58ee36f60dc789b2a6b3a978c4189bc548ba - event AllowanceUpdate(address indexed owner, address indexed spender, uint256 valueBefore, uint256 valueAfter); - - /// topic: 0xb4c03061fb5b7fed76389d5af8f2e0ddb09f8c70d1333abbb62582835e10accb - event Mint(address indexed minter, uint256 tokenAmount, uint256 assetAmount, uint256 price); - - /// topic: 0x743033787f4738ff4d6a7225ce2bd0977ee5f86b91a902a58f5e4d0b297b4644 - event Burn(address indexed burner, uint256 tokenAmount, uint256 assetAmount, uint256 price); - - /// topic: 0xc688ff9bd4a1c369dd44c5cf64efa9db6652fb6b280aa765cd43f17d256b816e - event FlashBorrow(address borrower, address target, address loanToken, uint256 loanAmount); - - /* Storage */ - - mapping(address => uint256) internal balances; - mapping(address => mapping(address => uint256)) internal allowed; - uint256 internal totalSupply_; - - /* Functions */ - - /** - * @notice Get the total supply of iTokens. - * @return The total number of iTokens in existence as of now. - * */ - function totalSupply() public view returns (uint256) { - return totalSupply_; - } - - /** - * @notice Get the amount of iTokens owned by an account. - * @param _owner The account owner of the iTokens. - * @return The number of iTokens an account owns. - * */ - function balanceOf(address _owner) public view returns (uint256) { - return balances[_owner]; - } - - /** - * @notice Get the amount of iTokens allowed to be spent by a - * given account on behalf of the owner. - * @param _owner The account owner of the iTokens. - * @param _spender The account allowed to send the iTokens. - * @return The number of iTokens an account is allowing the spender - * to send on its behalf. - * */ - function allowance(address _owner, address _spender) public view returns (uint256) { - return allowed[_owner][_spender]; - } + using SafeMath for uint256; + + /* Events */ + + /// topic: 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef + event Transfer(address indexed from, address indexed to, uint256 value); + + /// topic: 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925 + event Approval(address indexed owner, address indexed spender, uint256 value); + + /// topic: 0x628e75c63c1873bcd3885f7aee9f58ee36f60dc789b2a6b3a978c4189bc548ba + event AllowanceUpdate( + address indexed owner, + address indexed spender, + uint256 valueBefore, + uint256 valueAfter + ); + + /// topic: 0xb4c03061fb5b7fed76389d5af8f2e0ddb09f8c70d1333abbb62582835e10accb + event Mint(address indexed minter, uint256 tokenAmount, uint256 assetAmount, uint256 price); + + /// topic: 0x743033787f4738ff4d6a7225ce2bd0977ee5f86b91a902a58f5e4d0b297b4644 + event Burn(address indexed burner, uint256 tokenAmount, uint256 assetAmount, uint256 price); + + /// topic: 0xc688ff9bd4a1c369dd44c5cf64efa9db6652fb6b280aa765cd43f17d256b816e + event FlashBorrow(address borrower, address target, address loanToken, uint256 loanAmount); + + /* Storage */ + + mapping(address => uint256) internal balances; + mapping(address => mapping(address => uint256)) internal allowed; + uint256 internal totalSupply_; + + /* Functions */ + + /** + * @notice Get the total supply of iTokens. + * @return The total number of iTokens in existence as of now. + * */ + function totalSupply() public view returns (uint256) { + return totalSupply_; + } + + /** + * @notice Get the amount of iTokens owned by an account. + * @param _owner The account owner of the iTokens. + * @return The number of iTokens an account owns. + * */ + function balanceOf(address _owner) public view returns (uint256) { + return balances[_owner]; + } + + /** + * @notice Get the amount of iTokens allowed to be spent by a + * given account on behalf of the owner. + * @param _owner The account owner of the iTokens. + * @param _spender The account allowed to send the iTokens. + * @return The number of iTokens an account is allowing the spender + * to send on its behalf. + * */ + function allowance(address _owner, address _spender) public view returns (uint256) { + return allowed[_owner][_spender]; + } } diff --git a/contracts/connectors/loantoken/LoanToken.sol b/contracts/connectors/loantoken/LoanToken.sol index ab70b3ad4..d5d65f952 100644 --- a/contracts/connectors/loantoken/LoanToken.sol +++ b/contracts/connectors/loantoken/LoanToken.sol @@ -27,119 +27,119 @@ import "./AdvancedTokenStorage.sol"; * https://eips.ethereum.org/EIPS/eip-1822. It's really hard to work with this. * */ contract LoanToken is AdvancedTokenStorage { - /// @dev It is important to maintain the variables order so the delegate - /// calls can access sovrynContractAddress and wrbtcTokenAddress - address public sovrynContractAddress; - address public wrbtcTokenAddress; - address internal target_; - address public admin; + /// @dev It is important to maintain the variables order so the delegate + /// calls can access sovrynContractAddress and wrbtcTokenAddress + address public sovrynContractAddress; + address public wrbtcTokenAddress; + address internal target_; + address public admin; - /** - * @notice Deploy loan token proxy. - * Sets ERC20 parameters of the token. - * - * @param _newOwner The address of the new owner. - * @param _newTarget The address of the new target contract instance. - * @param _sovrynContractAddress The address of the new sovrynContract instance. - * @param _wrbtcTokenAddress The address of the new wrBTC instance. - * */ - constructor( - address _newOwner, - address _newTarget, - address _sovrynContractAddress, - address _wrbtcTokenAddress - ) public { - transferOwnership(_newOwner); - _setTarget(_newTarget); - _setSovrynContractAddress(_sovrynContractAddress); - _setWrbtcTokenAddress(_wrbtcTokenAddress); - } + /** + * @notice Deploy loan token proxy. + * Sets ERC20 parameters of the token. + * + * @param _newOwner The address of the new owner. + * @param _newTarget The address of the new target contract instance. + * @param _sovrynContractAddress The address of the new sovrynContract instance. + * @param _wrbtcTokenAddress The address of the new wrBTC instance. + * */ + constructor( + address _newOwner, + address _newTarget, + address _sovrynContractAddress, + address _wrbtcTokenAddress + ) public { + transferOwnership(_newOwner); + _setTarget(_newTarget); + _setSovrynContractAddress(_sovrynContractAddress); + _setWrbtcTokenAddress(_wrbtcTokenAddress); + } - /** - * @notice Fallback function performs a delegate call - * to the actual implementation address is pointing this proxy. - * Returns whatever the implementation call returns. - * */ - function() external payable { - if (gasleft() <= 2300) { - return; - } + /** + * @notice Fallback function performs a delegate call + * to the actual implementation address is pointing this proxy. + * Returns whatever the implementation call returns. + * */ + function() external payable { + if (gasleft() <= 2300) { + return; + } - address target = target_; - bytes memory data = msg.data; - assembly { - let result := delegatecall(gas, target, add(data, 0x20), mload(data), 0, 0) - let size := returndatasize - let ptr := mload(0x40) - returndatacopy(ptr, 0, size) - switch result - case 0 { - revert(ptr, size) - } - default { - return(ptr, size) - } - } - } + address target = target_; + bytes memory data = msg.data; + assembly { + let result := delegatecall(gas, target, add(data, 0x20), mload(data), 0, 0) + let size := returndatasize + let ptr := mload(0x40) + returndatacopy(ptr, 0, size) + switch result + case 0 { + revert(ptr, size) + } + default { + return(ptr, size) + } + } + } - /** - * @notice Public owner setter for target address. - * @dev Calls internal setter. - * @param _newTarget The address of the new target contract instance. - * */ - function setTarget(address _newTarget) public onlyOwner { - _setTarget(_newTarget); - } + /** + * @notice Public owner setter for target address. + * @dev Calls internal setter. + * @param _newTarget The address of the new target contract instance. + * */ + function setTarget(address _newTarget) public onlyOwner { + _setTarget(_newTarget); + } - /** - * @notice Internal setter for target address. - * @param _newTarget The address of the new target contract instance. - * */ - function _setTarget(address _newTarget) internal { - require(Address.isContract(_newTarget), "target not a contract"); - target_ = _newTarget; - } + /** + * @notice Internal setter for target address. + * @param _newTarget The address of the new target contract instance. + * */ + function _setTarget(address _newTarget) internal { + require(Address.isContract(_newTarget), "target not a contract"); + target_ = _newTarget; + } - /** - * @notice Internal setter for sovrynContract address. - * @param _sovrynContractAddress The address of the new sovrynContract instance. - * */ - function _setSovrynContractAddress(address _sovrynContractAddress) internal { - require(Address.isContract(_sovrynContractAddress), "sovryn not a contract"); - sovrynContractAddress = _sovrynContractAddress; - } + /** + * @notice Internal setter for sovrynContract address. + * @param _sovrynContractAddress The address of the new sovrynContract instance. + * */ + function _setSovrynContractAddress(address _sovrynContractAddress) internal { + require(Address.isContract(_sovrynContractAddress), "sovryn not a contract"); + sovrynContractAddress = _sovrynContractAddress; + } - /** - * @notice Internal setter for wrBTC address. - * @param _wrbtcTokenAddress The address of the new wrBTC instance. - * */ - function _setWrbtcTokenAddress(address _wrbtcTokenAddress) internal { - require(Address.isContract(_wrbtcTokenAddress), "wrbtc not a contract"); - wrbtcTokenAddress = _wrbtcTokenAddress; - } + /** + * @notice Internal setter for wrBTC address. + * @param _wrbtcTokenAddress The address of the new wrBTC instance. + * */ + function _setWrbtcTokenAddress(address _wrbtcTokenAddress) internal { + require(Address.isContract(_wrbtcTokenAddress), "wrbtc not a contract"); + wrbtcTokenAddress = _wrbtcTokenAddress; + } - /** - * @notice Public owner cloner for pointed loan token. - * Sets ERC20 parameters of the token. - * - * @dev TODO: add check for double init. - * idk but init usually can be called only once. - * - * @param _loanTokenAddress The address of the pointed loan token instance. - * @param _name The ERC20 token name. - * @param _symbol The ERC20 token symbol. - * */ - function initialize( - address _loanTokenAddress, - string memory _name, - string memory _symbol - ) public onlyOwner { - loanTokenAddress = _loanTokenAddress; + /** + * @notice Public owner cloner for pointed loan token. + * Sets ERC20 parameters of the token. + * + * @dev TODO: add check for double init. + * idk but init usually can be called only once. + * + * @param _loanTokenAddress The address of the pointed loan token instance. + * @param _name The ERC20 token name. + * @param _symbol The ERC20 token symbol. + * */ + function initialize( + address _loanTokenAddress, + string memory _name, + string memory _symbol + ) public onlyOwner { + loanTokenAddress = _loanTokenAddress; - name = _name; - symbol = _symbol; - decimals = IERC20(loanTokenAddress).decimals(); + name = _name; + symbol = _symbol; + decimals = IERC20(loanTokenAddress).decimals(); - initialPrice = 10**18; /// starting price of 1 - } + initialPrice = 10**18; /// starting price of 1 + } } diff --git a/contracts/connectors/loantoken/LoanTokenBase.sol b/contracts/connectors/loantoken/LoanTokenBase.sol index 556df20e9..5ea1d9352 100644 --- a/contracts/connectors/loantoken/LoanTokenBase.sol +++ b/contracts/connectors/loantoken/LoanTokenBase.sol @@ -36,42 +36,42 @@ import "./Pausable.sol"; * use the BZRX tokens, which are only used to pay fees on the network currently. * */ contract LoanTokenBase is ReentrancyGuard, Ownable, Pausable { - uint256 internal constant WEI_PRECISION = 10**18; - uint256 internal constant WEI_PERCENT_PRECISION = 10**20; + uint256 internal constant WEI_PRECISION = 10**18; + uint256 internal constant WEI_PERCENT_PRECISION = 10**20; - int256 internal constant sWEI_PRECISION = 10**18; + int256 internal constant sWEI_PRECISION = 10**18; - /// @notice Standard ERC-20 properties - string public name; - string public symbol; - uint8 public decimals; + /// @notice Standard ERC-20 properties + string public name; + string public symbol; + uint8 public decimals; - /// @notice The address of the loan token (asset to lend) instance. - address public loanTokenAddress; + /// @notice The address of the loan token (asset to lend) instance. + address public loanTokenAddress; - uint256 public baseRate; - uint256 public rateMultiplier; - uint256 public lowUtilBaseRate; - uint256 public lowUtilRateMultiplier; + uint256 public baseRate; + uint256 public rateMultiplier; + uint256 public lowUtilBaseRate; + uint256 public lowUtilRateMultiplier; - uint256 public targetLevel; - uint256 public kinkLevel; - uint256 public maxScaleRate; + uint256 public targetLevel; + uint256 public kinkLevel; + uint256 public maxScaleRate; - uint256 internal _flTotalAssetSupply; - uint256 public checkpointSupply; - uint256 public initialPrice; + uint256 internal _flTotalAssetSupply; + uint256 public checkpointSupply; + uint256 public initialPrice; - /// uint88 for tight packing -> 8 + 88 + 160 = 256 - uint88 internal lastSettleTime_; + /// uint88 for tight packing -> 8 + 88 + 160 = 256 + uint88 internal lastSettleTime_; - /// Mapping of keccak256(collateralToken, isTorqueLoan) to loanParamsId. - mapping(uint256 => bytes32) public loanParamsIds; + /// Mapping of keccak256(collateralToken, isTorqueLoan) to loanParamsId. + mapping(uint256 => bytes32) public loanParamsIds; - /// Price of token at last user checkpoint. - mapping(address => uint256) internal checkpointPrices_; + /// Price of token at last user checkpoint. + mapping(address => uint256) internal checkpointPrices_; - // the maximum trading/borrowing/lending limit per token address - mapping(address => uint256) public transactionLimit; - // 0 -> no limit + // the maximum trading/borrowing/lending limit per token address + mapping(address => uint256) public transactionLimit; + // 0 -> no limit } diff --git a/contracts/connectors/loantoken/LoanTokenLogicBeacon.sol b/contracts/connectors/loantoken/LoanTokenLogicBeacon.sol index 4a3d6c221..15539ba1d 100644 --- a/contracts/connectors/loantoken/LoanTokenLogicBeacon.sol +++ b/contracts/connectors/loantoken/LoanTokenLogicBeacon.sol @@ -16,129 +16,147 @@ import "../../openzeppelin/Address.sol"; */ contract LoanTokenLogicBeacon is PausableOz { - using EnumerableBytes32Set for EnumerableBytes32Set.Bytes32Set; // enumerable map of bytes32 or addresses - using EnumerableBytes4Set for EnumerableBytes4Set.Bytes4Set; // enumerable map of bytes4 or addresses - - mapping(bytes4 => address) private logicTargets; - - struct LoanTokenLogicModuleUpdate { - address implementation; // address implementaion of the module - uint256 updateTimestamp; // time of update - } - - mapping(bytes32 => LoanTokenLogicModuleUpdate[]) public moduleUpgradeLog; /** the module name as the key */ - - mapping(bytes32 => uint256) public activeModuleIndex; /** To store the current active index log for module */ - - mapping(bytes32 => EnumerableBytes4Set.Bytes4Set) private activeFuncSignatureList; /** Store the current active function signature */ - - /** - * @dev Modifier to make a function callable only when the contract is not paused. - * This is the overriden function from the pausable contract, so that we can use custom error message. - */ - modifier whenNotPaused() { - require(!_paused, "LoanTokenLogicBeacon:paused mode"); - _; - } - - /** - * @notice Register the loanTokenModule (LoanTokenSettingsLowerAdmin, LoanTokenLogicLM / LoanTokenLogicWrbtc, etc) - * - * @dev This function will store the updated protocol module to the storage (For rollback purposes) - * - * @param loanTokenModuleAddress The module target address - */ - function registerLoanTokenModule(address loanTokenModuleAddress) external onlyOwner { - bytes32 moduleName = _registerLoanTokenModule(loanTokenModuleAddress); - - // Store the upgrade to the log - moduleUpgradeLog[moduleName].push(LoanTokenLogicModuleUpdate(loanTokenModuleAddress, block.timestamp)); - activeModuleIndex[moduleName] = moduleUpgradeLog[moduleName].length - 1; - } - - /** - * @notice Register the loanTokenModule (LoanTokenSettingsLowerAdmin, LoanTokenLogicLM / LoanTokenLogicWrbtc, etc) - * - * @dev This registration will require target contract to have the exact function getListFunctionSignatures() which will return functionSignatureList and the moduleName in bytes32 - * - * @param loanTokenModuleAddress the target logic of the loan token module - * - * @return the module name - */ - function _registerLoanTokenModule(address loanTokenModuleAddress) private returns (bytes32) { - require(Address.isContract(loanTokenModuleAddress), "LoanTokenModuleAddress is not a contract"); - - // Get the list of function signature on this loanTokenModulesAddress - (bytes4[] memory functionSignatureList, bytes32 moduleName) = - ILoanTokenLogicModules(loanTokenModuleAddress).getListFunctionSignatures(); - - /// register / update the module function signature address implementation - for (uint256 i; i < functionSignatureList.length; i++) { - require(functionSignatureList[i] != bytes4(0x0), "ERR_EMPTY_FUNC_SIGNATURE"); - logicTargets[functionSignatureList[i]] = loanTokenModuleAddress; - if (!activeFuncSignatureList[moduleName].contains(functionSignatureList[i])) - activeFuncSignatureList[moduleName].addBytes4(functionSignatureList[i]); - } - - /// delete the "removed" module function signature in the current implementation - bytes4[] memory activeSignatureListEnum = - activeFuncSignatureList[moduleName].enumerate(0, activeFuncSignatureList[moduleName].length()); - for (uint256 i; i < activeSignatureListEnum.length; i++) { - bytes4 activeSigBytes = activeSignatureListEnum[i]; - if (logicTargets[activeSigBytes] != loanTokenModuleAddress) { - logicTargets[activeSigBytes] = address(0); - activeFuncSignatureList[moduleName].removeBytes4(activeSigBytes); - } - } - - return moduleName; - } - - /** - * @dev get all active function signature list based on the module name. - * - * @param moduleName in bytes32. - * - * @return the array of function signature. - */ - function getActiveFuncSignatureList(bytes32 moduleName) public view returns (bytes4[] memory signatureList) { - signatureList = activeFuncSignatureList[moduleName].enumerate(0, activeFuncSignatureList[moduleName].length()); - return signatureList; - } - - /** - * @dev Get total length of the module upgrade log. - * - * @param moduleName in bytes32. - * - * @return length of module upgrade log. - */ - function getModuleUpgradeLogLength(bytes32 moduleName) external view returns (uint256) { - return moduleUpgradeLog[moduleName].length; - } - - /** - * @notice This function will rollback particular module to the spesific index / version of deployment - * - * @param moduleName Name of module in bytes32 format - * @param index index / version of previous deployment - */ - function rollback(bytes32 moduleName, uint256 index) external onlyOwner { - address loanTokenModuleAddress = moduleUpgradeLog[moduleName][index].implementation; - moduleName = _registerLoanTokenModule(loanTokenModuleAddress); - activeModuleIndex[moduleName] = index; - } - - /** - * @notice External getter for target addresses. - * @param sig The signature. - * @return The address for a given signature. - * */ - function getTarget(bytes4 sig) external view whenNotPaused returns (address) { - return logicTargets[sig]; - } + using EnumerableBytes32Set for EnumerableBytes32Set.Bytes32Set; // enumerable map of bytes32 or addresses + using EnumerableBytes4Set for EnumerableBytes4Set.Bytes4Set; // enumerable map of bytes4 or addresses + + mapping(bytes4 => address) private logicTargets; + + struct LoanTokenLogicModuleUpdate { + address implementation; // address implementaion of the module + uint256 updateTimestamp; // time of update + } + + mapping(bytes32 => LoanTokenLogicModuleUpdate[]) public moduleUpgradeLog; /** the module name as the key */ + + mapping(bytes32 => uint256) public activeModuleIndex; /** To store the current active index log for module */ + + mapping(bytes32 => EnumerableBytes4Set.Bytes4Set) private activeFuncSignatureList; /** Store the current active function signature */ + + /** + * @dev Modifier to make a function callable only when the contract is not paused. + * This is the overriden function from the pausable contract, so that we can use custom error message. + */ + modifier whenNotPaused() { + require(!_paused, "LoanTokenLogicBeacon:paused mode"); + _; + } + + /** + * @notice Register the loanTokenModule (LoanTokenSettingsLowerAdmin, LoanTokenLogicLM / LoanTokenLogicWrbtc, etc) + * + * @dev This function will store the updated protocol module to the storage (For rollback purposes) + * + * @param loanTokenModuleAddress The module target address + */ + function registerLoanTokenModule(address loanTokenModuleAddress) external onlyOwner { + bytes32 moduleName = _registerLoanTokenModule(loanTokenModuleAddress); + + // Store the upgrade to the log + moduleUpgradeLog[moduleName].push( + LoanTokenLogicModuleUpdate(loanTokenModuleAddress, block.timestamp) + ); + activeModuleIndex[moduleName] = moduleUpgradeLog[moduleName].length - 1; + } + + /** + * @notice Register the loanTokenModule (LoanTokenSettingsLowerAdmin, LoanTokenLogicLM / LoanTokenLogicWrbtc, etc) + * + * @dev This registration will require target contract to have the exact function getListFunctionSignatures() which will return functionSignatureList and the moduleName in bytes32 + * + * @param loanTokenModuleAddress the target logic of the loan token module + * + * @return the module name + */ + function _registerLoanTokenModule(address loanTokenModuleAddress) private returns (bytes32) { + require( + Address.isContract(loanTokenModuleAddress), + "LoanTokenModuleAddress is not a contract" + ); + + // Get the list of function signature on this loanTokenModulesAddress + (bytes4[] memory functionSignatureList, bytes32 moduleName) = + ILoanTokenLogicModules(loanTokenModuleAddress).getListFunctionSignatures(); + + /// register / update the module function signature address implementation + for (uint256 i; i < functionSignatureList.length; i++) { + require(functionSignatureList[i] != bytes4(0x0), "ERR_EMPTY_FUNC_SIGNATURE"); + logicTargets[functionSignatureList[i]] = loanTokenModuleAddress; + if (!activeFuncSignatureList[moduleName].contains(functionSignatureList[i])) + activeFuncSignatureList[moduleName].addBytes4(functionSignatureList[i]); + } + + /// delete the "removed" module function signature in the current implementation + bytes4[] memory activeSignatureListEnum = + activeFuncSignatureList[moduleName].enumerate( + 0, + activeFuncSignatureList[moduleName].length() + ); + for (uint256 i; i < activeSignatureListEnum.length; i++) { + bytes4 activeSigBytes = activeSignatureListEnum[i]; + if (logicTargets[activeSigBytes] != loanTokenModuleAddress) { + logicTargets[activeSigBytes] = address(0); + activeFuncSignatureList[moduleName].removeBytes4(activeSigBytes); + } + } + + return moduleName; + } + + /** + * @dev get all active function signature list based on the module name. + * + * @param moduleName in bytes32. + * + * @return the array of function signature. + */ + function getActiveFuncSignatureList(bytes32 moduleName) + public + view + returns (bytes4[] memory signatureList) + { + signatureList = activeFuncSignatureList[moduleName].enumerate( + 0, + activeFuncSignatureList[moduleName].length() + ); + return signatureList; + } + + /** + * @dev Get total length of the module upgrade log. + * + * @param moduleName in bytes32. + * + * @return length of module upgrade log. + */ + function getModuleUpgradeLogLength(bytes32 moduleName) external view returns (uint256) { + return moduleUpgradeLog[moduleName].length; + } + + /** + * @notice This function will rollback particular module to the spesific index / version of deployment + * + * @param moduleName Name of module in bytes32 format + * @param index index / version of previous deployment + */ + function rollback(bytes32 moduleName, uint256 index) external onlyOwner { + address loanTokenModuleAddress = moduleUpgradeLog[moduleName][index].implementation; + moduleName = _registerLoanTokenModule(loanTokenModuleAddress); + activeModuleIndex[moduleName] = index; + } + + /** + * @notice External getter for target addresses. + * @param sig The signature. + * @return The address for a given signature. + * */ + function getTarget(bytes4 sig) external view whenNotPaused returns (address) { + return logicTargets[sig]; + } } interface ILoanTokenLogicModules { - function getListFunctionSignatures() external pure returns (bytes4[] memory, bytes32 moduleName); + function getListFunctionSignatures() + external + pure + returns (bytes4[] memory, bytes32 moduleName); } diff --git a/contracts/connectors/loantoken/LoanTokenLogicProxy.sol b/contracts/connectors/loantoken/LoanTokenLogicProxy.sol index f892e4e9a..bff883e8b 100644 --- a/contracts/connectors/loantoken/LoanTokenLogicProxy.sol +++ b/contracts/connectors/loantoken/LoanTokenLogicProxy.sol @@ -12,94 +12,101 @@ import "../../openzeppelin/Initializable.sol"; * */ contract LoanTokenLogicProxy is AdvancedTokenStorage { - /** - * @notice PLEASE DO NOT ADD ANY VARIABLES HERE UNLESS FOR SPESIFIC SLOT - */ + /** + * @notice PLEASE DO NOT ADD ANY VARIABLES HERE UNLESS FOR SPESIFIC SLOT + */ - /// ------------- MUST BE THE SAME AS IN LoanToken CONTRACT ------------------- - address public sovrynContractAddress; - address public wrbtcTokenAddress; - address public target_; - address public admin; - /// ------------- END MUST BE THE SAME AS IN LoanToken CONTRACT ------------------- + /// ------------- MUST BE THE SAME AS IN LoanToken CONTRACT ------------------- + address public sovrynContractAddress; + address public wrbtcTokenAddress; + address public target_; + address public admin; + /// ------------- END MUST BE THE SAME AS IN LoanToken CONTRACT ------------------- - /** - * @notice PLEASE DO NOT ADD ANY VARIABLES HERE UNLESS FOR SPESIFIC SLOT (CONSTANT / IMMUTABLE) - */ + /** + * @notice PLEASE DO NOT ADD ANY VARIABLES HERE UNLESS FOR SPESIFIC SLOT (CONSTANT / IMMUTABLE) + */ - bytes32 internal constant LOAN_TOKEN_LOGIC_BEACON_ADDRESS_SLOT = keccak256("LOAN_TOKEN_LOGIC_BEACON_ADDRESS_SLOT"); + bytes32 internal constant LOAN_TOKEN_LOGIC_BEACON_ADDRESS_SLOT = + keccak256("LOAN_TOKEN_LOGIC_BEACON_ADDRESS_SLOT"); - modifier onlyAdmin() { - require(isOwner(), "LoanTokenLogicProxy:unauthorized"); - _; - } + modifier onlyAdmin() { + require(isOwner(), "LoanTokenLogicProxy:unauthorized"); + _; + } - /** - * @notice Fallback function performs a logic implementation address query to LoanTokenLogicBeacon and then do delegate call to that query result address. - * Returns whatever the implementation call returns. - * */ - function() external payable { - // query the logic target implementation address from the LoanTokenLogicBeacon - address target = ILoanTokenLogicBeacon(_beaconAddress()).getTarget(msg.sig); - require(target != address(0), "LoanTokenLogicProxy:target not active"); + /** + * @notice Fallback function performs a logic implementation address query to LoanTokenLogicBeacon and then do delegate call to that query result address. + * Returns whatever the implementation call returns. + * */ + function() external payable { + // query the logic target implementation address from the LoanTokenLogicBeacon + address target = ILoanTokenLogicBeacon(_beaconAddress()).getTarget(msg.sig); + require(target != address(0), "LoanTokenLogicProxy:target not active"); - bytes memory data = msg.data; - assembly { - let result := delegatecall(gas, target, add(data, 0x20), mload(data), 0, 0) - let size := returndatasize - let ptr := mload(0x40) - returndatacopy(ptr, 0, size) - switch result - case 0 { - revert(ptr, size) - } - default { - return(ptr, size) - } - } - } + bytes memory data = msg.data; + assembly { + let result := delegatecall(gas, target, add(data, 0x20), mload(data), 0, 0) + let size := returndatasize + let ptr := mload(0x40) + returndatacopy(ptr, 0, size) + switch result + case 0 { + revert(ptr, size) + } + default { + return(ptr, size) + } + } + } - /** - * @dev Returns the current Loan Token logic Beacon. - * @return Address of the current LoanTokenLogicBeacon. - */ - function _beaconAddress() internal view returns (address beaconAddress) { - bytes32 slot = LOAN_TOKEN_LOGIC_BEACON_ADDRESS_SLOT; - assembly { - beaconAddress := sload(slot) - } - } + /** + * @dev Returns the current Loan Token logic Beacon. + * @return Address of the current LoanTokenLogicBeacon. + */ + function _beaconAddress() internal view returns (address beaconAddress) { + bytes32 slot = LOAN_TOKEN_LOGIC_BEACON_ADDRESS_SLOT; + assembly { + beaconAddress := sload(slot) + } + } - /** - * @return The address of the current LoanTokenLogicBeacon. - */ - function beaconAddress() external view returns (address) { - return _beaconAddress(); - } + /** + * @return The address of the current LoanTokenLogicBeacon. + */ + function beaconAddress() external view returns (address) { + return _beaconAddress(); + } - /** - * @dev Set/update the new beacon address. - * @param _newBeaconAddress Address of the new LoanTokenLogicBeacon. - */ - function _setBeaconAddress(address _newBeaconAddress) private { - require(Address.isContract(_newBeaconAddress), "Cannot set beacon address to a non-contract address"); + /** + * @dev Set/update the new beacon address. + * @param _newBeaconAddress Address of the new LoanTokenLogicBeacon. + */ + function _setBeaconAddress(address _newBeaconAddress) private { + require( + Address.isContract(_newBeaconAddress), + "Cannot set beacon address to a non-contract address" + ); - bytes32 slot = LOAN_TOKEN_LOGIC_BEACON_ADDRESS_SLOT; + bytes32 slot = LOAN_TOKEN_LOGIC_BEACON_ADDRESS_SLOT; - assembly { - sstore(slot, _newBeaconAddress) - } - } + assembly { + sstore(slot, _newBeaconAddress) + } + } - /** - * @dev External function to set the new LoanTokenLogicBeacon Address - * @param _newBeaconAddress Address of the new LoanTokenLogicBeacon - */ - function setBeaconAddress(address _newBeaconAddress) external onlyAdmin { - _setBeaconAddress(_newBeaconAddress); - } + /** + * @dev External function to set the new LoanTokenLogicBeacon Address + * @param _newBeaconAddress Address of the new LoanTokenLogicBeacon + */ + function setBeaconAddress(address _newBeaconAddress) external onlyAdmin { + _setBeaconAddress(_newBeaconAddress); + } } interface ILoanTokenLogicBeacon { - function getTarget(bytes4 functionSignature) external view returns (address logicTargetAddress); + function getTarget(bytes4 functionSignature) + external + view + returns (address logicTargetAddress); } diff --git a/contracts/connectors/loantoken/LoanTokenLogicStandard.sol b/contracts/connectors/loantoken/LoanTokenLogicStandard.sol index f318773a1..3347964e1 100644 --- a/contracts/connectors/loantoken/LoanTokenLogicStandard.sol +++ b/contracts/connectors/loantoken/LoanTokenLogicStandard.sol @@ -47,58 +47,66 @@ import "../../farm/ILiquidityMining.sol"; * individuals. * */ contract LoanTokenLogicStandard is LoanTokenLogicStorage { - using SafeMath for uint256; - using SignedSafeMath for int256; - - /* Events */ - - event WithdrawRBTCTo(address indexed to, uint256 amount); - - /// DON'T ADD VARIABLES HERE, PLEASE - - /* Public functions */ - - /** - * @notice Mint loan token wrapper. - * Adds a check before calling low level _mintToken function. - * The function retrieves the tokens from the message sender, so make sure - * to first approve the loan token contract to access your funds. This is - * done by calling approve(address spender, uint amount) on the ERC20 - * token contract, where spender is the loan token contract address and - * amount is the amount to be deposited. - * - * @param receiver The account getting the minted tokens. - * @param depositAmount The amount of underlying tokens provided on the - * loan. (Not the number of loan tokens to mint). - * - * @return The amount of loan tokens minted. - * */ - function mint(address receiver, uint256 depositAmount) external nonReentrant returns (uint256 mintAmount) { - return _mintToken(receiver, depositAmount); - } - - /** - * @notice Burn loan token wrapper. - * Adds a pay-out transfer after calling low level _burnToken function. - * In order to withdraw funds to the pool, call burn on the respective - * loan token contract. This will burn your loan tokens and send you the - * underlying token in exchange. - * - * @param receiver The account getting the minted tokens. - * @param burnAmount The amount of loan tokens to redeem. - * - * @return The amount of underlying tokens payed to lender. - * */ - function burn(address receiver, uint256 burnAmount) external nonReentrant returns (uint256 loanAmountPaid) { - loanAmountPaid = _burnToken(burnAmount); - - //this needs to be here and not in _burnTokens because of the WRBTC implementation - if (loanAmountPaid != 0) { - _safeTransfer(loanTokenAddress, receiver, loanAmountPaid, "5"); - } - } - - /* + using SafeMath for uint256; + using SignedSafeMath for int256; + + /* Events */ + + event WithdrawRBTCTo(address indexed to, uint256 amount); + + /// DON'T ADD VARIABLES HERE, PLEASE + + /* Public functions */ + + /** + * @notice Mint loan token wrapper. + * Adds a check before calling low level _mintToken function. + * The function retrieves the tokens from the message sender, so make sure + * to first approve the loan token contract to access your funds. This is + * done by calling approve(address spender, uint amount) on the ERC20 + * token contract, where spender is the loan token contract address and + * amount is the amount to be deposited. + * + * @param receiver The account getting the minted tokens. + * @param depositAmount The amount of underlying tokens provided on the + * loan. (Not the number of loan tokens to mint). + * + * @return The amount of loan tokens minted. + * */ + function mint(address receiver, uint256 depositAmount) + external + nonReentrant + returns (uint256 mintAmount) + { + return _mintToken(receiver, depositAmount); + } + + /** + * @notice Burn loan token wrapper. + * Adds a pay-out transfer after calling low level _burnToken function. + * In order to withdraw funds to the pool, call burn on the respective + * loan token contract. This will burn your loan tokens and send you the + * underlying token in exchange. + * + * @param receiver The account getting the minted tokens. + * @param burnAmount The amount of loan tokens to redeem. + * + * @return The amount of underlying tokens payed to lender. + * */ + function burn(address receiver, uint256 burnAmount) + external + nonReentrant + returns (uint256 loanAmountPaid) + { + loanAmountPaid = _burnToken(burnAmount); + + //this needs to be here and not in _burnTokens because of the WRBTC implementation + if (loanAmountPaid != 0) { + _safeTransfer(loanTokenAddress, receiver, loanAmountPaid, "5"); + } + } + + /* flashBorrow is disabled for the MVP, but is going to be added later. therefore, it needs to be revised @@ -165,1431 +173,1610 @@ contract LoanTokenLogicStandard is LoanTokenLogicStorage { } */ - /** - * @notice Borrow funds from the pool. - * The underlying loan token may not be used as collateral. - * - * @param loanId The ID of the loan, 0 for a new loan. - * @param withdrawAmount The amount to be withdrawn (actually borrowed). - * @param initialLoanDuration The duration of the loan in seconds. - * If the loan is not paid back until then, it'll need to be rolled over. - * @param collateralTokenSent The amount of collateral tokens provided by the user. - * (150% of the withdrawn amount worth in collateral tokens). - * @param collateralTokenAddress The address of the token to be used as - * collateral. Cannot be the loan token address. - * @param borrower The one paying for the collateral. - * @param receiver The one receiving the withdrawn amount. - * - * @return New principal and new collateral added to loan. - * */ - function borrow( - bytes32 loanId, /// 0 if new loan. - uint256 withdrawAmount, - uint256 initialLoanDuration, /// Duration in seconds. - uint256 collateralTokenSent, /// If 0, loanId must be provided; any rBTC sent must equal this value. - address collateralTokenAddress, /// If address(0), this means rBTC and rBTC must be sent with the call or loanId must be provided. - address borrower, - address receiver, - bytes memory /// loanDataBytes: arbitrary order data (for future use). - ) - public - payable - nonReentrant /// Note: needs to be removed to allow flashloan use cases. - returns ( - uint256, - uint256 /// Returns new principal and new collateral added to loan. - ) - { - require(withdrawAmount != 0, "6"); - - _checkPause(); - - /// Temporary: limit transaction size. - if (transactionLimit[collateralTokenAddress] > 0) require(collateralTokenSent <= transactionLimit[collateralTokenAddress]); - - require( - (msg.value == 0 || msg.value == collateralTokenSent) && - (collateralTokenSent != 0 || loanId != 0) && - (collateralTokenAddress != address(0) || msg.value != 0 || loanId != 0) && - (loanId == 0 || msg.sender == borrower), - "7" - ); - - /// @dev We have an issue regarding contract size code is too big. 1 of the solution is need to keep the error message 32 bytes length - // Temporarily, we combine this require to the above, so can save the contract size code - // require(collateralTokenSent != 0 || loanId != 0, "8"); - // require(collateralTokenAddress != address(0) || msg.value != 0 || loanId != 0, "9"); - - /// @dev Ensure authorized use of existing loan. - // require(loanId == 0 || msg.sender == borrower, "401 use of existing loan"); - - /// @dev The condition is never met. - /// Address zero is not allowed by previous require validation. - /// This check is unneeded and was lowering the test coverage index. - // if (collateralTokenAddress == address(0)) { - // collateralTokenAddress = wrbtcTokenAddress; - // } - - require(collateralTokenAddress != loanTokenAddress, "10"); - - _settleInterest(); - - address[4] memory sentAddresses; - uint256[5] memory sentAmounts; - - sentAddresses[0] = address(this); /// The lender. - sentAddresses[1] = borrower; - sentAddresses[2] = receiver; - /// sentAddresses[3] = address(0); /// The manager. - - sentAmounts[1] = withdrawAmount; - - /// interestRate, interestInitialAmount, borrowAmount (newBorrowAmount). - (sentAmounts[0], sentAmounts[2], sentAmounts[1]) = _getInterestRateAndBorrowAmount( - sentAmounts[1], - _totalAssetSupply(0), /// Interest is settled above. - initialLoanDuration - ); - - /// sentAmounts[3] = 0; /// loanTokenSent - sentAmounts[4] = collateralTokenSent; - - return - _borrowOrTrade( - loanId, - withdrawAmount, - ProtocolSettingsLike(sovrynContractAddress).minInitialMargin( - loanParamsIds[uint256(keccak256(abi.encodePacked(collateralTokenAddress, true)))] - ), - collateralTokenAddress, - sentAddresses, - sentAmounts, - "" /// loanDataBytes - ); - } - - /** - * @notice Borrow and immediately get into a position. - * - * Trading on margin is used to increase an investor's buying power. - * Margin is the amount of money required to open a position, while - * leverage is the multiple of exposure to account equity. - * - * Leverage allows you to trade positions LARGER than the amount - * of money in your trading account. Leverage is expressed as a ratio. - * - * When trading on margin, investors first deposit some token that then - * serves as collateral for the loan, and then pay ongoing interest - * payments on the money they borrow. - * - * Margin trading = taking a loan and swapping it: - * In order to open a margin trade position, - * 1.- The user calls marginTrade on the loan token contract. - * 2.- The loan token contract provides the loan and sends it for processing - * to the protocol proxy contract. - * 3.- The protocol proxy contract uses the module LoanOpening to create a - * position and swaps the loan tokens to collateral tokens. - * 4.- The Sovryn Swap network looks up the correct converter and swaps the - * tokens. - * If successful, the position is being held by the protocol proxy contract, - * which is why positions need to be closed at the protocol proxy contract. - * - * @param loanId The ID of the loan, 0 for a new loan. - * @param leverageAmount The multiple of exposure: 2x ... 5x. The leverage with 18 decimals. - * @param loanTokenSent The number of loan tokens provided by the user. - * @param collateralTokenSent The amount of collateral tokens provided by the user. - * @param collateralTokenAddress The token address of collateral. - * @param trader The account that performs this trade. - * @param minReturn Minimum amount (position size) in the collateral tokens - * @param loanDataBytes Additional loan data (not in use for token swaps). - * - * @return New principal and new collateral added to trade. - * */ - function marginTrade( - bytes32 loanId, /// 0 if new loan - uint256 leverageAmount, /// Expected in x * 10**18 where x is the actual leverage (2, 3, 4, or 5). - uint256 loanTokenSent, - uint256 collateralTokenSent, - address collateralTokenAddress, - address trader, - uint256 minReturn, // minimum position size in the collateral tokens - bytes memory loanDataBytes /// Arbitrary order data. - ) - public - payable - nonReentrant /// Note: needs to be removed to allow flashloan use cases. - returns ( - uint256, - uint256 /// Returns new principal and new collateral added to trade. - ) - { - _checkPause(); - - checkPriceDivergence(leverageAmount, loanTokenSent, collateralTokenSent, collateralTokenAddress, minReturn); - - if (collateralTokenAddress == address(0)) { - collateralTokenAddress = wrbtcTokenAddress; - } - - require(collateralTokenAddress != loanTokenAddress, "11"); - - /// @dev Ensure authorized use of existing loan. - require(loanId == 0 || msg.sender == trader, "401 use of existing loan"); - - /// Temporary: limit transaction size. - if (transactionLimit[collateralTokenAddress] > 0) require(collateralTokenSent <= transactionLimit[collateralTokenAddress]); - if (transactionLimit[loanTokenAddress] > 0) require(loanTokenSent <= transactionLimit[loanTokenAddress]); - - /// @dev Compute the worth of the total deposit in loan tokens. - /// (loanTokenSent + convert(collateralTokenSent)) - /// No actual swap happening here. - uint256 totalDeposit = _totalDeposit(collateralTokenAddress, collateralTokenSent, loanTokenSent); - require(totalDeposit != 0, "12"); - - address[4] memory sentAddresses; - uint256[5] memory sentAmounts; - - sentAddresses[0] = address(this); /// The lender. - sentAddresses[1] = trader; - sentAddresses[2] = trader; - /// sentAddresses[3] = address(0); /// The manager. - - /// sentAmounts[0] = 0; /// interestRate (found later). - sentAmounts[1] = totalDeposit; /// Total amount of deposit. - /// sentAmounts[2] = 0; /// interestInitialAmount (interest is calculated based on fixed-term loan). - sentAmounts[3] = loanTokenSent; - sentAmounts[4] = collateralTokenSent; - - _settleInterest(); - - (sentAmounts[1], sentAmounts[0]) = _getMarginBorrowAmountAndRate( /// borrowAmount, interestRate - leverageAmount, - sentAmounts[1] /// depositAmount - ); - - require(_getAmountInRbtc(loanTokenAddress, sentAmounts[1]) > TINY_AMOUNT, "principal too small"); - - /// @dev Converting to initialMargin - leverageAmount = SafeMath.div(10**38, leverageAmount); - return - _borrowOrTrade( - loanId, - 0, /// withdrawAmount - leverageAmount, //initial margin - collateralTokenAddress, - sentAddresses, - sentAmounts, - loanDataBytes - ); - } - - /** - * @notice Wrapper for marginTrade invoking setAffiliatesReferrer to track - * referral trade by affiliates program. - * - * @param loanId The ID of the loan, 0 for a new loan. - * @param leverageAmount The multiple of exposure: 2x ... 5x. The leverage with 18 decimals. - * @param loanTokenSent The number of loan tokens provided by the user. - * @param collateralTokenSent The amount of collateral tokens provided by the user. - * @param collateralTokenAddress The token address of collateral. - * @param trader The account that performs this trade. - * @param minReturn Minimum position size in the collateral tokens - * @param affiliateReferrer The address of the referrer from affiliates program. - * @param loanDataBytes Additional loan data (not in use for token swaps). - * - * @return New principal and new collateral added to trade. - */ - function marginTradeAffiliate( - bytes32 loanId, // 0 if new loan - uint256 leverageAmount, // expected in x * 10**18 where x is the actual leverage (2, 3, 4, or 5) - uint256 loanTokenSent, - uint256 collateralTokenSent, - address collateralTokenAddress, - address trader, - uint256 minReturn, /// Minimum position size in the collateral tokens. - address affiliateReferrer, /// The user was brought by the affiliate (referrer). - bytes calldata loanDataBytes /// Arbitrary order data. - ) - external - payable - returns ( - uint256, - uint256 /// Returns new principal and new collateral added to trade. - ) - { - if (affiliateReferrer != address(0)) - ProtocolAffiliatesInterface(sovrynContractAddress).setAffiliatesReferrer(trader, affiliateReferrer); - return - marginTrade( - loanId, - leverageAmount, - loanTokenSent, - collateralTokenSent, - collateralTokenAddress, - trader, - minReturn, - loanDataBytes - ); - } - - /** - * @notice Withdraws RBTC from the contract by Multisig. - * @param _receiverAddress The address where the rBTC has to be transferred. - * @param _amount The amount of rBTC to be transferred. - */ - function withdrawRBTCTo(address payable _receiverAddress, uint256 _amount) external onlyOwner { - require(_receiverAddress != address(0), "receiver address invalid"); - require(_amount > 0, "non-zero withdraw amount expected"); - require(_amount <= address(this).balance, "withdraw amount cannot exceed balance"); - _receiverAddress.transfer(_amount); - emit WithdrawRBTCTo(_receiverAddress, _amount); - } - - /** - * @notice Transfer tokens wrapper. - * Sets token owner the msg.sender. - * Sets maximun allowance uint256(-1) to ensure tokens are always transferred. - * - * @param _to The recipient of the tokens. - * @param _value The amount of tokens sent. - * @return Success true/false. - * */ - function transfer(address _to, uint256 _value) external returns (bool) { - return _internalTransferFrom(msg.sender, _to, _value, uint256(-1)); - } - - /** - * @notice Moves `_value` loan tokens from `_from` to `_to` using the - * allowance mechanism. Calls internal _internalTransferFrom function. - * - * @return A boolean value indicating whether the operation succeeded. - */ - function transferFrom( - address _from, - address _to, - uint256 _value - ) external returns (bool) { - return - _internalTransferFrom( - _from, - _to, - _value, - //allowed[_from][msg.sender] - ProtocolLike(sovrynContractAddress).isLoanPool(msg.sender) ? uint256(-1) : allowed[_from][msg.sender] - ); - } - - /** - * @notice Transfer tokens, low level. - * Checks allowance, updates sender and recipient balances - * and updates checkpoints too. - * - * @param _from The tokens' owner. - * @param _to The recipient of the tokens. - * @param _value The amount of tokens sent. - * @param _allowanceAmount The amount of tokens allowed to transfer. - * - * @return Success true/false. - * */ - function _internalTransferFrom( - address _from, - address _to, - uint256 _value, - uint256 _allowanceAmount - ) internal returns (bool) { - if (_allowanceAmount != uint256(-1)) { - allowed[_from][msg.sender] = _allowanceAmount.sub(_value, "14"); - /// @dev Allowance mapping update requires an event log - emit AllowanceUpdate(_from, msg.sender, _allowanceAmount, allowed[_from][msg.sender]); - } - - require(_to != address(0), "15"); - - uint256 _balancesFrom = balances[_from]; - uint256 _balancesFromNew = _balancesFrom.sub(_value, "16"); - balances[_from] = _balancesFromNew; - - uint256 _balancesTo = balances[_to]; - uint256 _balancesToNew = _balancesTo.add(_value); - balances[_to] = _balancesToNew; - - /// @dev Handle checkpoint update. - uint256 _currentPrice = tokenPrice(); - - //checkpoints are not being used by the smart contract logic itself, but just for external use (query the profit) - //only update the checkpoints of a user if he's not depositing to / withdrawing from the lending pool - if (_from != liquidityMiningAddress && _to != liquidityMiningAddress) { - _updateCheckpoints(_from, _balancesFrom, _balancesFromNew, _currentPrice); - _updateCheckpoints(_to, _balancesTo, _balancesToNew, _currentPrice); - } - - emit Transfer(_from, _to, _value); - return true; - } - - /** - * @notice Update the user's checkpoint price and profit so far. - * In this loan token contract, whenever some tokens are minted or burned, - * the _updateCheckpoints() function is invoked to update the stats to - * reflect the balance changes. - * - * @param _user The user address. - * @param _oldBalance The user's previous balance. - * @param _newBalance The user's updated balance. - * @param _currentPrice The current loan token price. - * */ - function _updateCheckpoints( - address _user, - uint256 _oldBalance, - uint256 _newBalance, - uint256 _currentPrice - ) internal { - /// @dev keccak256("iToken_ProfitSoFar") - bytes32 slot = keccak256(abi.encodePacked(_user, iToken_ProfitSoFar)); - - int256 _currentProfit; - if (_newBalance == 0) { - _currentPrice = 0; - } else if (_oldBalance != 0) { - _currentProfit = _profitOf(slot, _oldBalance, _currentPrice, checkpointPrices_[_user]); - } - - assembly { - sstore(slot, _currentProfit) - } - - checkpointPrices_[_user] = _currentPrice; - } - - /* Public View functions */ - - /** - * @notice Wrapper for internal _profitOf low level function. - * @param user The user address. - * @return The profit of a user. - * */ - function profitOf(address user) external view returns (int256) { - /// @dev keccak256("iToken_ProfitSoFar") - bytes32 slot = keccak256(abi.encodePacked(user, iToken_ProfitSoFar)); - //TODO + LM balance - return _profitOf(slot, balances[user], tokenPrice(), checkpointPrices_[user]); - } - - /** - * @notice Profit calculation based on checkpoints of price. - * @param slot The user slot. - * @param _balance The user balance. - * @param _currentPrice The current price of the loan token. - * @param _checkpointPrice The price of the loan token on checkpoint. - * @return The profit of a user. - * */ - function _profitOf( - bytes32 slot, - uint256 _balance, - uint256 _currentPrice, - uint256 _checkpointPrice - ) internal view returns (int256 profitSoFar) { - if (_checkpointPrice == 0) { - return 0; - } - - assembly { - profitSoFar := sload(slot) - } - - profitSoFar = int256(_currentPrice).sub(int256(_checkpointPrice)).mul(int256(_balance)).div(sWEI_PRECISION).add(profitSoFar); - } - - /** - * @notice Loan token price calculation considering unpaid interests. - * @return The loan token price. - * */ - function tokenPrice() public view returns (uint256 price) { - uint256 interestUnPaid; - if (lastSettleTime_ != uint88(block.timestamp)) { - (, interestUnPaid) = _getAllInterest(); - } - - return _tokenPrice(_totalAssetSupply(interestUnPaid)); - } - - /** - * @notice Getter for the price checkpoint mapping. - * @param _user The user account as the mapping index. - * @return The price on the checkpoint for this user. - * */ - function checkpointPrice(address _user) public view returns (uint256 price) { - return checkpointPrices_[_user]; - } - - /** - * @notice Get current liquidity. - * A part of total funds supplied are borrowed. Liquidity = supply - borrow - * @return The market liquidity. - * */ - function marketLiquidity() public view returns (uint256) { - uint256 totalSupply = _totalAssetSupply(0); - uint256 totalBorrow = totalAssetBorrow(); - if (totalSupply > totalBorrow) { - return totalSupply - totalBorrow; - } - } - - /** - * @notice Wrapper for average borrow interest. - * @return The average borrow interest. - * */ - function avgBorrowInterestRate() public view returns (uint256) { - return _avgBorrowInterestRate(totalAssetBorrow()); - } - - /** - * @notice Get borrow interest rate. - * The minimum rate the next base protocol borrower will receive - * for variable-rate loans. - * @return The borrow interest rate. - * */ - function borrowInterestRate() public view returns (uint256) { - return _nextBorrowInterestRate(0); - } - - /** - * @notice Public wrapper for internal call. - * @param borrowAmount The amount of tokens to borrow. - * @return The next borrow interest rate. - * */ - function nextBorrowInterestRate(uint256 borrowAmount) public view returns (uint256) { - return _nextBorrowInterestRate(borrowAmount); - } - - /** - * @notice Get interest rate. - * - * @return Interest that lenders are currently receiving when supplying to - * the pool. - * */ - function supplyInterestRate() public view returns (uint256) { - return totalSupplyInterestRate(_totalAssetSupply(0)); - } - - /** - * @notice Get interest rate w/ added supply. - * @param supplyAmount The amount of tokens supplied. - * @return Interest that lenders are currently receiving when supplying - * a given amount of tokens to the pool. - * */ - function nextSupplyInterestRate(uint256 supplyAmount) public view returns (uint256) { - return totalSupplyInterestRate(_totalAssetSupply(0).add(supplyAmount)); - } - - /** - * @notice Get interest rate w/ added supply assets. - * @param assetSupply The amount of loan tokens supplied. - * @return Interest that lenders are currently receiving when supplying - * a given amount of loan tokens to the pool. - * */ - function totalSupplyInterestRate(uint256 assetSupply) public view returns (uint256) { - uint256 assetBorrow = totalAssetBorrow(); - if (assetBorrow != 0) { - return calculateSupplyInterestRate(assetBorrow, assetSupply); - } - } - - /** - * @notice Get the total amount of loan tokens on debt. - * Calls protocol getTotalPrincipal function. - * In the context of borrowing, principal is the initial size of a loan. - * It can also be the amount still owed on a loan. If you take out a - * $50,000 mortgage, for example, the principal is $50,000. If you pay off - * $30,000, the principal balance now consists of the remaining $20,000. - * - * @return The total amount of loan tokens on debt. - * */ - function totalAssetBorrow() public view returns (uint256) { - return ProtocolLike(sovrynContractAddress).getTotalPrincipal(address(this), loanTokenAddress); - } - - /** - * @notice Get the total amount of loan tokens on supply. - * @dev Wrapper for internal _totalAssetSupply function. - * @return The total amount of loan tokens on supply. - * */ - function totalAssetSupply() public view returns (uint256) { - uint256 interestUnPaid; - if (lastSettleTime_ != uint88(block.timestamp)) { - (, interestUnPaid) = _getAllInterest(); - } - - return _totalAssetSupply(interestUnPaid); - } - - /** - * @notice Compute the maximum deposit amount under current market conditions. - * @dev maxEscrowAmount = liquidity * (100 - interestForDuration) / 100 - * @param leverageAmount The chosen multiplier with 18 decimals. - * */ - function getMaxEscrowAmount(uint256 leverageAmount) public view returns (uint256 maxEscrowAmount) { - /** - * @dev Mathematical imperfection: depending on liquidity we might be able - * to borrow more if utilization is below the kink level. - * */ - uint256 interestForDuration = maxScaleRate.mul(28).div(365); - uint256 factor = uint256(10**20).sub(interestForDuration); - uint256 maxLoanSize = marketLiquidity().mul(factor).div(10**20); - maxEscrowAmount = maxLoanSize.mul(10**18).div(leverageAmount); - } - - /** - * @notice Get loan token balance. - * @return The user's balance of underlying token. - * */ - function assetBalanceOf(address _owner) public view returns (uint256) { - uint256 balanceOnLM = 0; - if (liquidityMiningAddress != address(0)) { - balanceOnLM = ILiquidityMining(liquidityMiningAddress).getUserPoolTokenBalance(address(this), _owner); - } - return balanceOf(_owner).add(balanceOnLM).mul(tokenPrice()).div(10**18); - } - - /** - * @notice Get margin information on a trade. - * - * @param leverageAmount The multiple of exposure: 2x ... 5x. The leverage with 18 decimals. - * @param loanTokenSent The number of loan tokens provided by the user. - * @param collateralTokenSent The amount of collateral tokens provided by the user. - * @param collateralTokenAddress The token address of collateral. - * - * @return The principal, the collateral and the interestRate. - * */ - function getEstimatedMarginDetails( - uint256 leverageAmount, - uint256 loanTokenSent, - uint256 collateralTokenSent, - address collateralTokenAddress // address(0) means ETH - ) - public - view - returns ( - uint256 principal, - uint256 collateral, - uint256 interestRate - ) - { - if (collateralTokenAddress == address(0)) { - collateralTokenAddress = wrbtcTokenAddress; - } - - uint256 totalDeposit = _totalDeposit(collateralTokenAddress, collateralTokenSent, loanTokenSent); - - (principal, interestRate) = _getMarginBorrowAmountAndRate(leverageAmount, totalDeposit); - if (principal > _underlyingBalance()) { - return (0, 0, 0); - } - - loanTokenSent = loanTokenSent.add(principal); - - collateral = ProtocolLike(sovrynContractAddress).getEstimatedMarginExposure( - loanTokenAddress, - collateralTokenAddress, - loanTokenSent, - collateralTokenSent, - interestRate, - principal - ); - } - - /** - * @notice Calculate the deposit required to a given borrow. - * - * The function for doing over-collateralized borrows against loan tokens - * expects a minimum amount of collateral be sent to satisfy collateral - * requirements of the loan, for borrow amount, interest rate, and - * initial loan duration. To determine appropriate values to pass to this - * function for a given loan, `getDepositAmountForBorrow` and - * 'getBorrowAmountForDeposit` are required. - * - * @param borrowAmount The amount of borrow. - * @param initialLoanDuration The duration of the loan. - * @param collateralTokenAddress The token address of collateral. - * - * @return The amount of deposit required. - * */ - function getDepositAmountForBorrow( - uint256 borrowAmount, - uint256 initialLoanDuration, /// Duration in seconds. - address collateralTokenAddress /// address(0) means rBTC - ) public view returns (uint256 depositAmount) { - if (borrowAmount != 0) { - (, , uint256 newBorrowAmount) = _getInterestRateAndBorrowAmount(borrowAmount, totalAssetSupply(), initialLoanDuration); - - if (newBorrowAmount <= _underlyingBalance()) { - if (collateralTokenAddress == address(0)) collateralTokenAddress = wrbtcTokenAddress; - bytes32 loanParamsId = loanParamsIds[uint256(keccak256(abi.encodePacked(collateralTokenAddress, true)))]; - return - ProtocolLike(sovrynContractAddress) - .getRequiredCollateral( - loanTokenAddress, - collateralTokenAddress, - newBorrowAmount, - ProtocolSettingsLike(sovrynContractAddress).minInitialMargin(loanParamsId), /// initialMargin - true /// isTorqueLoan - ) - .add(10); /// Some dust to compensate for rounding errors. - } - } - } - - /** - * @notice Calculate the borrow allowed for a given deposit. - * - * The function for doing over-collateralized borrows against loan tokens - * expects a minimum amount of collateral be sent to satisfy collateral - * requirements of the loan, for borrow amount, interest rate, and - * initial loan duration. To determine appropriate values to pass to this - * function for a given loan, `getDepositAmountForBorrow` and - * 'getBorrowAmountForDeposit` are required. - * - * @param depositAmount The amount of deposit. - * @param initialLoanDuration The duration of the loan. - * @param collateralTokenAddress The token address of collateral. - * - * @return The amount of borrow allowed. - * */ - function getBorrowAmountForDeposit( - uint256 depositAmount, - uint256 initialLoanDuration, /// Duration in seconds. - address collateralTokenAddress /// address(0) means rBTC - ) public view returns (uint256 borrowAmount) { - if (depositAmount != 0) { - if (collateralTokenAddress == address(0)) collateralTokenAddress = wrbtcTokenAddress; - bytes32 loanParamsId = loanParamsIds[uint256(keccak256(abi.encodePacked(collateralTokenAddress, true)))]; - borrowAmount = ProtocolLike(sovrynContractAddress).getBorrowAmount( - loanTokenAddress, - collateralTokenAddress, - depositAmount, - ProtocolSettingsLike(sovrynContractAddress).minInitialMargin(loanParamsId), /// initialMargin, - true /// isTorqueLoan - ); - - (, , borrowAmount) = _getInterestRateAndBorrowAmount(borrowAmount, totalAssetSupply(), initialLoanDuration); - - if (borrowAmount > _underlyingBalance()) { - borrowAmount = 0; - } - } - } - - function checkPriceDivergence( - uint256 leverageAmount, - uint256 loanTokenSent, - uint256 collateralTokenSent, - address collateralTokenAddress, - uint256 minReturn - ) public view { - (, uint256 estimatedCollateral, ) = - getEstimatedMarginDetails(leverageAmount, loanTokenSent, collateralTokenSent, collateralTokenAddress); - require(estimatedCollateral >= minReturn, "coll too low"); - } - - /* Internal functions */ - - /** - * @notice transfers the underlying asset from the msg.sender and mints tokens for the receiver - * @param receiver the address of the iToken receiver - * @param depositAmount the amount of underlying assets to be deposited - * @return the amount of iTokens issued - */ - function _mintToken(address receiver, uint256 depositAmount) internal returns (uint256 mintAmount) { - uint256 currentPrice; - - //calculate amount to mint and transfer the underlying asset - (mintAmount, currentPrice) = _prepareMinting(depositAmount); - - //compute balances needed for checkpoint update, considering that the user might have a pool token balance - //on the liquidity mining contract - uint256 balanceOnLM = 0; - if (liquidityMiningAddress != address(0)) - balanceOnLM = ILiquidityMining(liquidityMiningAddress).getUserPoolTokenBalance(address(this), receiver); - uint256 oldBalance = balances[receiver].add(balanceOnLM); - uint256 newBalance = oldBalance.add(mintAmount); - - //mint the tokens to the receiver - _mint(receiver, mintAmount, depositAmount, currentPrice); - - //update the checkpoint of the receiver - _updateCheckpoints(receiver, oldBalance, newBalance, currentPrice); - } - - /** - * calculates the amount of tokens to mint and transfers the underlying asset to this contract - * @param depositAmount the amount of the underyling asset deposited - * @return the amount to be minted - */ - function _prepareMinting(uint256 depositAmount) internal returns (uint256 mintAmount, uint256 currentPrice) { - require(depositAmount != 0, "17"); - - _settleInterest(); - - currentPrice = _tokenPrice(_totalAssetSupply(0)); - mintAmount = depositAmount.mul(10**18).div(currentPrice); - - if (msg.value == 0) { - _safeTransferFrom(loanTokenAddress, msg.sender, address(this), depositAmount, "18"); - } else { - IWrbtc(wrbtcTokenAddress).deposit.value(depositAmount)(); - } - } - - /** - * @notice A wrapper for AdvancedToken::_burn - * - * @param burnAmount The amount of loan tokens to redeem. - * - * @return The amount of underlying tokens payed to lender. - * */ - function _burnToken(uint256 burnAmount) internal returns (uint256 loanAmountPaid) { - require(burnAmount != 0, "19"); - - if (burnAmount > balanceOf(msg.sender)) { - require(burnAmount == uint256(-1), "32"); - burnAmount = balanceOf(msg.sender); - } - - _settleInterest(); - - uint256 currentPrice = _tokenPrice(_totalAssetSupply(0)); - - uint256 loanAmountOwed = burnAmount.mul(currentPrice).div(10**18); - uint256 loanAmountAvailableInContract = _underlyingBalance(); - - loanAmountPaid = loanAmountOwed; - require(loanAmountPaid <= loanAmountAvailableInContract, "37"); - - //compute balances needed for checkpoint update, considering that the user might have a pool token balance - //on the liquidity mining contract - uint256 balanceOnLM = 0; - if (liquidityMiningAddress != address(0)) - balanceOnLM = ILiquidityMining(liquidityMiningAddress).getUserPoolTokenBalance(address(this), msg.sender); - uint256 oldBalance = balances[msg.sender].add(balanceOnLM); - uint256 newBalance = oldBalance.sub(burnAmount); - - _burn(msg.sender, burnAmount, loanAmountPaid, currentPrice); - - //this function does not only update the checkpoints but also the current profit of the user - //all for external use only - _updateCheckpoints(msg.sender, oldBalance, newBalance, currentPrice); - } - - /** - * @notice Withdraw loan token interests from protocol. - * This function only operates once per block. - * It asks protocol to withdraw accrued interests for the loan token. - * - * @dev Internal sync required on every loan trade before starting. - * */ - function _settleInterest() internal { - uint88 ts = uint88(block.timestamp); - if (lastSettleTime_ != ts) { - ProtocolLike(sovrynContractAddress).withdrawAccruedInterest(loanTokenAddress); - - lastSettleTime_ = ts; - } - } - - /** - * @notice Compute what the deposit is worth in loan tokens using the swap rate - * used for loan size computation. - * - * @param collateralTokenAddress The token address of the collateral. - * @param collateralTokenSent The amount of collateral tokens provided by the user. - * @param loanTokenSent The number of loan tokens provided by the user. - * - * @return The value of the deposit in loan tokens. - * */ - function _totalDeposit( - address collateralTokenAddress, - uint256 collateralTokenSent, - uint256 loanTokenSent - ) internal view returns (uint256 totalDeposit) { - totalDeposit = loanTokenSent; - - if (collateralTokenSent != 0) { - /// @dev Get the oracle rate from collateral -> loan - (uint256 collateralToLoanRate, uint256 collateralToLoanPrecision) = - FeedsLike(ProtocolLike(sovrynContractAddress).priceFeeds()).queryRate(collateralTokenAddress, loanTokenAddress); - require((collateralToLoanRate != 0) && (collateralToLoanPrecision != 0), "invalid rate collateral token"); - - /// @dev Compute the loan token amount with the oracle rate. - uint256 loanTokenAmount = collateralTokenSent.mul(collateralToLoanRate).div(collateralToLoanPrecision); - - /// @dev See how many collateralTokens we would get if exchanging this amount of loan tokens to collateral tokens. - uint256 collateralTokenAmount = - ProtocolLike(sovrynContractAddress).getSwapExpectedReturn(loanTokenAddress, collateralTokenAddress, loanTokenAmount); - - /// @dev Probably not the same due to the price difference. - if (collateralTokenAmount != collateralTokenSent) { - //scale the loan token amount accordingly, so we'll get the expected position size in the end - loanTokenAmount = loanTokenAmount.mul(collateralTokenAmount).div(collateralTokenSent); - } - - totalDeposit = loanTokenAmount.add(totalDeposit); - } - } - - /** - * @dev returns amount of the asset converted to RBTC - * @param asset the asset to be transferred - * @param amount the amount to be transferred - * @return amount in RBTC - * */ - function _getAmountInRbtc(address asset, uint256 amount) internal returns (uint256) { - (uint256 rbtcRate, uint256 rbtcPrecision) = - FeedsLike(ProtocolLike(sovrynContractAddress).priceFeeds()).queryRate(asset, wrbtcTokenAddress); - return amount.mul(rbtcRate).div(rbtcPrecision); - } - - /* - * @notice Compute interest rate and other loan parameters. - * - * @param borrowAmount The amount of tokens to borrow. - * @param assetSupply The amount of loan tokens supplied. - * @param initialLoanDuration The duration of the loan in seconds. - * If the loan is not paid back until then, it'll need to be rolled over. - * - * @return The interest rate, the interest calculated based on fixed-term - * loan, and the new borrow amount. - * */ - function _getInterestRateAndBorrowAmount( - uint256 borrowAmount, - uint256 assetSupply, - uint256 initialLoanDuration /// Duration in seconds. - ) - internal - view - returns ( - uint256 interestRate, - uint256 interestInitialAmount, - uint256 newBorrowAmount - ) - { - interestRate = _nextBorrowInterestRate2(borrowAmount, assetSupply); - - /// newBorrowAmount = borrowAmount * 10^18 / (10^18 - interestRate * 7884000 * 10^18 / 31536000 / 10^20) - newBorrowAmount = borrowAmount.mul(10**18).div( - SafeMath.sub( - 10**18, - interestRate.mul(initialLoanDuration).mul(10**18).div(31536000 * 10**20) /// 365 * 86400 * 10**20 - ) - ); - - interestInitialAmount = newBorrowAmount.sub(borrowAmount); - } - - /** - * @notice Compute principal and collateral. - * - * @param loanId The ID of the loan, 0 for a new loan. - * @param withdrawAmount The amount to be withdrawn (actually borrowed). - * @param initialMargin The initial margin with 18 decimals - * @param collateralTokenAddress The address of the token to be used as - * collateral. Cannot be the loan token address. - * @param sentAddresses The addresses to send tokens: lender, borrower, - * receiver and manager. - * @param sentAmounts The amounts to send to each address. - * @param loanDataBytes Additional loan data (not in use for token swaps). - * - * @return The new principal and the new collateral. Principal is the - * complete borrowed amount (in loan tokens). Collateral is the complete - * position size (loan + margin) (in collateral tokens). - * */ - function _borrowOrTrade( - bytes32 loanId, - uint256 withdrawAmount, - uint256 initialMargin, - address collateralTokenAddress, - address[4] memory sentAddresses, - uint256[5] memory sentAmounts, - bytes memory loanDataBytes - ) internal returns (uint256, uint256) { - _checkPause(); - require( - sentAmounts[1] <= _underlyingBalance() && /// newPrincipal (borrowed amount + fees) - sentAddresses[1] != address(0), /// The borrower. - "24" - ); - - if (sentAddresses[2] == address(0)) { - sentAddresses[2] = sentAddresses[1]; /// The receiver = the borrower. - } - - /// @dev Handle transfers prior to adding newPrincipal to loanTokenSent - uint256 msgValue = _verifyTransfers(collateralTokenAddress, sentAddresses, sentAmounts, withdrawAmount); - - /** - * @dev Adding the loan token portion from the lender to loanTokenSent - * (add the loan to the loan tokens sent from the user). - * */ - sentAmounts[3] = sentAmounts[3].add(sentAmounts[1]); /// newPrincipal - - if (withdrawAmount != 0) { - /// @dev withdrawAmount already sent to the borrower, so we aren't sending it to the protocol. - sentAmounts[3] = sentAmounts[3].sub(withdrawAmount); - } - - bool withdrawAmountExist = false; /// Default is false, but added just as to make sure. - - if (withdrawAmount != 0) { - withdrawAmountExist = true; - } - - bytes32 loanParamsId = loanParamsIds[uint256(keccak256(abi.encodePacked(collateralTokenAddress, withdrawAmountExist)))]; - - (sentAmounts[1], sentAmounts[4]) = ProtocolLike(sovrynContractAddress).borrowOrTradeFromPool.value(msgValue)( /// newPrincipal, newCollateral - loanParamsId, - loanId, - withdrawAmountExist, - initialMargin, - sentAddresses, - sentAmounts, - loanDataBytes - ); - require(sentAmounts[1] != 0, "25"); - - /// @dev Setting not-first-trade flag to prevent binding to an affiliate existing users post factum. - /// @dev REFACTOR: move to a general interface: ProtocolSettingsLike? - ProtocolAffiliatesInterface(sovrynContractAddress).setUserNotFirstTradeFlag(sentAddresses[1]); - - return (sentAmounts[1], sentAmounts[4]); // newPrincipal, newCollateral - } - - /// sentAddresses[0]: lender - /// sentAddresses[1]: borrower - /// sentAddresses[2]: receiver - /// sentAddresses[3]: manager - /// sentAmounts[0]: interestRate - /// sentAmounts[1]: newPrincipal - /// sentAmounts[2]: interestInitialAmount - /// sentAmounts[3]: loanTokenSent - /// sentAmounts[4]: collateralTokenSent - /** - * @notice . - * - * @param collateralTokenAddress The address of the token to be used as - * collateral. Cannot be the loan token address. - * @param sentAddresses The addresses to send tokens: lender, borrower, - * receiver and manager. - * @param sentAmounts The amounts to send to each address. - * @param withdrawalAmount The amount of tokens to withdraw. - * - * @return msgValue The amount of rBTC sent minus the collateral on tokens. - * */ - function _verifyTransfers( - address collateralTokenAddress, - address[4] memory sentAddresses, - uint256[5] memory sentAmounts, - uint256 withdrawalAmount - ) internal returns (uint256 msgValue) { - address _wrbtcToken = wrbtcTokenAddress; - address _loanTokenAddress = loanTokenAddress; - address receiver = sentAddresses[2]; - uint256 newPrincipal = sentAmounts[1]; - uint256 loanTokenSent = sentAmounts[3]; - uint256 collateralTokenSent = sentAmounts[4]; - - require(_loanTokenAddress != collateralTokenAddress, "26"); - - msgValue = msg.value; - - if (withdrawalAmount != 0) { - /// withdrawOnOpen == true - _safeTransfer(_loanTokenAddress, receiver, withdrawalAmount, ""); - if (newPrincipal > withdrawalAmount) { - _safeTransfer(_loanTokenAddress, sovrynContractAddress, newPrincipal - withdrawalAmount, ""); - } - } else { - _safeTransfer(_loanTokenAddress, sovrynContractAddress, newPrincipal, "27"); - } - /** - * This is a critical piece of code! - * rBTC are supposed to be held by the contract itself, while other tokens are being transfered from the sender directly. - * */ - if (collateralTokenSent != 0) { - if (collateralTokenAddress == _wrbtcToken && msgValue != 0 && msgValue >= collateralTokenSent) { - IWrbtc(_wrbtcToken).deposit.value(collateralTokenSent)(); - _safeTransfer(collateralTokenAddress, sovrynContractAddress, collateralTokenSent, "28-a"); - msgValue -= collateralTokenSent; - } else { - _safeTransferFrom(collateralTokenAddress, msg.sender, sovrynContractAddress, collateralTokenSent, "28-b"); - } - } - - if (loanTokenSent != 0) { - _safeTransferFrom(_loanTokenAddress, msg.sender, sovrynContractAddress, loanTokenSent, "29"); - } - } - - /** - * @notice Execute the ERC20 token's `transfer` function and reverts - * upon failure the main purpose of this function is to prevent a non - * standard ERC20 token from failing silently. - * - * @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. - * - * @param token The ERC20 token address. - * @param to The target address. - * @param amount The transfer amount. - * @param errorMsg The error message on failure. - */ - function _safeTransfer( - address token, - address to, - uint256 amount, - string memory errorMsg - ) internal { - _callOptionalReturn(token, abi.encodeWithSelector(IERC20(token).transfer.selector, to, amount), errorMsg); - } - - /** - * @notice Execute the ERC20 token's `transferFrom` function and reverts - * upon failure the main purpose of this function is to prevent a non - * standard ERC20 token from failing silently. - * - * @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. - * - * @param token The ERC20 token address. - * @param from The source address. - * @param to The target address. - * @param amount The transfer amount. - * @param errorMsg The error message on failure. - */ - function _safeTransferFrom( - address token, - address from, - address to, - uint256 amount, - string memory errorMsg - ) internal { - _callOptionalReturn(token, abi.encodeWithSelector(IERC20(token).transferFrom.selector, from, to, amount), errorMsg); - } - - /** - * @notice Imitate 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). - * @param errorMsg The error message on failure. - * */ - function _callOptionalReturn( - address token, - bytes memory data, - string memory errorMsg - ) internal { - require(Address.isContract(token), "call to a non-contract address"); - (bool success, bytes memory returndata) = token.call(data); - require(success, errorMsg); - - if (returndata.length != 0) { - require(abi.decode(returndata, (bool)), errorMsg); - } - } - - /** - * @notice Get the loan contract balance. - * @return The balance of the loan token for this contract. - * */ - function _underlyingBalance() internal view returns (uint256) { - return IERC20(loanTokenAddress).balanceOf(address(this)); - } - - /* Internal View functions */ - - /** - * @notice Compute the token price. - * @param assetSupply The amount of loan tokens supplied. - * @return The token price. - * */ - function _tokenPrice(uint256 assetSupply) internal view returns (uint256) { - uint256 totalTokenSupply = totalSupply_; - - return totalTokenSupply != 0 ? assetSupply.mul(10**18).div(totalTokenSupply) : initialPrice; - } - - /** - * @notice Compute the average borrow interest rate. - * @param assetBorrow The amount of loan tokens on debt. - * @return The average borrow interest rate. - * */ - function _avgBorrowInterestRate(uint256 assetBorrow) internal view returns (uint256) { - if (assetBorrow != 0) { - (uint256 interestOwedPerDay, ) = _getAllInterest(); - return interestOwedPerDay.mul(10**20).mul(365).div(assetBorrow); - } - } - - /** - * @notice Compute the next supply interest adjustment. - * @param assetBorrow The amount of loan tokens on debt. - * @param assetSupply The amount of loan tokens supplied. - * @return The next supply interest adjustment. - * */ - function calculateSupplyInterestRate(uint256 assetBorrow, uint256 assetSupply) public view returns (uint256) { - if (assetBorrow != 0 && assetSupply >= assetBorrow) { - return - _avgBorrowInterestRate(assetBorrow) - .mul(_utilizationRate(assetBorrow, assetSupply)) - .mul(SafeMath.sub(10**20, ProtocolLike(sovrynContractAddress).lendingFeePercent())) - .div(10**40); - } - } - - /** - * @notice Compute the next borrow interest adjustment. - * @param borrowAmount The amount of tokens to borrow. - * @return The next borrow interest adjustment. - * */ - function _nextBorrowInterestRate(uint256 borrowAmount) internal view returns (uint256) { - uint256 interestUnPaid; - if (borrowAmount != 0) { - if (lastSettleTime_ != uint88(block.timestamp)) { - (, interestUnPaid) = _getAllInterest(); - } - - uint256 balance = _underlyingBalance().add(interestUnPaid); - if (borrowAmount > balance) { - borrowAmount = balance; - } - } - - return _nextBorrowInterestRate2(borrowAmount, _totalAssetSupply(interestUnPaid)); - } - - /** - * @notice Compute the next borrow interest adjustment under target-kink - * level analysis. - * - * The "kink" in the cDAI interest rate model reflects the utilization rate - * at which the slope of the interest rate goes from "gradual" to "steep". - * That is, below this utilization rate, the slope of the interest rate - * curve is gradual. Above this utilization rate, it is steep. - * - * Because of this dynamic between the interest rate curves before and - * after the "kink", the "kink" can be thought of as the target utilization - * rate. Above that rate, it quickly becomes expensive to borrow (and - * commensurately lucrative for suppliers). - * - * @param newBorrowAmount The new amount of tokens to borrow. - * @param assetSupply The amount of loan tokens supplied. - * @return The next borrow interest adjustment. - * */ - function _nextBorrowInterestRate2(uint256 newBorrowAmount, uint256 assetSupply) internal view returns (uint256 nextRate) { - uint256 utilRate = _utilizationRate(totalAssetBorrow().add(newBorrowAmount), assetSupply); - - uint256 thisMinRate; - uint256 thisRateAtKink; - uint256 thisBaseRate = baseRate; - uint256 thisRateMultiplier = rateMultiplier; - uint256 thisTargetLevel = targetLevel; - uint256 thisKinkLevel = kinkLevel; - uint256 thisMaxScaleRate = maxScaleRate; - - if (utilRate < thisTargetLevel) { - // target targetLevel utilization when utilization is under targetLevel - utilRate = thisTargetLevel; - } - - if (utilRate > thisKinkLevel) { - /// @dev Scale rate proportionally up to 100% - uint256 thisMaxRange = WEI_PERCENT_PRECISION - thisKinkLevel; /// Will not overflow. - - utilRate -= thisKinkLevel; - if (utilRate > thisMaxRange) utilRate = thisMaxRange; - - // Modified the rate calculation as it is slightly exaggerated around kink level - // thisRateAtKink = thisRateMultiplier.add(thisBaseRate).mul(thisKinkLevel).div(WEI_PERCENT_PRECISION); - thisRateAtKink = thisKinkLevel.mul(thisRateMultiplier).div(WEI_PERCENT_PRECISION).add(thisBaseRate); - - nextRate = utilRate.mul(SafeMath.sub(thisMaxScaleRate, thisRateAtKink)).div(thisMaxRange).add(thisRateAtKink); - } else { - nextRate = utilRate.mul(thisRateMultiplier).div(WEI_PERCENT_PRECISION).add(thisBaseRate); - - thisMinRate = thisBaseRate; - thisRateAtKink = thisRateMultiplier.add(thisBaseRate); - - if (nextRate < thisMinRate) nextRate = thisMinRate; - else if (nextRate > thisRateAtKink) nextRate = thisRateAtKink; - } - } - - /** - * @notice Get two kind of interests: owed per day and yet to be paid. - * @return interestOwedPerDay The interest per day. - * @return interestUnPaid The interest not yet paid. - * */ - function _getAllInterest() internal view returns (uint256 interestOwedPerDay, uint256 interestUnPaid) { - /// interestPaid, interestPaidDate, interestOwedPerDay, interestUnPaid, interestFeePercent, principalTotal - uint256 interestFeePercent; - (, , interestOwedPerDay, interestUnPaid, interestFeePercent, ) = ProtocolLike(sovrynContractAddress).getLenderInterestData( - address(this), - loanTokenAddress - ); - - interestUnPaid = interestUnPaid.mul(SafeMath.sub(10**20, interestFeePercent)).div(10**20); - } - - /** - * @notice Compute the loan size and interest rate. - * @param leverageAmount The leverage with 18 decimals. - * @param depositAmount The amount the user deposited in underlying loan tokens. - * @return borrowAmount The amount of tokens to borrow. - * @return interestRate The interest rate to pay on the position. - * */ - function _getMarginBorrowAmountAndRate(uint256 leverageAmount, uint256 depositAmount) - internal - view - returns (uint256 borrowAmount, uint256 interestRate) - { - uint256 loanSizeBeforeInterest = depositAmount.mul(leverageAmount).div(10**18); - /** - * @dev Mathematical imperfection. we calculate the interest rate based on - * the loanSizeBeforeInterest, but the actual borrowed amount will be bigger. - * */ - interestRate = _nextBorrowInterestRate2(loanSizeBeforeInterest, _totalAssetSupply(0)); - /// @dev Assumes that loan, collateral, and interest token are the same. - borrowAmount = _adjustLoanSize(interestRate, 28 days, loanSizeBeforeInterest); - } - - /** - * @notice Compute the total amount of loan tokens on supply. - * @param interestUnPaid The interest not yet paid. - * @return assetSupply The total amount of loan tokens on supply. - * */ - function _totalAssetSupply(uint256 interestUnPaid) internal view returns (uint256 assetSupply) { - if (totalSupply_ != 0) { - uint256 assetsBalance = _flTotalAssetSupply; /// Temporary locked totalAssetSupply during a flash loan transaction. - if (assetsBalance == 0) { - assetsBalance = _underlyingBalance().add(totalAssetBorrow()); - } - - return assetsBalance.add(interestUnPaid); - } - } - - /** - * @notice Check whether a function is paused. - * - * @dev Used to read externally from the smart contract to see if a - * function is paused. - * - * @param funcId The function ID, the selector. - * - * @return isPaused Whether the function is paused: true or false. - * */ - function checkPause(string memory funcId) public view returns (bool isPaused) { - bytes4 sig = bytes4(keccak256(abi.encodePacked(funcId))); - bytes32 slot = keccak256(abi.encodePacked(sig, uint256(0xd46a704bc285dbd6ff5ad3863506260b1df02812f4f857c8cc852317a6ac64f2))); - assembly { - isPaused := sload(slot) - } - return isPaused; - } - - /** - * @notice Make sure call is not paused. - * @dev Used for internal verification if the called function is paused. - * It throws an exception in case it's not. - * */ - function _checkPause() internal view { - /// keccak256("iToken_FunctionPause") - bytes32 slot = keccak256(abi.encodePacked(msg.sig, uint256(0xd46a704bc285dbd6ff5ad3863506260b1df02812f4f857c8cc852317a6ac64f2))); - bool isPaused; - assembly { - isPaused := sload(slot) - } - require(!isPaused, "unauthorized"); - } - - /** - * @notice Adjusts the loan size to make sure the expected exposure remains after prepaying the interest. - * @dev loanSizeWithInterest = loanSizeBeforeInterest * 100 / (100 - interestForDuration) - * @param interestRate The interest rate to pay on the position. - * @param maxDuration The maximum duration of the position (until rollover). - * @param loanSizeBeforeInterest The loan size before interest is added. - * */ - function _adjustLoanSize( - uint256 interestRate, - uint256 maxDuration, - uint256 loanSizeBeforeInterest - ) internal pure returns (uint256 loanSizeWithInterest) { - uint256 interestForDuration = interestRate.mul(maxDuration).div(365 days); - uint256 divisor = uint256(10**20).sub(interestForDuration); - loanSizeWithInterest = loanSizeBeforeInterest.mul(10**20).div(divisor); - } - - /** - * @notice Calculate the utilization rate. - * @dev Utilization rate = assetBorrow / assetSupply - * @param assetBorrow The amount of loan tokens on debt. - * @param assetSupply The amount of loan tokens supplied. - * @return The utilization rate. - * */ - function _utilizationRate(uint256 assetBorrow, uint256 assetSupply) internal pure returns (uint256) { - if (assetBorrow != 0 && assetSupply != 0) { - /// U = total_borrow / total_supply - return assetBorrow.mul(10**20).div(assetSupply); - } - } - - /** - * @notice sets the liquidity mining contract address - * @param LMAddress the address of the liquidity mining contract - */ - function setLiquidityMiningAddress(address LMAddress) external onlyOwner { - liquidityMiningAddress = LMAddress; - } + /** + * @notice Borrow funds from the pool. + * The underlying loan token may not be used as collateral. + * + * @param loanId The ID of the loan, 0 for a new loan. + * @param withdrawAmount The amount to be withdrawn (actually borrowed). + * @param initialLoanDuration The duration of the loan in seconds. + * If the loan is not paid back until then, it'll need to be rolled over. + * @param collateralTokenSent The amount of collateral tokens provided by the user. + * (150% of the withdrawn amount worth in collateral tokens). + * @param collateralTokenAddress The address of the token to be used as + * collateral. Cannot be the loan token address. + * @param borrower The one paying for the collateral. + * @param receiver The one receiving the withdrawn amount. + * + * @return New principal and new collateral added to loan. + * */ + function borrow( + bytes32 loanId, /// 0 if new loan. + uint256 withdrawAmount, + uint256 initialLoanDuration, /// Duration in seconds. + uint256 collateralTokenSent, /// If 0, loanId must be provided; any rBTC sent must equal this value. + address collateralTokenAddress, /// If address(0), this means rBTC and rBTC must be sent with the call or loanId must be provided. + address borrower, + address receiver, + bytes memory /// loanDataBytes: arbitrary order data (for future use). + ) + public + payable + nonReentrant /// Note: needs to be removed to allow flashloan use cases. + returns ( + uint256, + uint256 /// Returns new principal and new collateral added to loan. + ) + { + require(withdrawAmount != 0, "6"); + + _checkPause(); + + /// Temporary: limit transaction size. + if (transactionLimit[collateralTokenAddress] > 0) + require(collateralTokenSent <= transactionLimit[collateralTokenAddress]); + + require( + (msg.value == 0 || msg.value == collateralTokenSent) && + (collateralTokenSent != 0 || loanId != 0) && + (collateralTokenAddress != address(0) || msg.value != 0 || loanId != 0) && + (loanId == 0 || msg.sender == borrower), + "7" + ); + + /// @dev We have an issue regarding contract size code is too big. 1 of the solution is need to keep the error message 32 bytes length + // Temporarily, we combine this require to the above, so can save the contract size code + // require(collateralTokenSent != 0 || loanId != 0, "8"); + // require(collateralTokenAddress != address(0) || msg.value != 0 || loanId != 0, "9"); + + /// @dev Ensure authorized use of existing loan. + // require(loanId == 0 || msg.sender == borrower, "401 use of existing loan"); + + /// @dev The condition is never met. + /// Address zero is not allowed by previous require validation. + /// This check is unneeded and was lowering the test coverage index. + // if (collateralTokenAddress == address(0)) { + // collateralTokenAddress = wrbtcTokenAddress; + // } + + require(collateralTokenAddress != loanTokenAddress, "10"); + + _settleInterest(); + + address[4] memory sentAddresses; + uint256[5] memory sentAmounts; + + sentAddresses[0] = address(this); /// The lender. + sentAddresses[1] = borrower; + sentAddresses[2] = receiver; + /// sentAddresses[3] = address(0); /// The manager. + + sentAmounts[1] = withdrawAmount; + + /// interestRate, interestInitialAmount, borrowAmount (newBorrowAmount). + (sentAmounts[0], sentAmounts[2], sentAmounts[1]) = _getInterestRateAndBorrowAmount( + sentAmounts[1], + _totalAssetSupply(0), /// Interest is settled above. + initialLoanDuration + ); + + /// sentAmounts[3] = 0; /// loanTokenSent + sentAmounts[4] = collateralTokenSent; + + return + _borrowOrTrade( + loanId, + withdrawAmount, + ProtocolSettingsLike(sovrynContractAddress).minInitialMargin( + loanParamsIds[ + uint256(keccak256(abi.encodePacked(collateralTokenAddress, true))) + ] + ), + collateralTokenAddress, + sentAddresses, + sentAmounts, + "" /// loanDataBytes + ); + } + + /** + * @notice Borrow and immediately get into a position. + * + * Trading on margin is used to increase an investor's buying power. + * Margin is the amount of money required to open a position, while + * leverage is the multiple of exposure to account equity. + * + * Leverage allows you to trade positions LARGER than the amount + * of money in your trading account. Leverage is expressed as a ratio. + * + * When trading on margin, investors first deposit some token that then + * serves as collateral for the loan, and then pay ongoing interest + * payments on the money they borrow. + * + * Margin trading = taking a loan and swapping it: + * In order to open a margin trade position, + * 1.- The user calls marginTrade on the loan token contract. + * 2.- The loan token contract provides the loan and sends it for processing + * to the protocol proxy contract. + * 3.- The protocol proxy contract uses the module LoanOpening to create a + * position and swaps the loan tokens to collateral tokens. + * 4.- The Sovryn Swap network looks up the correct converter and swaps the + * tokens. + * If successful, the position is being held by the protocol proxy contract, + * which is why positions need to be closed at the protocol proxy contract. + * + * @param loanId The ID of the loan, 0 for a new loan. + * @param leverageAmount The multiple of exposure: 2x ... 5x. The leverage with 18 decimals. + * @param loanTokenSent The number of loan tokens provided by the user. + * @param collateralTokenSent The amount of collateral tokens provided by the user. + * @param collateralTokenAddress The token address of collateral. + * @param trader The account that performs this trade. + * @param minReturn Minimum amount (position size) in the collateral tokens + * @param loanDataBytes Additional loan data (not in use for token swaps). + * + * @return New principal and new collateral added to trade. + * */ + function marginTrade( + bytes32 loanId, /// 0 if new loan + uint256 leverageAmount, /// Expected in x * 10**18 where x is the actual leverage (2, 3, 4, or 5). + uint256 loanTokenSent, + uint256 collateralTokenSent, + address collateralTokenAddress, + address trader, + uint256 minReturn, // minimum position size in the collateral tokens + bytes memory loanDataBytes /// Arbitrary order data. + ) + public + payable + nonReentrant /// Note: needs to be removed to allow flashloan use cases. + returns ( + uint256, + uint256 /// Returns new principal and new collateral added to trade. + ) + { + _checkPause(); + + checkPriceDivergence( + leverageAmount, + loanTokenSent, + collateralTokenSent, + collateralTokenAddress, + minReturn + ); + + if (collateralTokenAddress == address(0)) { + collateralTokenAddress = wrbtcTokenAddress; + } + + require(collateralTokenAddress != loanTokenAddress, "11"); + + /// @dev Ensure authorized use of existing loan. + require(loanId == 0 || msg.sender == trader, "401 use of existing loan"); + + /// Temporary: limit transaction size. + if (transactionLimit[collateralTokenAddress] > 0) + require(collateralTokenSent <= transactionLimit[collateralTokenAddress]); + if (transactionLimit[loanTokenAddress] > 0) + require(loanTokenSent <= transactionLimit[loanTokenAddress]); + + /// @dev Compute the worth of the total deposit in loan tokens. + /// (loanTokenSent + convert(collateralTokenSent)) + /// No actual swap happening here. + uint256 totalDeposit = + _totalDeposit(collateralTokenAddress, collateralTokenSent, loanTokenSent); + require(totalDeposit != 0, "12"); + + address[4] memory sentAddresses; + uint256[5] memory sentAmounts; + + sentAddresses[0] = address(this); /// The lender. + sentAddresses[1] = trader; + sentAddresses[2] = trader; + /// sentAddresses[3] = address(0); /// The manager. + + /// sentAmounts[0] = 0; /// interestRate (found later). + sentAmounts[1] = totalDeposit; /// Total amount of deposit. + /// sentAmounts[2] = 0; /// interestInitialAmount (interest is calculated based on fixed-term loan). + sentAmounts[3] = loanTokenSent; + sentAmounts[4] = collateralTokenSent; + + _settleInterest(); + + (sentAmounts[1], sentAmounts[0]) = _getMarginBorrowAmountAndRate( /// borrowAmount, interestRate + leverageAmount, + sentAmounts[1] /// depositAmount + ); + + require( + _getAmountInRbtc(loanTokenAddress, sentAmounts[1]) > TINY_AMOUNT, + "principal too small" + ); + + /// @dev Converting to initialMargin + leverageAmount = SafeMath.div(10**38, leverageAmount); + return + _borrowOrTrade( + loanId, + 0, /// withdrawAmount + leverageAmount, //initial margin + collateralTokenAddress, + sentAddresses, + sentAmounts, + loanDataBytes + ); + } + + /** + * @notice Wrapper for marginTrade invoking setAffiliatesReferrer to track + * referral trade by affiliates program. + * + * @param loanId The ID of the loan, 0 for a new loan. + * @param leverageAmount The multiple of exposure: 2x ... 5x. The leverage with 18 decimals. + * @param loanTokenSent The number of loan tokens provided by the user. + * @param collateralTokenSent The amount of collateral tokens provided by the user. + * @param collateralTokenAddress The token address of collateral. + * @param trader The account that performs this trade. + * @param minReturn Minimum position size in the collateral tokens + * @param affiliateReferrer The address of the referrer from affiliates program. + * @param loanDataBytes Additional loan data (not in use for token swaps). + * + * @return New principal and new collateral added to trade. + */ + function marginTradeAffiliate( + bytes32 loanId, // 0 if new loan + uint256 leverageAmount, // expected in x * 10**18 where x is the actual leverage (2, 3, 4, or 5) + uint256 loanTokenSent, + uint256 collateralTokenSent, + address collateralTokenAddress, + address trader, + uint256 minReturn, /// Minimum position size in the collateral tokens. + address affiliateReferrer, /// The user was brought by the affiliate (referrer). + bytes calldata loanDataBytes /// Arbitrary order data. + ) + external + payable + returns ( + uint256, + uint256 /// Returns new principal and new collateral added to trade. + ) + { + if (affiliateReferrer != address(0)) + ProtocolAffiliatesInterface(sovrynContractAddress).setAffiliatesReferrer( + trader, + affiliateReferrer + ); + return + marginTrade( + loanId, + leverageAmount, + loanTokenSent, + collateralTokenSent, + collateralTokenAddress, + trader, + minReturn, + loanDataBytes + ); + } + + /** + * @notice Withdraws RBTC from the contract by Multisig. + * @param _receiverAddress The address where the rBTC has to be transferred. + * @param _amount The amount of rBTC to be transferred. + */ + function withdrawRBTCTo(address payable _receiverAddress, uint256 _amount) external onlyOwner { + require(_receiverAddress != address(0), "receiver address invalid"); + require(_amount > 0, "non-zero withdraw amount expected"); + require(_amount <= address(this).balance, "withdraw amount cannot exceed balance"); + _receiverAddress.transfer(_amount); + emit WithdrawRBTCTo(_receiverAddress, _amount); + } - /** + /** + * @notice Transfer tokens wrapper. + * Sets token owner the msg.sender. + * Sets maximun allowance uint256(-1) to ensure tokens are always transferred. + * + * @param _to The recipient of the tokens. + * @param _value The amount of tokens sent. + * @return Success true/false. + * */ + function transfer(address _to, uint256 _value) external returns (bool) { + return _internalTransferFrom(msg.sender, _to, _value, uint256(-1)); + } + + /** + * @notice Moves `_value` loan tokens from `_from` to `_to` using the + * allowance mechanism. Calls internal _internalTransferFrom function. + * + * @return A boolean value indicating whether the operation succeeded. + */ + function transferFrom( + address _from, + address _to, + uint256 _value + ) external returns (bool) { + return + _internalTransferFrom( + _from, + _to, + _value, + //allowed[_from][msg.sender] + ProtocolLike(sovrynContractAddress).isLoanPool(msg.sender) + ? uint256(-1) + : allowed[_from][msg.sender] + ); + } + + /** + * @notice Transfer tokens, low level. + * Checks allowance, updates sender and recipient balances + * and updates checkpoints too. + * + * @param _from The tokens' owner. + * @param _to The recipient of the tokens. + * @param _value The amount of tokens sent. + * @param _allowanceAmount The amount of tokens allowed to transfer. + * + * @return Success true/false. + * */ + function _internalTransferFrom( + address _from, + address _to, + uint256 _value, + uint256 _allowanceAmount + ) internal returns (bool) { + if (_allowanceAmount != uint256(-1)) { + allowed[_from][msg.sender] = _allowanceAmount.sub(_value, "14"); + /// @dev Allowance mapping update requires an event log + emit AllowanceUpdate(_from, msg.sender, _allowanceAmount, allowed[_from][msg.sender]); + } + + require(_to != address(0), "15"); + + uint256 _balancesFrom = balances[_from]; + uint256 _balancesFromNew = _balancesFrom.sub(_value, "16"); + balances[_from] = _balancesFromNew; + + uint256 _balancesTo = balances[_to]; + uint256 _balancesToNew = _balancesTo.add(_value); + balances[_to] = _balancesToNew; + + /// @dev Handle checkpoint update. + uint256 _currentPrice = tokenPrice(); + + //checkpoints are not being used by the smart contract logic itself, but just for external use (query the profit) + //only update the checkpoints of a user if he's not depositing to / withdrawing from the lending pool + if (_from != liquidityMiningAddress && _to != liquidityMiningAddress) { + _updateCheckpoints(_from, _balancesFrom, _balancesFromNew, _currentPrice); + _updateCheckpoints(_to, _balancesTo, _balancesToNew, _currentPrice); + } + + emit Transfer(_from, _to, _value); + return true; + } + + /** + * @notice Update the user's checkpoint price and profit so far. + * In this loan token contract, whenever some tokens are minted or burned, + * the _updateCheckpoints() function is invoked to update the stats to + * reflect the balance changes. + * + * @param _user The user address. + * @param _oldBalance The user's previous balance. + * @param _newBalance The user's updated balance. + * @param _currentPrice The current loan token price. + * */ + function _updateCheckpoints( + address _user, + uint256 _oldBalance, + uint256 _newBalance, + uint256 _currentPrice + ) internal { + /// @dev keccak256("iToken_ProfitSoFar") + bytes32 slot = keccak256(abi.encodePacked(_user, iToken_ProfitSoFar)); + + int256 _currentProfit; + if (_newBalance == 0) { + _currentPrice = 0; + } else if (_oldBalance != 0) { + _currentProfit = _profitOf(slot, _oldBalance, _currentPrice, checkpointPrices_[_user]); + } + + assembly { + sstore(slot, _currentProfit) + } + + checkpointPrices_[_user] = _currentPrice; + } + + /* Public View functions */ + + /** + * @notice Wrapper for internal _profitOf low level function. + * @param user The user address. + * @return The profit of a user. + * */ + function profitOf(address user) external view returns (int256) { + /// @dev keccak256("iToken_ProfitSoFar") + bytes32 slot = keccak256(abi.encodePacked(user, iToken_ProfitSoFar)); + //TODO + LM balance + return _profitOf(slot, balances[user], tokenPrice(), checkpointPrices_[user]); + } + + /** + * @notice Profit calculation based on checkpoints of price. + * @param slot The user slot. + * @param _balance The user balance. + * @param _currentPrice The current price of the loan token. + * @param _checkpointPrice The price of the loan token on checkpoint. + * @return The profit of a user. + * */ + function _profitOf( + bytes32 slot, + uint256 _balance, + uint256 _currentPrice, + uint256 _checkpointPrice + ) internal view returns (int256 profitSoFar) { + if (_checkpointPrice == 0) { + return 0; + } + + assembly { + profitSoFar := sload(slot) + } + + profitSoFar = int256(_currentPrice) + .sub(int256(_checkpointPrice)) + .mul(int256(_balance)) + .div(sWEI_PRECISION) + .add(profitSoFar); + } + + /** + * @notice Loan token price calculation considering unpaid interests. + * @return The loan token price. + * */ + function tokenPrice() public view returns (uint256 price) { + uint256 interestUnPaid; + if (lastSettleTime_ != uint88(block.timestamp)) { + (, interestUnPaid) = _getAllInterest(); + } + + return _tokenPrice(_totalAssetSupply(interestUnPaid)); + } + + /** + * @notice Getter for the price checkpoint mapping. + * @param _user The user account as the mapping index. + * @return The price on the checkpoint for this user. + * */ + function checkpointPrice(address _user) public view returns (uint256 price) { + return checkpointPrices_[_user]; + } + + /** + * @notice Get current liquidity. + * A part of total funds supplied are borrowed. Liquidity = supply - borrow + * @return The market liquidity. + * */ + function marketLiquidity() public view returns (uint256) { + uint256 totalSupply = _totalAssetSupply(0); + uint256 totalBorrow = totalAssetBorrow(); + if (totalSupply > totalBorrow) { + return totalSupply - totalBorrow; + } + } + + /** + * @notice Wrapper for average borrow interest. + * @return The average borrow interest. + * */ + function avgBorrowInterestRate() public view returns (uint256) { + return _avgBorrowInterestRate(totalAssetBorrow()); + } + + /** + * @notice Get borrow interest rate. + * The minimum rate the next base protocol borrower will receive + * for variable-rate loans. + * @return The borrow interest rate. + * */ + function borrowInterestRate() public view returns (uint256) { + return _nextBorrowInterestRate(0); + } + + /** + * @notice Public wrapper for internal call. + * @param borrowAmount The amount of tokens to borrow. + * @return The next borrow interest rate. + * */ + function nextBorrowInterestRate(uint256 borrowAmount) public view returns (uint256) { + return _nextBorrowInterestRate(borrowAmount); + } + + /** + * @notice Get interest rate. + * + * @return Interest that lenders are currently receiving when supplying to + * the pool. + * */ + function supplyInterestRate() public view returns (uint256) { + return totalSupplyInterestRate(_totalAssetSupply(0)); + } + + /** + * @notice Get interest rate w/ added supply. + * @param supplyAmount The amount of tokens supplied. + * @return Interest that lenders are currently receiving when supplying + * a given amount of tokens to the pool. + * */ + function nextSupplyInterestRate(uint256 supplyAmount) public view returns (uint256) { + return totalSupplyInterestRate(_totalAssetSupply(0).add(supplyAmount)); + } + + /** + * @notice Get interest rate w/ added supply assets. + * @param assetSupply The amount of loan tokens supplied. + * @return Interest that lenders are currently receiving when supplying + * a given amount of loan tokens to the pool. + * */ + function totalSupplyInterestRate(uint256 assetSupply) public view returns (uint256) { + uint256 assetBorrow = totalAssetBorrow(); + if (assetBorrow != 0) { + return calculateSupplyInterestRate(assetBorrow, assetSupply); + } + } + + /** + * @notice Get the total amount of loan tokens on debt. + * Calls protocol getTotalPrincipal function. + * In the context of borrowing, principal is the initial size of a loan. + * It can also be the amount still owed on a loan. If you take out a + * $50,000 mortgage, for example, the principal is $50,000. If you pay off + * $30,000, the principal balance now consists of the remaining $20,000. + * + * @return The total amount of loan tokens on debt. + * */ + function totalAssetBorrow() public view returns (uint256) { + return + ProtocolLike(sovrynContractAddress).getTotalPrincipal(address(this), loanTokenAddress); + } + + /** + * @notice Get the total amount of loan tokens on supply. + * @dev Wrapper for internal _totalAssetSupply function. + * @return The total amount of loan tokens on supply. + * */ + function totalAssetSupply() public view returns (uint256) { + uint256 interestUnPaid; + if (lastSettleTime_ != uint88(block.timestamp)) { + (, interestUnPaid) = _getAllInterest(); + } + + return _totalAssetSupply(interestUnPaid); + } + + /** + * @notice Compute the maximum deposit amount under current market conditions. + * @dev maxEscrowAmount = liquidity * (100 - interestForDuration) / 100 + * @param leverageAmount The chosen multiplier with 18 decimals. + * */ + function getMaxEscrowAmount(uint256 leverageAmount) + public + view + returns (uint256 maxEscrowAmount) + { + /** + * @dev Mathematical imperfection: depending on liquidity we might be able + * to borrow more if utilization is below the kink level. + * */ + uint256 interestForDuration = maxScaleRate.mul(28).div(365); + uint256 factor = uint256(10**20).sub(interestForDuration); + uint256 maxLoanSize = marketLiquidity().mul(factor).div(10**20); + maxEscrowAmount = maxLoanSize.mul(10**18).div(leverageAmount); + } + + /** + * @notice Get loan token balance. + * @return The user's balance of underlying token. + * */ + function assetBalanceOf(address _owner) public view returns (uint256) { + uint256 balanceOnLM = 0; + if (liquidityMiningAddress != address(0)) { + balanceOnLM = ILiquidityMining(liquidityMiningAddress).getUserPoolTokenBalance( + address(this), + _owner + ); + } + return balanceOf(_owner).add(balanceOnLM).mul(tokenPrice()).div(10**18); + } + + /** + * @notice Get margin information on a trade. + * + * @param leverageAmount The multiple of exposure: 2x ... 5x. The leverage with 18 decimals. + * @param loanTokenSent The number of loan tokens provided by the user. + * @param collateralTokenSent The amount of collateral tokens provided by the user. + * @param collateralTokenAddress The token address of collateral. + * + * @return The principal, the collateral and the interestRate. + * */ + function getEstimatedMarginDetails( + uint256 leverageAmount, + uint256 loanTokenSent, + uint256 collateralTokenSent, + address collateralTokenAddress // address(0) means ETH + ) + public + view + returns ( + uint256 principal, + uint256 collateral, + uint256 interestRate + ) + { + if (collateralTokenAddress == address(0)) { + collateralTokenAddress = wrbtcTokenAddress; + } + + uint256 totalDeposit = + _totalDeposit(collateralTokenAddress, collateralTokenSent, loanTokenSent); + + (principal, interestRate) = _getMarginBorrowAmountAndRate(leverageAmount, totalDeposit); + if (principal > _underlyingBalance()) { + return (0, 0, 0); + } + + loanTokenSent = loanTokenSent.add(principal); + + collateral = ProtocolLike(sovrynContractAddress).getEstimatedMarginExposure( + loanTokenAddress, + collateralTokenAddress, + loanTokenSent, + collateralTokenSent, + interestRate, + principal + ); + } + + /** + * @notice Calculate the deposit required to a given borrow. + * + * The function for doing over-collateralized borrows against loan tokens + * expects a minimum amount of collateral be sent to satisfy collateral + * requirements of the loan, for borrow amount, interest rate, and + * initial loan duration. To determine appropriate values to pass to this + * function for a given loan, `getDepositAmountForBorrow` and + * 'getBorrowAmountForDeposit` are required. + * + * @param borrowAmount The amount of borrow. + * @param initialLoanDuration The duration of the loan. + * @param collateralTokenAddress The token address of collateral. + * + * @return The amount of deposit required. + * */ + function getDepositAmountForBorrow( + uint256 borrowAmount, + uint256 initialLoanDuration, /// Duration in seconds. + address collateralTokenAddress /// address(0) means rBTC + ) public view returns (uint256 depositAmount) { + if (borrowAmount != 0) { + (, , uint256 newBorrowAmount) = + _getInterestRateAndBorrowAmount( + borrowAmount, + totalAssetSupply(), + initialLoanDuration + ); + + if (newBorrowAmount <= _underlyingBalance()) { + if (collateralTokenAddress == address(0)) + collateralTokenAddress = wrbtcTokenAddress; + bytes32 loanParamsId = + loanParamsIds[ + uint256(keccak256(abi.encodePacked(collateralTokenAddress, true))) + ]; + return + ProtocolLike(sovrynContractAddress) + .getRequiredCollateral( + loanTokenAddress, + collateralTokenAddress, + newBorrowAmount, + ProtocolSettingsLike(sovrynContractAddress).minInitialMargin(loanParamsId), /// initialMargin + true /// isTorqueLoan + ) + .add(10); /// Some dust to compensate for rounding errors. + } + } + } + + /** + * @notice Calculate the borrow allowed for a given deposit. + * + * The function for doing over-collateralized borrows against loan tokens + * expects a minimum amount of collateral be sent to satisfy collateral + * requirements of the loan, for borrow amount, interest rate, and + * initial loan duration. To determine appropriate values to pass to this + * function for a given loan, `getDepositAmountForBorrow` and + * 'getBorrowAmountForDeposit` are required. + * + * @param depositAmount The amount of deposit. + * @param initialLoanDuration The duration of the loan. + * @param collateralTokenAddress The token address of collateral. + * + * @return The amount of borrow allowed. + * */ + function getBorrowAmountForDeposit( + uint256 depositAmount, + uint256 initialLoanDuration, /// Duration in seconds. + address collateralTokenAddress /// address(0) means rBTC + ) public view returns (uint256 borrowAmount) { + if (depositAmount != 0) { + if (collateralTokenAddress == address(0)) collateralTokenAddress = wrbtcTokenAddress; + bytes32 loanParamsId = + loanParamsIds[uint256(keccak256(abi.encodePacked(collateralTokenAddress, true)))]; + borrowAmount = ProtocolLike(sovrynContractAddress).getBorrowAmount( + loanTokenAddress, + collateralTokenAddress, + depositAmount, + ProtocolSettingsLike(sovrynContractAddress).minInitialMargin(loanParamsId), /// initialMargin, + true /// isTorqueLoan + ); + + (, , borrowAmount) = _getInterestRateAndBorrowAmount( + borrowAmount, + totalAssetSupply(), + initialLoanDuration + ); + + if (borrowAmount > _underlyingBalance()) { + borrowAmount = 0; + } + } + } + + function checkPriceDivergence( + uint256 leverageAmount, + uint256 loanTokenSent, + uint256 collateralTokenSent, + address collateralTokenAddress, + uint256 minReturn + ) public view { + (, uint256 estimatedCollateral, ) = + getEstimatedMarginDetails( + leverageAmount, + loanTokenSent, + collateralTokenSent, + collateralTokenAddress + ); + require(estimatedCollateral >= minReturn, "coll too low"); + } + + /* Internal functions */ + + /** + * @notice transfers the underlying asset from the msg.sender and mints tokens for the receiver + * @param receiver the address of the iToken receiver + * @param depositAmount the amount of underlying assets to be deposited + * @return the amount of iTokens issued + */ + function _mintToken(address receiver, uint256 depositAmount) + internal + returns (uint256 mintAmount) + { + uint256 currentPrice; + + //calculate amount to mint and transfer the underlying asset + (mintAmount, currentPrice) = _prepareMinting(depositAmount); + + //compute balances needed for checkpoint update, considering that the user might have a pool token balance + //on the liquidity mining contract + uint256 balanceOnLM = 0; + if (liquidityMiningAddress != address(0)) + balanceOnLM = ILiquidityMining(liquidityMiningAddress).getUserPoolTokenBalance( + address(this), + receiver + ); + uint256 oldBalance = balances[receiver].add(balanceOnLM); + uint256 newBalance = oldBalance.add(mintAmount); + + //mint the tokens to the receiver + _mint(receiver, mintAmount, depositAmount, currentPrice); + + //update the checkpoint of the receiver + _updateCheckpoints(receiver, oldBalance, newBalance, currentPrice); + } + + /** + * calculates the amount of tokens to mint and transfers the underlying asset to this contract + * @param depositAmount the amount of the underyling asset deposited + * @return the amount to be minted + */ + function _prepareMinting(uint256 depositAmount) + internal + returns (uint256 mintAmount, uint256 currentPrice) + { + require(depositAmount != 0, "17"); + + _settleInterest(); + + currentPrice = _tokenPrice(_totalAssetSupply(0)); + mintAmount = depositAmount.mul(10**18).div(currentPrice); + + if (msg.value == 0) { + _safeTransferFrom(loanTokenAddress, msg.sender, address(this), depositAmount, "18"); + } else { + IWrbtc(wrbtcTokenAddress).deposit.value(depositAmount)(); + } + } + + /** + * @notice A wrapper for AdvancedToken::_burn + * + * @param burnAmount The amount of loan tokens to redeem. + * + * @return The amount of underlying tokens payed to lender. + * */ + function _burnToken(uint256 burnAmount) internal returns (uint256 loanAmountPaid) { + require(burnAmount != 0, "19"); + + if (burnAmount > balanceOf(msg.sender)) { + require(burnAmount == uint256(-1), "32"); + burnAmount = balanceOf(msg.sender); + } + + _settleInterest(); + + uint256 currentPrice = _tokenPrice(_totalAssetSupply(0)); + + uint256 loanAmountOwed = burnAmount.mul(currentPrice).div(10**18); + uint256 loanAmountAvailableInContract = _underlyingBalance(); + + loanAmountPaid = loanAmountOwed; + require(loanAmountPaid <= loanAmountAvailableInContract, "37"); + + //compute balances needed for checkpoint update, considering that the user might have a pool token balance + //on the liquidity mining contract + uint256 balanceOnLM = 0; + if (liquidityMiningAddress != address(0)) + balanceOnLM = ILiquidityMining(liquidityMiningAddress).getUserPoolTokenBalance( + address(this), + msg.sender + ); + uint256 oldBalance = balances[msg.sender].add(balanceOnLM); + uint256 newBalance = oldBalance.sub(burnAmount); + + _burn(msg.sender, burnAmount, loanAmountPaid, currentPrice); + + //this function does not only update the checkpoints but also the current profit of the user + //all for external use only + _updateCheckpoints(msg.sender, oldBalance, newBalance, currentPrice); + } + + /** + * @notice Withdraw loan token interests from protocol. + * This function only operates once per block. + * It asks protocol to withdraw accrued interests for the loan token. + * + * @dev Internal sync required on every loan trade before starting. + * */ + function _settleInterest() internal { + uint88 ts = uint88(block.timestamp); + if (lastSettleTime_ != ts) { + ProtocolLike(sovrynContractAddress).withdrawAccruedInterest(loanTokenAddress); + + lastSettleTime_ = ts; + } + } + + /** + * @notice Compute what the deposit is worth in loan tokens using the swap rate + * used for loan size computation. + * + * @param collateralTokenAddress The token address of the collateral. + * @param collateralTokenSent The amount of collateral tokens provided by the user. + * @param loanTokenSent The number of loan tokens provided by the user. + * + * @return The value of the deposit in loan tokens. + * */ + function _totalDeposit( + address collateralTokenAddress, + uint256 collateralTokenSent, + uint256 loanTokenSent + ) internal view returns (uint256 totalDeposit) { + totalDeposit = loanTokenSent; + + if (collateralTokenSent != 0) { + /// @dev Get the oracle rate from collateral -> loan + (uint256 collateralToLoanRate, uint256 collateralToLoanPrecision) = + FeedsLike(ProtocolLike(sovrynContractAddress).priceFeeds()).queryRate( + collateralTokenAddress, + loanTokenAddress + ); + require( + (collateralToLoanRate != 0) && (collateralToLoanPrecision != 0), + "invalid rate collateral token" + ); + + /// @dev Compute the loan token amount with the oracle rate. + uint256 loanTokenAmount = + collateralTokenSent.mul(collateralToLoanRate).div(collateralToLoanPrecision); + + /// @dev See how many collateralTokens we would get if exchanging this amount of loan tokens to collateral tokens. + uint256 collateralTokenAmount = + ProtocolLike(sovrynContractAddress).getSwapExpectedReturn( + loanTokenAddress, + collateralTokenAddress, + loanTokenAmount + ); + + /// @dev Probably not the same due to the price difference. + if (collateralTokenAmount != collateralTokenSent) { + //scale the loan token amount accordingly, so we'll get the expected position size in the end + loanTokenAmount = loanTokenAmount.mul(collateralTokenAmount).div( + collateralTokenSent + ); + } + + totalDeposit = loanTokenAmount.add(totalDeposit); + } + } + + /** + * @dev returns amount of the asset converted to RBTC + * @param asset the asset to be transferred + * @param amount the amount to be transferred + * @return amount in RBTC + * */ + function _getAmountInRbtc(address asset, uint256 amount) internal returns (uint256) { + (uint256 rbtcRate, uint256 rbtcPrecision) = + FeedsLike(ProtocolLike(sovrynContractAddress).priceFeeds()).queryRate( + asset, + wrbtcTokenAddress + ); + return amount.mul(rbtcRate).div(rbtcPrecision); + } + + /* + * @notice Compute interest rate and other loan parameters. + * + * @param borrowAmount The amount of tokens to borrow. + * @param assetSupply The amount of loan tokens supplied. + * @param initialLoanDuration The duration of the loan in seconds. + * If the loan is not paid back until then, it'll need to be rolled over. + * + * @return The interest rate, the interest calculated based on fixed-term + * loan, and the new borrow amount. + * */ + function _getInterestRateAndBorrowAmount( + uint256 borrowAmount, + uint256 assetSupply, + uint256 initialLoanDuration /// Duration in seconds. + ) + internal + view + returns ( + uint256 interestRate, + uint256 interestInitialAmount, + uint256 newBorrowAmount + ) + { + interestRate = _nextBorrowInterestRate2(borrowAmount, assetSupply); + + /// newBorrowAmount = borrowAmount * 10^18 / (10^18 - interestRate * 7884000 * 10^18 / 31536000 / 10^20) + newBorrowAmount = borrowAmount.mul(10**18).div( + SafeMath.sub( + 10**18, + interestRate.mul(initialLoanDuration).mul(10**18).div(31536000 * 10**20) /// 365 * 86400 * 10**20 + ) + ); + + interestInitialAmount = newBorrowAmount.sub(borrowAmount); + } + + /** + * @notice Compute principal and collateral. + * + * @param loanId The ID of the loan, 0 for a new loan. + * @param withdrawAmount The amount to be withdrawn (actually borrowed). + * @param initialMargin The initial margin with 18 decimals + * @param collateralTokenAddress The address of the token to be used as + * collateral. Cannot be the loan token address. + * @param sentAddresses The addresses to send tokens: lender, borrower, + * receiver and manager. + * @param sentAmounts The amounts to send to each address. + * @param loanDataBytes Additional loan data (not in use for token swaps). + * + * @return The new principal and the new collateral. Principal is the + * complete borrowed amount (in loan tokens). Collateral is the complete + * position size (loan + margin) (in collateral tokens). + * */ + function _borrowOrTrade( + bytes32 loanId, + uint256 withdrawAmount, + uint256 initialMargin, + address collateralTokenAddress, + address[4] memory sentAddresses, + uint256[5] memory sentAmounts, + bytes memory loanDataBytes + ) internal returns (uint256, uint256) { + _checkPause(); + require( + sentAmounts[1] <= _underlyingBalance() && /// newPrincipal (borrowed amount + fees) + sentAddresses[1] != address(0), /// The borrower. + "24" + ); + + if (sentAddresses[2] == address(0)) { + sentAddresses[2] = sentAddresses[1]; /// The receiver = the borrower. + } + + /// @dev Handle transfers prior to adding newPrincipal to loanTokenSent + uint256 msgValue = + _verifyTransfers(collateralTokenAddress, sentAddresses, sentAmounts, withdrawAmount); + + /** + * @dev Adding the loan token portion from the lender to loanTokenSent + * (add the loan to the loan tokens sent from the user). + * */ + sentAmounts[3] = sentAmounts[3].add(sentAmounts[1]); /// newPrincipal + + if (withdrawAmount != 0) { + /// @dev withdrawAmount already sent to the borrower, so we aren't sending it to the protocol. + sentAmounts[3] = sentAmounts[3].sub(withdrawAmount); + } + + bool withdrawAmountExist = false; /// Default is false, but added just as to make sure. + + if (withdrawAmount != 0) { + withdrawAmountExist = true; + } + + bytes32 loanParamsId = + loanParamsIds[ + uint256(keccak256(abi.encodePacked(collateralTokenAddress, withdrawAmountExist))) + ]; + + (sentAmounts[1], sentAmounts[4]) = ProtocolLike(sovrynContractAddress) + .borrowOrTradeFromPool + .value(msgValue)( /// newPrincipal, newCollateral + loanParamsId, + loanId, + withdrawAmountExist, + initialMargin, + sentAddresses, + sentAmounts, + loanDataBytes + ); + require(sentAmounts[1] != 0, "25"); + + /// @dev Setting not-first-trade flag to prevent binding to an affiliate existing users post factum. + /// @dev REFACTOR: move to a general interface: ProtocolSettingsLike? + ProtocolAffiliatesInterface(sovrynContractAddress).setUserNotFirstTradeFlag( + sentAddresses[1] + ); + + return (sentAmounts[1], sentAmounts[4]); // newPrincipal, newCollateral + } + + /// sentAddresses[0]: lender + /// sentAddresses[1]: borrower + /// sentAddresses[2]: receiver + /// sentAddresses[3]: manager + /// sentAmounts[0]: interestRate + /// sentAmounts[1]: newPrincipal + /// sentAmounts[2]: interestInitialAmount + /// sentAmounts[3]: loanTokenSent + /// sentAmounts[4]: collateralTokenSent + /** + * @notice . + * + * @param collateralTokenAddress The address of the token to be used as + * collateral. Cannot be the loan token address. + * @param sentAddresses The addresses to send tokens: lender, borrower, + * receiver and manager. + * @param sentAmounts The amounts to send to each address. + * @param withdrawalAmount The amount of tokens to withdraw. + * + * @return msgValue The amount of rBTC sent minus the collateral on tokens. + * */ + function _verifyTransfers( + address collateralTokenAddress, + address[4] memory sentAddresses, + uint256[5] memory sentAmounts, + uint256 withdrawalAmount + ) internal returns (uint256 msgValue) { + address _wrbtcToken = wrbtcTokenAddress; + address _loanTokenAddress = loanTokenAddress; + address receiver = sentAddresses[2]; + uint256 newPrincipal = sentAmounts[1]; + uint256 loanTokenSent = sentAmounts[3]; + uint256 collateralTokenSent = sentAmounts[4]; + + require(_loanTokenAddress != collateralTokenAddress, "26"); + + msgValue = msg.value; + + if (withdrawalAmount != 0) { + /// withdrawOnOpen == true + _safeTransfer(_loanTokenAddress, receiver, withdrawalAmount, ""); + if (newPrincipal > withdrawalAmount) { + _safeTransfer( + _loanTokenAddress, + sovrynContractAddress, + newPrincipal - withdrawalAmount, + "" + ); + } + } else { + _safeTransfer(_loanTokenAddress, sovrynContractAddress, newPrincipal, "27"); + } + /** + * This is a critical piece of code! + * rBTC are supposed to be held by the contract itself, while other tokens are being transfered from the sender directly. + * */ + if (collateralTokenSent != 0) { + if ( + collateralTokenAddress == _wrbtcToken && + msgValue != 0 && + msgValue >= collateralTokenSent + ) { + IWrbtc(_wrbtcToken).deposit.value(collateralTokenSent)(); + _safeTransfer( + collateralTokenAddress, + sovrynContractAddress, + collateralTokenSent, + "28-a" + ); + msgValue -= collateralTokenSent; + } else { + _safeTransferFrom( + collateralTokenAddress, + msg.sender, + sovrynContractAddress, + collateralTokenSent, + "28-b" + ); + } + } + + if (loanTokenSent != 0) { + _safeTransferFrom( + _loanTokenAddress, + msg.sender, + sovrynContractAddress, + loanTokenSent, + "29" + ); + } + } + + /** + * @notice Execute the ERC20 token's `transfer` function and reverts + * upon failure the main purpose of this function is to prevent a non + * standard ERC20 token from failing silently. + * + * @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. + * + * @param token The ERC20 token address. + * @param to The target address. + * @param amount The transfer amount. + * @param errorMsg The error message on failure. + */ + function _safeTransfer( + address token, + address to, + uint256 amount, + string memory errorMsg + ) internal { + _callOptionalReturn( + token, + abi.encodeWithSelector(IERC20(token).transfer.selector, to, amount), + errorMsg + ); + } + + /** + * @notice Execute the ERC20 token's `transferFrom` function and reverts + * upon failure the main purpose of this function is to prevent a non + * standard ERC20 token from failing silently. + * + * @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. + * + * @param token The ERC20 token address. + * @param from The source address. + * @param to The target address. + * @param amount The transfer amount. + * @param errorMsg The error message on failure. + */ + function _safeTransferFrom( + address token, + address from, + address to, + uint256 amount, + string memory errorMsg + ) internal { + _callOptionalReturn( + token, + abi.encodeWithSelector(IERC20(token).transferFrom.selector, from, to, amount), + errorMsg + ); + } + + /** + * @notice Imitate 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). + * @param errorMsg The error message on failure. + * */ + function _callOptionalReturn( + address token, + bytes memory data, + string memory errorMsg + ) internal { + require(Address.isContract(token), "call to a non-contract address"); + (bool success, bytes memory returndata) = token.call(data); + require(success, errorMsg); + + if (returndata.length != 0) { + require(abi.decode(returndata, (bool)), errorMsg); + } + } + + /** + * @notice Get the loan contract balance. + * @return The balance of the loan token for this contract. + * */ + function _underlyingBalance() internal view returns (uint256) { + return IERC20(loanTokenAddress).balanceOf(address(this)); + } + + /* Internal View functions */ + + /** + * @notice Compute the token price. + * @param assetSupply The amount of loan tokens supplied. + * @return The token price. + * */ + function _tokenPrice(uint256 assetSupply) internal view returns (uint256) { + uint256 totalTokenSupply = totalSupply_; + + return + totalTokenSupply != 0 ? assetSupply.mul(10**18).div(totalTokenSupply) : initialPrice; + } + + /** + * @notice Compute the average borrow interest rate. + * @param assetBorrow The amount of loan tokens on debt. + * @return The average borrow interest rate. + * */ + function _avgBorrowInterestRate(uint256 assetBorrow) internal view returns (uint256) { + if (assetBorrow != 0) { + (uint256 interestOwedPerDay, ) = _getAllInterest(); + return interestOwedPerDay.mul(10**20).mul(365).div(assetBorrow); + } + } + + /** + * @notice Compute the next supply interest adjustment. + * @param assetBorrow The amount of loan tokens on debt. + * @param assetSupply The amount of loan tokens supplied. + * @return The next supply interest adjustment. + * */ + function calculateSupplyInterestRate(uint256 assetBorrow, uint256 assetSupply) + public + view + returns (uint256) + { + if (assetBorrow != 0 && assetSupply >= assetBorrow) { + return + _avgBorrowInterestRate(assetBorrow) + .mul(_utilizationRate(assetBorrow, assetSupply)) + .mul( + SafeMath.sub(10**20, ProtocolLike(sovrynContractAddress).lendingFeePercent()) + ) + .div(10**40); + } + } + + /** + * @notice Compute the next borrow interest adjustment. + * @param borrowAmount The amount of tokens to borrow. + * @return The next borrow interest adjustment. + * */ + function _nextBorrowInterestRate(uint256 borrowAmount) internal view returns (uint256) { + uint256 interestUnPaid; + if (borrowAmount != 0) { + if (lastSettleTime_ != uint88(block.timestamp)) { + (, interestUnPaid) = _getAllInterest(); + } + + uint256 balance = _underlyingBalance().add(interestUnPaid); + if (borrowAmount > balance) { + borrowAmount = balance; + } + } + + return _nextBorrowInterestRate2(borrowAmount, _totalAssetSupply(interestUnPaid)); + } + + /** + * @notice Compute the next borrow interest adjustment under target-kink + * level analysis. + * + * The "kink" in the cDAI interest rate model reflects the utilization rate + * at which the slope of the interest rate goes from "gradual" to "steep". + * That is, below this utilization rate, the slope of the interest rate + * curve is gradual. Above this utilization rate, it is steep. + * + * Because of this dynamic between the interest rate curves before and + * after the "kink", the "kink" can be thought of as the target utilization + * rate. Above that rate, it quickly becomes expensive to borrow (and + * commensurately lucrative for suppliers). + * + * @param newBorrowAmount The new amount of tokens to borrow. + * @param assetSupply The amount of loan tokens supplied. + * @return The next borrow interest adjustment. + * */ + function _nextBorrowInterestRate2(uint256 newBorrowAmount, uint256 assetSupply) + internal + view + returns (uint256 nextRate) + { + uint256 utilRate = _utilizationRate(totalAssetBorrow().add(newBorrowAmount), assetSupply); + + uint256 thisMinRate; + uint256 thisRateAtKink; + uint256 thisBaseRate = baseRate; + uint256 thisRateMultiplier = rateMultiplier; + uint256 thisTargetLevel = targetLevel; + uint256 thisKinkLevel = kinkLevel; + uint256 thisMaxScaleRate = maxScaleRate; + + if (utilRate < thisTargetLevel) { + // target targetLevel utilization when utilization is under targetLevel + utilRate = thisTargetLevel; + } + + if (utilRate > thisKinkLevel) { + /// @dev Scale rate proportionally up to 100% + uint256 thisMaxRange = WEI_PERCENT_PRECISION - thisKinkLevel; /// Will not overflow. + + utilRate -= thisKinkLevel; + if (utilRate > thisMaxRange) utilRate = thisMaxRange; + + // Modified the rate calculation as it is slightly exaggerated around kink level + // thisRateAtKink = thisRateMultiplier.add(thisBaseRate).mul(thisKinkLevel).div(WEI_PERCENT_PRECISION); + thisRateAtKink = thisKinkLevel.mul(thisRateMultiplier).div(WEI_PERCENT_PRECISION).add( + thisBaseRate + ); + + nextRate = utilRate + .mul(SafeMath.sub(thisMaxScaleRate, thisRateAtKink)) + .div(thisMaxRange) + .add(thisRateAtKink); + } else { + nextRate = utilRate.mul(thisRateMultiplier).div(WEI_PERCENT_PRECISION).add( + thisBaseRate + ); + + thisMinRate = thisBaseRate; + thisRateAtKink = thisRateMultiplier.add(thisBaseRate); + + if (nextRate < thisMinRate) nextRate = thisMinRate; + else if (nextRate > thisRateAtKink) nextRate = thisRateAtKink; + } + } + + /** + * @notice Get two kind of interests: owed per day and yet to be paid. + * @return interestOwedPerDay The interest per day. + * @return interestUnPaid The interest not yet paid. + * */ + function _getAllInterest() + internal + view + returns (uint256 interestOwedPerDay, uint256 interestUnPaid) + { + /// interestPaid, interestPaidDate, interestOwedPerDay, interestUnPaid, interestFeePercent, principalTotal + uint256 interestFeePercent; + (, , interestOwedPerDay, interestUnPaid, interestFeePercent, ) = ProtocolLike( + sovrynContractAddress + ) + .getLenderInterestData(address(this), loanTokenAddress); + + interestUnPaid = interestUnPaid.mul(SafeMath.sub(10**20, interestFeePercent)).div(10**20); + } + + /** + * @notice Compute the loan size and interest rate. + * @param leverageAmount The leverage with 18 decimals. + * @param depositAmount The amount the user deposited in underlying loan tokens. + * @return borrowAmount The amount of tokens to borrow. + * @return interestRate The interest rate to pay on the position. + * */ + function _getMarginBorrowAmountAndRate(uint256 leverageAmount, uint256 depositAmount) + internal + view + returns (uint256 borrowAmount, uint256 interestRate) + { + uint256 loanSizeBeforeInterest = depositAmount.mul(leverageAmount).div(10**18); + /** + * @dev Mathematical imperfection. we calculate the interest rate based on + * the loanSizeBeforeInterest, but the actual borrowed amount will be bigger. + * */ + interestRate = _nextBorrowInterestRate2(loanSizeBeforeInterest, _totalAssetSupply(0)); + /// @dev Assumes that loan, collateral, and interest token are the same. + borrowAmount = _adjustLoanSize(interestRate, 28 days, loanSizeBeforeInterest); + } + + /** + * @notice Compute the total amount of loan tokens on supply. + * @param interestUnPaid The interest not yet paid. + * @return assetSupply The total amount of loan tokens on supply. + * */ + function _totalAssetSupply(uint256 interestUnPaid) + internal + view + returns (uint256 assetSupply) + { + if (totalSupply_ != 0) { + uint256 assetsBalance = _flTotalAssetSupply; /// Temporary locked totalAssetSupply during a flash loan transaction. + if (assetsBalance == 0) { + assetsBalance = _underlyingBalance().add(totalAssetBorrow()); + } + + return assetsBalance.add(interestUnPaid); + } + } + + /** + * @notice Check whether a function is paused. + * + * @dev Used to read externally from the smart contract to see if a + * function is paused. + * + * @param funcId The function ID, the selector. + * + * @return isPaused Whether the function is paused: true or false. + * */ + function checkPause(string memory funcId) public view returns (bool isPaused) { + bytes4 sig = bytes4(keccak256(abi.encodePacked(funcId))); + bytes32 slot = + keccak256( + abi.encodePacked( + sig, + uint256(0xd46a704bc285dbd6ff5ad3863506260b1df02812f4f857c8cc852317a6ac64f2) + ) + ); + assembly { + isPaused := sload(slot) + } + return isPaused; + } + + /** + * @notice Make sure call is not paused. + * @dev Used for internal verification if the called function is paused. + * It throws an exception in case it's not. + * */ + function _checkPause() internal view { + /// keccak256("iToken_FunctionPause") + bytes32 slot = + keccak256( + abi.encodePacked( + msg.sig, + uint256(0xd46a704bc285dbd6ff5ad3863506260b1df02812f4f857c8cc852317a6ac64f2) + ) + ); + bool isPaused; + assembly { + isPaused := sload(slot) + } + require(!isPaused, "unauthorized"); + } + + /** + * @notice Adjusts the loan size to make sure the expected exposure remains after prepaying the interest. + * @dev loanSizeWithInterest = loanSizeBeforeInterest * 100 / (100 - interestForDuration) + * @param interestRate The interest rate to pay on the position. + * @param maxDuration The maximum duration of the position (until rollover). + * @param loanSizeBeforeInterest The loan size before interest is added. + * */ + function _adjustLoanSize( + uint256 interestRate, + uint256 maxDuration, + uint256 loanSizeBeforeInterest + ) internal pure returns (uint256 loanSizeWithInterest) { + uint256 interestForDuration = interestRate.mul(maxDuration).div(365 days); + uint256 divisor = uint256(10**20).sub(interestForDuration); + loanSizeWithInterest = loanSizeBeforeInterest.mul(10**20).div(divisor); + } + + /** + * @notice Calculate the utilization rate. + * @dev Utilization rate = assetBorrow / assetSupply + * @param assetBorrow The amount of loan tokens on debt. + * @param assetSupply The amount of loan tokens supplied. + * @return The utilization rate. + * */ + function _utilizationRate(uint256 assetBorrow, uint256 assetSupply) + internal + pure + returns (uint256) + { + if (assetBorrow != 0 && assetSupply != 0) { + /// U = total_borrow / total_supply + return assetBorrow.mul(10**20).div(assetSupply); + } + } + + /** + * @notice sets the liquidity mining contract address + * @param LMAddress the address of the liquidity mining contract + */ + function setLiquidityMiningAddress(address LMAddress) external onlyOwner { + liquidityMiningAddress = LMAddress; + } + + /** * @notice We need separate getter for newly added storage variable * @notice Getter for liquidityMiningAddress * @return liquidityMiningAddress */ - function getLiquidityMiningAddress() public view returns (address) { - return liquidityMiningAddress; - } - - function _mintWithLM(address receiver, uint256 depositAmount) internal returns (uint256 minted) { - //mint the tokens for the receiver - minted = _mintToken(receiver, depositAmount); - - //transfer the tokens from the receiver to the LM address - _internalTransferFrom(receiver, liquidityMiningAddress, minted, minted); - - //inform the LM mining contract - ILiquidityMining(liquidityMiningAddress).onTokensDeposited(receiver, minted); - } - - function _burnFromLM(uint256 burnAmount) internal returns (uint256) { - uint256 balanceOnLM = ILiquidityMining(liquidityMiningAddress).getUserPoolTokenBalance(address(this), msg.sender); - require(balanceOnLM.add(balanceOf(msg.sender)) >= burnAmount, "not enough balance"); - - if (balanceOnLM > 0) { - //withdraw pool tokens and LM rewards to the passed address - if (balanceOnLM < burnAmount) { - ILiquidityMining(liquidityMiningAddress).withdraw(address(this), balanceOnLM, msg.sender); - } else { - ILiquidityMining(liquidityMiningAddress).withdraw(address(this), burnAmount, msg.sender); - } - } - //burn the tokens of the msg.sender - return _burnToken(burnAmount); - } + function getLiquidityMiningAddress() public view returns (address) { + return liquidityMiningAddress; + } + + function _mintWithLM(address receiver, uint256 depositAmount) + internal + returns (uint256 minted) + { + //mint the tokens for the receiver + minted = _mintToken(receiver, depositAmount); + + //transfer the tokens from the receiver to the LM address + _internalTransferFrom(receiver, liquidityMiningAddress, minted, minted); + + //inform the LM mining contract + ILiquidityMining(liquidityMiningAddress).onTokensDeposited(receiver, minted); + } + + function _burnFromLM(uint256 burnAmount) internal returns (uint256) { + uint256 balanceOnLM = + ILiquidityMining(liquidityMiningAddress).getUserPoolTokenBalance( + address(this), + msg.sender + ); + require(balanceOnLM.add(balanceOf(msg.sender)) >= burnAmount, "not enough balance"); + + if (balanceOnLM > 0) { + //withdraw pool tokens and LM rewards to the passed address + if (balanceOnLM < burnAmount) { + ILiquidityMining(liquidityMiningAddress).withdraw( + address(this), + balanceOnLM, + msg.sender + ); + } else { + ILiquidityMining(liquidityMiningAddress).withdraw( + address(this), + burnAmount, + msg.sender + ); + } + } + //burn the tokens of the msg.sender + return _burnToken(burnAmount); + } } diff --git a/contracts/connectors/loantoken/LoanTokenLogicStorage.sol b/contracts/connectors/loantoken/LoanTokenLogicStorage.sol index 9a9bd6caa..ad5e0af5b 100644 --- a/contracts/connectors/loantoken/LoanTokenLogicStorage.sol +++ b/contracts/connectors/loantoken/LoanTokenLogicStorage.sol @@ -3,39 +3,40 @@ pragma solidity 0.5.17; import "./AdvancedToken.sol"; contract LoanTokenLogicStorage is AdvancedToken { - /// DO NOT ADD VARIABLES HERE - SEE BELOW - - /// @dev It is important to maintain the variables order so the delegate - /// calls can access sovrynContractAddress - - /// ------------- MUST BE THE SAME AS IN LoanToken CONTRACT ------------------- - address public sovrynContractAddress; - address public wrbtcTokenAddress; - address public target_; - address public admin; - /// ------------- END MUST BE THE SAME AS IN LoanToken CONTRACT ------------------- - - /// @dev Add new variables here on the bottom. - address public earlyAccessToken; //not used anymore, but staying for upgradability - address public pauser; - /** The address of the liquidity mining contract */ - address public liquidityMiningAddress; - - /// @dev Used by flashBorrow function. - uint256 public constant VERSION = 6; - /// @dev Used by flashBorrow function. - address internal constant arbitraryCaller = 0x000F400e6818158D541C3EBE45FE3AA0d47372FF; - bytes32 internal constant iToken_ProfitSoFar = 0x37aa2b7d583612f016e4a4de4292cb015139b3d7762663d06a53964912ea2fb6; // keccak256("iToken_ProfitSoFar") - uint256 public constant TINY_AMOUNT = 25e13; - - function stringToBytes32(string memory source) public pure returns (bytes32 result) { - bytes memory tempEmptyStringTest = bytes(source); - if (tempEmptyStringTest.length == 0) { - return 0x0; - } - - assembly { - result := mload(add(source, 32)) - } - } + /// DO NOT ADD VARIABLES HERE - SEE BELOW + + /// @dev It is important to maintain the variables order so the delegate + /// calls can access sovrynContractAddress + + /// ------------- MUST BE THE SAME AS IN LoanToken CONTRACT ------------------- + address public sovrynContractAddress; + address public wrbtcTokenAddress; + address public target_; + address public admin; + /// ------------- END MUST BE THE SAME AS IN LoanToken CONTRACT ------------------- + + /// @dev Add new variables here on the bottom. + address public earlyAccessToken; //not used anymore, but staying for upgradability + address public pauser; + /** The address of the liquidity mining contract */ + address public liquidityMiningAddress; + + /// @dev Used by flashBorrow function. + uint256 public constant VERSION = 6; + /// @dev Used by flashBorrow function. + address internal constant arbitraryCaller = 0x000F400e6818158D541C3EBE45FE3AA0d47372FF; + bytes32 internal constant iToken_ProfitSoFar = + 0x37aa2b7d583612f016e4a4de4292cb015139b3d7762663d06a53964912ea2fb6; // keccak256("iToken_ProfitSoFar") + uint256 public constant TINY_AMOUNT = 25e13; + + function stringToBytes32(string memory source) public pure returns (bytes32 result) { + bytes memory tempEmptyStringTest = bytes(source); + if (tempEmptyStringTest.length == 0) { + return 0x0; + } + + assembly { + result := mload(add(source, 32)) + } + } } diff --git a/contracts/connectors/loantoken/Pausable.sol b/contracts/connectors/loantoken/Pausable.sol index abda026c1..e76adfaa1 100644 --- a/contracts/connectors/loantoken/Pausable.sol +++ b/contracts/connectors/loantoken/Pausable.sol @@ -14,28 +14,29 @@ pragma solidity 0.5.17; * pause state of contract functions. * */ contract Pausable { - /// keccak256("Pausable_FunctionPause") - bytes32 internal constant Pausable_FunctionPause = 0xa7143c84d793a15503da6f19bf9119a2dac94448ca45d77c8bf08f57b2e91047; + /// keccak256("Pausable_FunctionPause") + bytes32 internal constant Pausable_FunctionPause = + 0xa7143c84d793a15503da6f19bf9119a2dac94448ca45d77c8bf08f57b2e91047; - modifier pausable(bytes4 sig) { - require(!_isPaused(sig), "unauthorized"); - _; - } + modifier pausable(bytes4 sig) { + require(!_isPaused(sig), "unauthorized"); + _; + } - /** - * @notice Check whether a function is paused. - * - * @dev Used to read externally from the smart contract to see if a - * function is paused. - * - * @param sig The function ID, the selector on bytes4. - * - * @return isPaused Whether the function is paused: true or false. - * */ - function _isPaused(bytes4 sig) internal view returns (bool isPaused) { - bytes32 slot = keccak256(abi.encodePacked(sig, Pausable_FunctionPause)); - assembly { - isPaused := sload(slot) - } - } + /** + * @notice Check whether a function is paused. + * + * @dev Used to read externally from the smart contract to see if a + * function is paused. + * + * @param sig The function ID, the selector on bytes4. + * + * @return isPaused Whether the function is paused: true or false. + * */ + function _isPaused(bytes4 sig) internal view returns (bool isPaused) { + bytes32 slot = keccak256(abi.encodePacked(sig, Pausable_FunctionPause)); + assembly { + isPaused := sload(slot) + } + } } diff --git a/contracts/connectors/loantoken/interfaces/FeedsLike.sol b/contracts/connectors/loantoken/interfaces/FeedsLike.sol index 5e7e1c4d0..029e9a1e6 100644 --- a/contracts/connectors/loantoken/interfaces/FeedsLike.sol +++ b/contracts/connectors/loantoken/interfaces/FeedsLike.sol @@ -6,5 +6,8 @@ pragma solidity 0.5.17; interface FeedsLike { - function queryRate(address sourceTokenAddress, address destTokenAddress) external view returns (uint256 rate, uint256 precision); + function queryRate(address sourceTokenAddress, address destTokenAddress) + external + view + returns (uint256 rate, uint256 precision); } diff --git a/contracts/connectors/loantoken/interfaces/ProtocolLike.sol b/contracts/connectors/loantoken/interfaces/ProtocolLike.sol index d315f9502..f964d4a7b 100644 --- a/contracts/connectors/loantoken/interfaces/ProtocolLike.sol +++ b/contracts/connectors/loantoken/interfaces/ProtocolLike.sol @@ -6,75 +6,75 @@ pragma solidity 0.5.17; interface ProtocolLike { - function borrowOrTradeFromPool( - bytes32 loanParamsId, - bytes32 loanId, // if 0, start a new loan - bool isTorqueLoan, - uint256 initialMargin, - address[4] calldata sentAddresses, - // lender: must match loan if loanId provided - // borrower: must match loan if loanId provided - // receiver: receiver of funds (address(0) assumes borrower address) - // manager: delegated manager of loan unless address(0) - uint256[5] calldata sentValues, - // newRate: new loan interest rate - // newPrincipal: new loan size (borrowAmount + any borrowed interest) - // torqueInterest: new amount of interest to escrow for Torque loan (determines initial loan length) - // loanTokenReceived: total loanToken deposit (amount not sent to borrower in the case of Torque loans) - // collateralTokenReceived: total collateralToken deposit - bytes calldata loanDataBytes - ) external payable returns (uint256 newPrincipal, uint256 newCollateral); + function borrowOrTradeFromPool( + bytes32 loanParamsId, + bytes32 loanId, // if 0, start a new loan + bool isTorqueLoan, + uint256 initialMargin, + address[4] calldata sentAddresses, + // lender: must match loan if loanId provided + // borrower: must match loan if loanId provided + // receiver: receiver of funds (address(0) assumes borrower address) + // manager: delegated manager of loan unless address(0) + uint256[5] calldata sentValues, + // newRate: new loan interest rate + // newPrincipal: new loan size (borrowAmount + any borrowed interest) + // torqueInterest: new amount of interest to escrow for Torque loan (determines initial loan length) + // loanTokenReceived: total loanToken deposit (amount not sent to borrower in the case of Torque loans) + // collateralTokenReceived: total collateralToken deposit + bytes calldata loanDataBytes + ) external payable returns (uint256 newPrincipal, uint256 newCollateral); - function getTotalPrincipal(address lender, address loanToken) external view returns (uint256); + function getTotalPrincipal(address lender, address loanToken) external view returns (uint256); - function withdrawAccruedInterest(address loanToken) external; + function withdrawAccruedInterest(address loanToken) external; - function getLenderInterestData(address lender, address loanToken) - external - view - returns ( - uint256 interestPaid, - uint256 interestPaidDate, - uint256 interestOwedPerDay, - uint256 interestUnPaid, - uint256 interestFeePercent, - uint256 principalTotal - ); + function getLenderInterestData(address lender, address loanToken) + external + view + returns ( + uint256 interestPaid, + uint256 interestPaidDate, + uint256 interestOwedPerDay, + uint256 interestUnPaid, + uint256 interestFeePercent, + uint256 principalTotal + ); - function priceFeeds() external view returns (address); + function priceFeeds() external view returns (address); - function getEstimatedMarginExposure( - address loanToken, - address collateralToken, - uint256 loanTokenSent, - uint256 collateralTokenSent, - uint256 interestRate, - uint256 newPrincipal - ) external view returns (uint256); + function getEstimatedMarginExposure( + address loanToken, + address collateralToken, + uint256 loanTokenSent, + uint256 collateralTokenSent, + uint256 interestRate, + uint256 newPrincipal + ) external view returns (uint256); - function getRequiredCollateral( - address loanToken, - address collateralToken, - uint256 newPrincipal, - uint256 marginAmount, - bool isTorqueLoan - ) external view returns (uint256 collateralAmountRequired); + function getRequiredCollateral( + address loanToken, + address collateralToken, + uint256 newPrincipal, + uint256 marginAmount, + bool isTorqueLoan + ) external view returns (uint256 collateralAmountRequired); - function getBorrowAmount( - address loanToken, - address collateralToken, - uint256 collateralTokenAmount, - uint256 marginAmount, - bool isTorqueLoan - ) external view returns (uint256 borrowAmount); + function getBorrowAmount( + address loanToken, + address collateralToken, + uint256 collateralTokenAmount, + uint256 marginAmount, + bool isTorqueLoan + ) external view returns (uint256 borrowAmount); - function isLoanPool(address loanPool) external view returns (bool); + function isLoanPool(address loanPool) external view returns (bool); - function lendingFeePercent() external view returns (uint256); + function lendingFeePercent() external view returns (uint256); - function getSwapExpectedReturn( - address sourceToken, - address destToken, - uint256 sourceTokenAmount - ) external view returns (uint256); + function getSwapExpectedReturn( + address sourceToken, + address destToken, + uint256 sourceTokenAmount + ) external view returns (uint256); } diff --git a/contracts/connectors/loantoken/interfaces/ProtocolSettingsLike.sol b/contracts/connectors/loantoken/interfaces/ProtocolSettingsLike.sol index 65009b986..80ccd0499 100644 --- a/contracts/connectors/loantoken/interfaces/ProtocolSettingsLike.sol +++ b/contracts/connectors/loantoken/interfaces/ProtocolSettingsLike.sol @@ -9,9 +9,11 @@ pragma experimental ABIEncoderV2; import "../../../core/objects/LoanParamsStruct.sol"; interface ProtocolSettingsLike { - function setupLoanParams(LoanParamsStruct.LoanParams[] calldata loanParamsList) external returns (bytes32[] memory loanParamsIdList); + function setupLoanParams(LoanParamsStruct.LoanParams[] calldata loanParamsList) + external + returns (bytes32[] memory loanParamsIdList); - function disableLoanParams(bytes32[] calldata loanParamsIdList) external; + function disableLoanParams(bytes32[] calldata loanParamsIdList) external; - function minInitialMargin(bytes32 loanParamsId) external view returns (uint256); + function minInitialMargin(bytes32 loanParamsId) external view returns (uint256); } diff --git a/contracts/connectors/loantoken/modules/beaconLogicLM/LoanTokenLogicLM.sol b/contracts/connectors/loantoken/modules/beaconLogicLM/LoanTokenLogicLM.sol index 50d829705..d8baa9c18 100644 --- a/contracts/connectors/loantoken/modules/beaconLogicLM/LoanTokenLogicLM.sol +++ b/contracts/connectors/loantoken/modules/beaconLogicLM/LoanTokenLogicLM.sol @@ -4,108 +4,112 @@ pragma experimental ABIEncoderV2; import "../../LoanTokenLogicStandard.sol"; contract LoanTokenLogicLM is LoanTokenLogicStandard { - /** - * @notice This function is MANDATORY, which will be called by LoanTokenLogicBeacon and be registered. - * Every new public function, the signature needs to be included in this function. - * - * @dev This function will return the list of function signature in this contract that are available for public call - * Then this function will be called by LoanTokenLogicBeacon, and the function signatures will be registred in LoanTokenLogicBeacon. - * @dev To save the gas we can just directly return the list of function signature from this pure function. - * The other workaround (fancy way) is we can create a storage for the list of the function signature, and then we can store each function signature to that storage from the constructor. - * Then, in this function we just need to return that storage variable. - * - * @return The list of function signatures (bytes4[]) - */ - function getListFunctionSignatures() external pure returns (bytes4[] memory functionSignatures, bytes32 moduleName) { - bytes4[] memory res = new bytes4[](36); + /** + * @notice This function is MANDATORY, which will be called by LoanTokenLogicBeacon and be registered. + * Every new public function, the signature needs to be included in this function. + * + * @dev This function will return the list of function signature in this contract that are available for public call + * Then this function will be called by LoanTokenLogicBeacon, and the function signatures will be registred in LoanTokenLogicBeacon. + * @dev To save the gas we can just directly return the list of function signature from this pure function. + * The other workaround (fancy way) is we can create a storage for the list of the function signature, and then we can store each function signature to that storage from the constructor. + * Then, in this function we just need to return that storage variable. + * + * @return The list of function signatures (bytes4[]) + */ + function getListFunctionSignatures() + external + pure + returns (bytes4[] memory functionSignatures, bytes32 moduleName) + { + bytes4[] memory res = new bytes4[](36); - // Loan Token Logic Standard - res[0] = this.borrow.selector; - res[1] = this.marginTrade.selector; - res[2] = this.marginTradeAffiliate.selector; - res[3] = this.transfer.selector; - res[4] = this.transferFrom.selector; - res[5] = this.profitOf.selector; - res[6] = this.tokenPrice.selector; - res[7] = this.checkpointPrice.selector; - res[8] = this.marketLiquidity.selector; - res[9] = this.avgBorrowInterestRate.selector; - res[10] = this.borrowInterestRate.selector; - res[11] = this.nextBorrowInterestRate.selector; - res[12] = this.supplyInterestRate.selector; - res[13] = this.nextSupplyInterestRate.selector; - res[14] = this.totalSupplyInterestRate.selector; - res[15] = this.totalAssetBorrow.selector; - res[16] = this.totalAssetSupply.selector; - res[17] = this.getMaxEscrowAmount.selector; - res[18] = this.assetBalanceOf.selector; - res[19] = this.getEstimatedMarginDetails.selector; - res[20] = this.getDepositAmountForBorrow.selector; - res[21] = this.getBorrowAmountForDeposit.selector; - res[22] = this.checkPriceDivergence.selector; - res[23] = this.checkPause.selector; - res[24] = this.setLiquidityMiningAddress.selector; - res[25] = this.calculateSupplyInterestRate.selector; + // Loan Token Logic Standard + res[0] = this.borrow.selector; + res[1] = this.marginTrade.selector; + res[2] = this.marginTradeAffiliate.selector; + res[3] = this.transfer.selector; + res[4] = this.transferFrom.selector; + res[5] = this.profitOf.selector; + res[6] = this.tokenPrice.selector; + res[7] = this.checkpointPrice.selector; + res[8] = this.marketLiquidity.selector; + res[9] = this.avgBorrowInterestRate.selector; + res[10] = this.borrowInterestRate.selector; + res[11] = this.nextBorrowInterestRate.selector; + res[12] = this.supplyInterestRate.selector; + res[13] = this.nextSupplyInterestRate.selector; + res[14] = this.totalSupplyInterestRate.selector; + res[15] = this.totalAssetBorrow.selector; + res[16] = this.totalAssetSupply.selector; + res[17] = this.getMaxEscrowAmount.selector; + res[18] = this.assetBalanceOf.selector; + res[19] = this.getEstimatedMarginDetails.selector; + res[20] = this.getDepositAmountForBorrow.selector; + res[21] = this.getBorrowAmountForDeposit.selector; + res[22] = this.checkPriceDivergence.selector; + res[23] = this.checkPause.selector; + res[24] = this.setLiquidityMiningAddress.selector; + res[25] = this.calculateSupplyInterestRate.selector; - // Loan Token LM & OVERLOADING function - /** - * @notice BE CAREFUL, - * LoanTokenLogicStandard also has mint & burn function (overloading). - * You need to compute the function signature manually --> bytes4(keccak256("mint(address,uint256,bool)")) - */ - res[26] = bytes4(keccak256("mint(address,uint256)")); /// LoanTokenLogicStandard - res[27] = bytes4(keccak256("mint(address,uint256,bool)")); /// LoanTokenLogicLM - res[28] = bytes4(keccak256("burn(address,uint256)")); /// LoanTokenLogicStandard - res[29] = bytes4(keccak256("burn(address,uint256,bool)")); /// LoanTokenLogicLM + // Loan Token LM & OVERLOADING function + /** + * @notice BE CAREFUL, + * LoanTokenLogicStandard also has mint & burn function (overloading). + * You need to compute the function signature manually --> bytes4(keccak256("mint(address,uint256,bool)")) + */ + res[26] = bytes4(keccak256("mint(address,uint256)")); /// LoanTokenLogicStandard + res[27] = bytes4(keccak256("mint(address,uint256,bool)")); /// LoanTokenLogicLM + res[28] = bytes4(keccak256("burn(address,uint256)")); /// LoanTokenLogicStandard + res[29] = bytes4(keccak256("burn(address,uint256,bool)")); /// LoanTokenLogicLM - // Advanced Token - res[30] = this.approve.selector; + // Advanced Token + res[30] = this.approve.selector; - // Advanced Token Storage - res[31] = this.totalSupply.selector; - res[32] = this.balanceOf.selector; - res[33] = this.allowance.selector; + // Advanced Token Storage + res[31] = this.totalSupply.selector; + res[32] = this.balanceOf.selector; + res[33] = this.allowance.selector; - // Loan Token Logic Storage Additional Variable - res[34] = this.getLiquidityMiningAddress.selector; - res[35] = this.withdrawRBTCTo.selector; + // Loan Token Logic Storage Additional Variable + res[34] = this.getLiquidityMiningAddress.selector; + res[35] = this.withdrawRBTCTo.selector; - return (res, stringToBytes32("LoanTokenLogicLM")); - } + return (res, stringToBytes32("LoanTokenLogicLM")); + } - /** - * @notice deposit into the lending pool and optionally participate at the Liquidity Mining Program - * @param receiver the receiver of the tokens - * @param depositAmount The amount of underlying tokens provided on the loan. - * (Not the number of loan tokens to mint). - * @param useLM if true -> deposit the pool tokens into the Liquidity Mining contract - */ - function mint( - address receiver, - uint256 depositAmount, - bool useLM - ) external nonReentrant returns (uint256 minted) { - if (useLM) return _mintWithLM(receiver, depositAmount); - else return _mintToken(receiver, depositAmount); - } + /** + * @notice deposit into the lending pool and optionally participate at the Liquidity Mining Program + * @param receiver the receiver of the tokens + * @param depositAmount The amount of underlying tokens provided on the loan. + * (Not the number of loan tokens to mint). + * @param useLM if true -> deposit the pool tokens into the Liquidity Mining contract + */ + function mint( + address receiver, + uint256 depositAmount, + bool useLM + ) external nonReentrant returns (uint256 minted) { + if (useLM) return _mintWithLM(receiver, depositAmount); + else return _mintToken(receiver, depositAmount); + } - /** - * @notice withdraws from the lending pool and optionally retrieves the pool tokens from the - * Liquidity Mining Contract - * @param receiver the receiver of the underlying tokens. note: potetial LM rewards are always sent to the msg.sender - * @param burnAmount The amount of pool tokens to redeem. - * @param useLM if true -> deposit the pool tokens into the Liquidity Mining contract - */ - function burn( - address receiver, - uint256 burnAmount, - bool useLM - ) external nonReentrant returns (uint256 redeemed) { - if (useLM) redeemed = _burnFromLM(burnAmount); - else redeemed = _burnToken(burnAmount); - //this needs to be here and not in _burnTokens because of the WRBTC implementation - if (redeemed != 0) { - _safeTransfer(loanTokenAddress, receiver, redeemed, "asset transfer failed"); - } - } + /** + * @notice withdraws from the lending pool and optionally retrieves the pool tokens from the + * Liquidity Mining Contract + * @param receiver the receiver of the underlying tokens. note: potetial LM rewards are always sent to the msg.sender + * @param burnAmount The amount of pool tokens to redeem. + * @param useLM if true -> deposit the pool tokens into the Liquidity Mining contract + */ + function burn( + address receiver, + uint256 burnAmount, + bool useLM + ) external nonReentrant returns (uint256 redeemed) { + if (useLM) redeemed = _burnFromLM(burnAmount); + else redeemed = _burnToken(burnAmount); + //this needs to be here and not in _burnTokens because of the WRBTC implementation + if (redeemed != 0) { + _safeTransfer(loanTokenAddress, receiver, redeemed, "asset transfer failed"); + } + } } diff --git a/contracts/connectors/loantoken/modules/beaconLogicWRBTC/LoanTokenLogicWrbtc.sol b/contracts/connectors/loantoken/modules/beaconLogicWRBTC/LoanTokenLogicWrbtc.sol index ce536e895..54c326239 100644 --- a/contracts/connectors/loantoken/modules/beaconLogicWRBTC/LoanTokenLogicWrbtc.sol +++ b/contracts/connectors/loantoken/modules/beaconLogicWRBTC/LoanTokenLogicWrbtc.sol @@ -9,152 +9,178 @@ pragma experimental ABIEncoderV2; import "../../LoanTokenLogicStandard.sol"; contract LoanTokenLogicWrbtc is LoanTokenLogicStandard { - /** - * @notice This function is MANDATORY, which will be called by LoanTokenLogicBeacon and be registered. - * Every new public function, the sginature needs to be included in this function. - * - * @dev This function will return the list of function signature in this contract that are available for public call - * Then this function will be called by LoanTokenLogicBeacon, and the function signatures will be registred in LoanTokenLogicBeacon. - * @dev To save the gas we can just directly return the list of function signature from this pure function. - * The other workaround (fancy way) is we can create a storage for the list of the function signature, and then we can store each function signature to that storage from the constructor. - * Then, in this function we just need to return that storage variable. - * - * @return The list of function signatures (bytes4[]) - */ - function getListFunctionSignatures() external pure returns (bytes4[] memory functionSignatures, bytes32 moduleName) { - bytes4[] memory res = new bytes4[](36); - - // Loan Token Logic Standard - res[0] = this.mint.selector; - res[1] = this.burn.selector; - res[2] = this.borrow.selector; - res[3] = this.marginTrade.selector; - res[4] = this.marginTradeAffiliate.selector; - res[5] = this.transfer.selector; - res[6] = this.transferFrom.selector; - res[7] = this.profitOf.selector; - res[8] = this.tokenPrice.selector; - res[9] = this.checkpointPrice.selector; - res[10] = this.marketLiquidity.selector; - res[11] = this.avgBorrowInterestRate.selector; - res[12] = this.borrowInterestRate.selector; - res[13] = this.nextBorrowInterestRate.selector; - res[14] = this.supplyInterestRate.selector; - res[15] = this.nextSupplyInterestRate.selector; - res[16] = this.totalSupplyInterestRate.selector; - res[17] = this.totalAssetBorrow.selector; - res[18] = this.totalAssetSupply.selector; - res[19] = this.getMaxEscrowAmount.selector; - res[20] = this.assetBalanceOf.selector; - res[21] = this.getEstimatedMarginDetails.selector; - res[22] = this.getDepositAmountForBorrow.selector; - res[23] = this.getBorrowAmountForDeposit.selector; - res[24] = this.checkPriceDivergence.selector; - res[25] = this.checkPause.selector; - res[26] = this.setLiquidityMiningAddress.selector; - res[27] = this.calculateSupplyInterestRate.selector; - - // Loan Token WRBTC - res[28] = this.mintWithBTC.selector; - res[29] = this.burnToBTC.selector; - - // Advanced Token - res[30] = this.approve.selector; - - // Advanced Token Storage - res[31] = this.totalSupply.selector; - res[32] = this.balanceOf.selector; - res[33] = this.allowance.selector; - - // Loan Token Logic Storage Additional Variable - res[34] = this.getLiquidityMiningAddress.selector; - res[35] = this.withdrawRBTCTo.selector; - - return (res, stringToBytes32("LoanTokenLogicWrbtc")); - } - - function mintWithBTC(address receiver, bool useLM) external payable nonReentrant returns (uint256 mintAmount) { - if (useLM) return _mintWithLM(receiver, msg.value); - else return _mintToken(receiver, msg.value); - } - - function burnToBTC( - address receiver, - uint256 burnAmount, - bool useLM - ) external nonReentrant returns (uint256 loanAmountPaid) { - if (useLM) loanAmountPaid = _burnFromLM(burnAmount); - else loanAmountPaid = _burnToken(burnAmount); - - if (loanAmountPaid != 0) { - IWrbtcERC20(wrbtcTokenAddress).withdraw(loanAmountPaid); - Address.sendValue(receiver, loanAmountPaid); - } - } - - /* Internal functions */ - - /** - * @notice Handle transfers prior to adding newPrincipal to loanTokenSent. - * - * @param collateralTokenAddress The address of the collateral token. - * @param sentAddresses The array of addresses: - * sentAddresses[0]: lender - * sentAddresses[1]: borrower - * sentAddresses[2]: receiver - * sentAddresses[3]: manager - * - * @param sentAmounts The array of amounts: - * sentAmounts[0]: interestRate - * sentAmounts[1]: newPrincipal - * sentAmounts[2]: interestInitialAmount - * sentAmounts[3]: loanTokenSent - * sentAmounts[4]: collateralTokenSent - * - * @param withdrawalAmount The amount to withdraw. - * - * @return msgValue The amount of value sent. - * */ - function _verifyTransfers( - address collateralTokenAddress, - address[4] memory sentAddresses, - uint256[5] memory sentAmounts, - uint256 withdrawalAmount - ) internal returns (uint256 msgValue) { - address _wrbtcToken = wrbtcTokenAddress; - address _loanTokenAddress = _wrbtcToken; - address receiver = sentAddresses[2]; - uint256 newPrincipal = sentAmounts[1]; - uint256 loanTokenSent = sentAmounts[3]; - uint256 collateralTokenSent = sentAmounts[4]; - - require(_loanTokenAddress != collateralTokenAddress, "26"); - - msgValue = msg.value; - - if (withdrawalAmount != 0) { - /// withdrawOnOpen == true - IWrbtcERC20(_wrbtcToken).withdraw(withdrawalAmount); - Address.sendValue(receiver, withdrawalAmount); - if (newPrincipal > withdrawalAmount) { - _safeTransfer(_loanTokenAddress, sovrynContractAddress, newPrincipal - withdrawalAmount, ""); - } - } else { - _safeTransfer(_loanTokenAddress, sovrynContractAddress, newPrincipal, "27"); - } - - if (collateralTokenSent != 0) { - _safeTransferFrom(collateralTokenAddress, msg.sender, sovrynContractAddress, collateralTokenSent, "28"); - } - - if (loanTokenSent != 0) { - if (msgValue != 0 && msgValue >= loanTokenSent) { - IWrbtc(_wrbtcToken).deposit.value(loanTokenSent)(); - _safeTransfer(_loanTokenAddress, sovrynContractAddress, loanTokenSent, "29"); - msgValue -= loanTokenSent; - } else { - _safeTransferFrom(_loanTokenAddress, msg.sender, sovrynContractAddress, loanTokenSent, "29"); - } - } - } + /** + * @notice This function is MANDATORY, which will be called by LoanTokenLogicBeacon and be registered. + * Every new public function, the sginature needs to be included in this function. + * + * @dev This function will return the list of function signature in this contract that are available for public call + * Then this function will be called by LoanTokenLogicBeacon, and the function signatures will be registred in LoanTokenLogicBeacon. + * @dev To save the gas we can just directly return the list of function signature from this pure function. + * The other workaround (fancy way) is we can create a storage for the list of the function signature, and then we can store each function signature to that storage from the constructor. + * Then, in this function we just need to return that storage variable. + * + * @return The list of function signatures (bytes4[]) + */ + function getListFunctionSignatures() + external + pure + returns (bytes4[] memory functionSignatures, bytes32 moduleName) + { + bytes4[] memory res = new bytes4[](36); + + // Loan Token Logic Standard + res[0] = this.mint.selector; + res[1] = this.burn.selector; + res[2] = this.borrow.selector; + res[3] = this.marginTrade.selector; + res[4] = this.marginTradeAffiliate.selector; + res[5] = this.transfer.selector; + res[6] = this.transferFrom.selector; + res[7] = this.profitOf.selector; + res[8] = this.tokenPrice.selector; + res[9] = this.checkpointPrice.selector; + res[10] = this.marketLiquidity.selector; + res[11] = this.avgBorrowInterestRate.selector; + res[12] = this.borrowInterestRate.selector; + res[13] = this.nextBorrowInterestRate.selector; + res[14] = this.supplyInterestRate.selector; + res[15] = this.nextSupplyInterestRate.selector; + res[16] = this.totalSupplyInterestRate.selector; + res[17] = this.totalAssetBorrow.selector; + res[18] = this.totalAssetSupply.selector; + res[19] = this.getMaxEscrowAmount.selector; + res[20] = this.assetBalanceOf.selector; + res[21] = this.getEstimatedMarginDetails.selector; + res[22] = this.getDepositAmountForBorrow.selector; + res[23] = this.getBorrowAmountForDeposit.selector; + res[24] = this.checkPriceDivergence.selector; + res[25] = this.checkPause.selector; + res[26] = this.setLiquidityMiningAddress.selector; + res[27] = this.calculateSupplyInterestRate.selector; + + // Loan Token WRBTC + res[28] = this.mintWithBTC.selector; + res[29] = this.burnToBTC.selector; + + // Advanced Token + res[30] = this.approve.selector; + + // Advanced Token Storage + res[31] = this.totalSupply.selector; + res[32] = this.balanceOf.selector; + res[33] = this.allowance.selector; + + // Loan Token Logic Storage Additional Variable + res[34] = this.getLiquidityMiningAddress.selector; + res[35] = this.withdrawRBTCTo.selector; + + return (res, stringToBytes32("LoanTokenLogicWrbtc")); + } + + function mintWithBTC(address receiver, bool useLM) + external + payable + nonReentrant + returns (uint256 mintAmount) + { + if (useLM) return _mintWithLM(receiver, msg.value); + else return _mintToken(receiver, msg.value); + } + + function burnToBTC( + address receiver, + uint256 burnAmount, + bool useLM + ) external nonReentrant returns (uint256 loanAmountPaid) { + if (useLM) loanAmountPaid = _burnFromLM(burnAmount); + else loanAmountPaid = _burnToken(burnAmount); + + if (loanAmountPaid != 0) { + IWrbtcERC20(wrbtcTokenAddress).withdraw(loanAmountPaid); + Address.sendValue(receiver, loanAmountPaid); + } + } + + /* Internal functions */ + + /** + * @notice Handle transfers prior to adding newPrincipal to loanTokenSent. + * + * @param collateralTokenAddress The address of the collateral token. + * @param sentAddresses The array of addresses: + * sentAddresses[0]: lender + * sentAddresses[1]: borrower + * sentAddresses[2]: receiver + * sentAddresses[3]: manager + * + * @param sentAmounts The array of amounts: + * sentAmounts[0]: interestRate + * sentAmounts[1]: newPrincipal + * sentAmounts[2]: interestInitialAmount + * sentAmounts[3]: loanTokenSent + * sentAmounts[4]: collateralTokenSent + * + * @param withdrawalAmount The amount to withdraw. + * + * @return msgValue The amount of value sent. + * */ + function _verifyTransfers( + address collateralTokenAddress, + address[4] memory sentAddresses, + uint256[5] memory sentAmounts, + uint256 withdrawalAmount + ) internal returns (uint256 msgValue) { + address _wrbtcToken = wrbtcTokenAddress; + address _loanTokenAddress = _wrbtcToken; + address receiver = sentAddresses[2]; + uint256 newPrincipal = sentAmounts[1]; + uint256 loanTokenSent = sentAmounts[3]; + uint256 collateralTokenSent = sentAmounts[4]; + + require(_loanTokenAddress != collateralTokenAddress, "26"); + + msgValue = msg.value; + + if (withdrawalAmount != 0) { + /// withdrawOnOpen == true + IWrbtcERC20(_wrbtcToken).withdraw(withdrawalAmount); + Address.sendValue(receiver, withdrawalAmount); + if (newPrincipal > withdrawalAmount) { + _safeTransfer( + _loanTokenAddress, + sovrynContractAddress, + newPrincipal - withdrawalAmount, + "" + ); + } + } else { + _safeTransfer(_loanTokenAddress, sovrynContractAddress, newPrincipal, "27"); + } + + if (collateralTokenSent != 0) { + _safeTransferFrom( + collateralTokenAddress, + msg.sender, + sovrynContractAddress, + collateralTokenSent, + "28" + ); + } + + if (loanTokenSent != 0) { + if (msgValue != 0 && msgValue >= loanTokenSent) { + IWrbtc(_wrbtcToken).deposit.value(loanTokenSent)(); + _safeTransfer(_loanTokenAddress, sovrynContractAddress, loanTokenSent, "29"); + msgValue -= loanTokenSent; + } else { + _safeTransferFrom( + _loanTokenAddress, + msg.sender, + sovrynContractAddress, + loanTokenSent, + "29" + ); + } + } + } } diff --git a/contracts/connectors/loantoken/modules/shared/LoanTokenSettingsLowerAdmin.sol b/contracts/connectors/loantoken/modules/shared/LoanTokenSettingsLowerAdmin.sol index 92c89a916..f443b0c1f 100644 --- a/contracts/connectors/loantoken/modules/shared/LoanTokenSettingsLowerAdmin.sol +++ b/contracts/connectors/loantoken/modules/shared/LoanTokenSettingsLowerAdmin.sol @@ -11,221 +11,246 @@ import "../../interfaces/ProtocolSettingsLike.sol"; import "../../LoanTokenLogicStorage.sol"; contract LoanTokenSettingsLowerAdmin is LoanTokenLogicStorage { - using SafeMath for uint256; - - /// @dev TODO: Check for restrictions in this contract. - modifier onlyAdmin() { - require(isOwner() || msg.sender == admin, "unauthorized"); - _; - } - - /* Events */ - - event SetTransactionLimits(address[] addresses, uint256[] limits); - event ToggledFunctionPaused(string functionId, bool prevFlag, bool newFlag); - - /* Functions */ - - /** - * @notice This function is MANDATORY, which will be called by LoanTokenLogicBeacon and be registered. - * Every new public function, the sginature needs to be included in this function. - * - * @dev This function will return the list of function signature in this contract that are available for public call - * Then this function will be called by LoanTokenLogicBeacon, and the function signatures will be registred in LoanTokenLogicBeacon. - * @dev To save the gas we can just directly return the list of function signature from this pure function. - * The other workaround (fancy way) is we can create a storage for the list of the function signature, and then we can store each function signature to that storage from the constructor. - * Then, in this function we just need to return that storage variable. - * - * @return The list of function signatures (bytes4[]) - */ - function getListFunctionSignatures() external pure returns (bytes4[] memory functionSignatures, bytes32 moduleName) { - bytes4[] memory res = new bytes4[](9); - res[0] = this.setAdmin.selector; - res[1] = this.setPauser.selector; - res[2] = this.setupLoanParams.selector; - res[3] = this.disableLoanParams.selector; - res[4] = this.setDemandCurve.selector; - res[5] = this.toggleFunctionPause.selector; - res[6] = this.setTransactionLimits.selector; - res[7] = this.changeLoanTokenNameAndSymbol.selector; - res[8] = this.pauser.selector; - return (res, stringToBytes32("LoanTokenSettingsLowerAdmin")); - } - - /** - * @notice Set admin account. - * @param _admin The address of the account to grant admin permissions. - * */ - function setAdmin(address _admin) public onlyOwner { - admin = _admin; - } - - /** - * @notice Set pauser account. - * @param _pauser The address of the account to grant pause permissions. - * */ - function setPauser(address _pauser) public onlyOwner { - pauser = _pauser; - } - - /** - * @notice Fallback function not allowed - * */ - function() external { - revert("LoanTokenSettingsLowerAdmin - fallback not allowed"); - } - - /** - * @notice Set loan token parameters. - * - * @param loanParamsList The array of loan parameters. - * @param areTorqueLoans Whether the loan is a torque loan. - * */ - function setupLoanParams(LoanParamsStruct.LoanParams[] memory loanParamsList, bool areTorqueLoans) public onlyAdmin { - bytes32[] memory loanParamsIdList; - address _loanTokenAddress = loanTokenAddress; - - for (uint256 i = 0; i < loanParamsList.length; i++) { - loanParamsList[i].loanToken = _loanTokenAddress; - loanParamsList[i].maxLoanTerm = areTorqueLoans ? 0 : 28 days; - } - - loanParamsIdList = ProtocolSettingsLike(sovrynContractAddress).setupLoanParams(loanParamsList); - for (uint256 i = 0; i < loanParamsIdList.length; i++) { - loanParamsIds[ - uint256( - keccak256( - abi.encodePacked( - loanParamsList[i].collateralToken, - areTorqueLoans /// isTorqueLoan - ) - ) - ) - ] = loanParamsIdList[i]; - } - } - - /** - * @notice Disable loan token parameters. - * - * @param collateralTokens The array of collateral tokens. - * @param isTorqueLoans Whether the loan is a torque loan. - * */ - function disableLoanParams(address[] calldata collateralTokens, bool[] calldata isTorqueLoans) external onlyAdmin { - require(collateralTokens.length == isTorqueLoans.length, "count mismatch"); - - bytes32[] memory loanParamsIdList = new bytes32[](collateralTokens.length); - for (uint256 i = 0; i < collateralTokens.length; i++) { - uint256 id = uint256(keccak256(abi.encodePacked(collateralTokens[i], isTorqueLoans[i]))); - loanParamsIdList[i] = loanParamsIds[id]; - delete loanParamsIds[id]; - } - - ProtocolSettingsLike(sovrynContractAddress).disableLoanParams(loanParamsIdList); - } - - /** - * @notice Set loan token parameters about the demand curve. - * - * @dev These params should be percentages represented - * like so: 5% = 5000000000000000000 /// 18 digits precision. - * rateMultiplier + baseRate can't exceed 100% - * - * To maintain a healthy credit score, it's important to keep your - * credit utilization rate (CUR) low (_lowUtilBaseRate). In general - * you don't want your CUR to exceed 30%, but increasingly financial - * experts are recommending that you don't want to go above 10% if you - * really want an excellent credit score. - * - * Interest rates tend to cluster around the kink level of a kinked - * interest rate model. More info at https://arxiv.org/pdf/2006.13922.pdf - * and https://compound.finance/governance/proposals/12 - * - * @param _baseRate The interest rate. - * @param _rateMultiplier The precision multiplier for base rate. - * @param _lowUtilBaseRate The credit utilization rate (CUR) low value. - * @param _lowUtilRateMultiplier The precision multiplier for low util base rate. - * @param _targetLevel The target level. - * @param _kinkLevel The level that interest rates cluster on kinked model. - * @param _maxScaleRate The maximum rate of the scale. - * */ - function setDemandCurve( - uint256 _baseRate, - uint256 _rateMultiplier, - uint256 _lowUtilBaseRate, - uint256 _lowUtilRateMultiplier, - uint256 _targetLevel, - uint256 _kinkLevel, - uint256 _maxScaleRate - ) public onlyAdmin { - require(_rateMultiplier.add(_baseRate) <= WEI_PERCENT_PRECISION, "curve params too high"); - require(_lowUtilRateMultiplier.add(_lowUtilBaseRate) <= WEI_PERCENT_PRECISION, "curve params too high"); - - require(_targetLevel <= WEI_PERCENT_PRECISION && _kinkLevel <= WEI_PERCENT_PRECISION, "levels too high"); - - baseRate = _baseRate; - rateMultiplier = _rateMultiplier; - lowUtilBaseRate = _lowUtilBaseRate; - lowUtilRateMultiplier = _lowUtilRateMultiplier; - - targetLevel = _targetLevel; /// 80 ether - kinkLevel = _kinkLevel; /// 90 ether - maxScaleRate = _maxScaleRate; /// 100 ether - } - - /** - * @notice Set the pause flag for a function to true or false. - * - * @dev Combining the hash of "iToken_FunctionPause" string and a function - * selector gets a slot to write a flag for pause state. - * - * @param funcId The ID of a function, the selector. - * @param isPaused true/false value of the flag. - * */ - function toggleFunctionPause( - string memory funcId, /// example: "mint(uint256,uint256)" - bool isPaused - ) public { - bool paused; - require(msg.sender == pauser, "onlyPauser"); - /// keccak256("iToken_FunctionPause") - bytes32 slot = - keccak256( - abi.encodePacked( - bytes4(keccak256(abi.encodePacked(funcId))), - uint256(0xd46a704bc285dbd6ff5ad3863506260b1df02812f4f857c8cc852317a6ac64f2) - ) - ); - assembly { - paused := sload(slot) - } - require(paused != isPaused, "isPaused is already set to that value"); - assembly { - sstore(slot, isPaused) - } - emit ToggledFunctionPaused(funcId, !isPaused, isPaused); - } - - /** - * Set the transaction limit per token address. - * @param addresses The token addresses. - * @param limits The limit denominated in the currency of the token address. - * */ - function setTransactionLimits(address[] memory addresses, uint256[] memory limits) public onlyAdmin { - require(addresses.length == limits.length, "mismatched array lengths"); - for (uint256 i = 0; i < addresses.length; i++) { - transactionLimit[addresses[i]] = limits[i]; - } - emit SetTransactionLimits(addresses, limits); - } - - /** - * @notice Update the loan token parameters. - * @param _name The new name of the loan token. - * @param _symbol The new symbol of the loan token. - * */ - function changeLoanTokenNameAndSymbol(string memory _name, string memory _symbol) public onlyAdmin { - name = _name; - symbol = _symbol; - } + using SafeMath for uint256; + + /// @dev TODO: Check for restrictions in this contract. + modifier onlyAdmin() { + require(isOwner() || msg.sender == admin, "unauthorized"); + _; + } + + /* Events */ + + event SetTransactionLimits(address[] addresses, uint256[] limits); + event ToggledFunctionPaused(string functionId, bool prevFlag, bool newFlag); + + /* Functions */ + + /** + * @notice This function is MANDATORY, which will be called by LoanTokenLogicBeacon and be registered. + * Every new public function, the sginature needs to be included in this function. + * + * @dev This function will return the list of function signature in this contract that are available for public call + * Then this function will be called by LoanTokenLogicBeacon, and the function signatures will be registred in LoanTokenLogicBeacon. + * @dev To save the gas we can just directly return the list of function signature from this pure function. + * The other workaround (fancy way) is we can create a storage for the list of the function signature, and then we can store each function signature to that storage from the constructor. + * Then, in this function we just need to return that storage variable. + * + * @return The list of function signatures (bytes4[]) + */ + function getListFunctionSignatures() + external + pure + returns (bytes4[] memory functionSignatures, bytes32 moduleName) + { + bytes4[] memory res = new bytes4[](9); + res[0] = this.setAdmin.selector; + res[1] = this.setPauser.selector; + res[2] = this.setupLoanParams.selector; + res[3] = this.disableLoanParams.selector; + res[4] = this.setDemandCurve.selector; + res[5] = this.toggleFunctionPause.selector; + res[6] = this.setTransactionLimits.selector; + res[7] = this.changeLoanTokenNameAndSymbol.selector; + res[8] = this.pauser.selector; + return (res, stringToBytes32("LoanTokenSettingsLowerAdmin")); + } + + /** + * @notice Set admin account. + * @param _admin The address of the account to grant admin permissions. + * */ + function setAdmin(address _admin) public onlyOwner { + admin = _admin; + } + + /** + * @notice Set pauser account. + * @param _pauser The address of the account to grant pause permissions. + * */ + function setPauser(address _pauser) public onlyOwner { + pauser = _pauser; + } + + /** + * @notice Fallback function not allowed + * */ + function() external { + revert("LoanTokenSettingsLowerAdmin - fallback not allowed"); + } + + /** + * @notice Set loan token parameters. + * + * @param loanParamsList The array of loan parameters. + * @param areTorqueLoans Whether the loan is a torque loan. + * */ + function setupLoanParams( + LoanParamsStruct.LoanParams[] memory loanParamsList, + bool areTorqueLoans + ) public onlyAdmin { + bytes32[] memory loanParamsIdList; + address _loanTokenAddress = loanTokenAddress; + + for (uint256 i = 0; i < loanParamsList.length; i++) { + loanParamsList[i].loanToken = _loanTokenAddress; + loanParamsList[i].maxLoanTerm = areTorqueLoans ? 0 : 28 days; + } + + loanParamsIdList = ProtocolSettingsLike(sovrynContractAddress).setupLoanParams( + loanParamsList + ); + for (uint256 i = 0; i < loanParamsIdList.length; i++) { + loanParamsIds[ + uint256( + keccak256( + abi.encodePacked( + loanParamsList[i].collateralToken, + areTorqueLoans /// isTorqueLoan + ) + ) + ) + ] = loanParamsIdList[i]; + } + } + + /** + * @notice Disable loan token parameters. + * + * @param collateralTokens The array of collateral tokens. + * @param isTorqueLoans Whether the loan is a torque loan. + * */ + function disableLoanParams(address[] calldata collateralTokens, bool[] calldata isTorqueLoans) + external + onlyAdmin + { + require(collateralTokens.length == isTorqueLoans.length, "count mismatch"); + + bytes32[] memory loanParamsIdList = new bytes32[](collateralTokens.length); + for (uint256 i = 0; i < collateralTokens.length; i++) { + uint256 id = + uint256(keccak256(abi.encodePacked(collateralTokens[i], isTorqueLoans[i]))); + loanParamsIdList[i] = loanParamsIds[id]; + delete loanParamsIds[id]; + } + + ProtocolSettingsLike(sovrynContractAddress).disableLoanParams(loanParamsIdList); + } + + /** + * @notice Set loan token parameters about the demand curve. + * + * @dev These params should be percentages represented + * like so: 5% = 5000000000000000000 /// 18 digits precision. + * rateMultiplier + baseRate can't exceed 100% + * + * To maintain a healthy credit score, it's important to keep your + * credit utilization rate (CUR) low (_lowUtilBaseRate). In general + * you don't want your CUR to exceed 30%, but increasingly financial + * experts are recommending that you don't want to go above 10% if you + * really want an excellent credit score. + * + * Interest rates tend to cluster around the kink level of a kinked + * interest rate model. More info at https://arxiv.org/pdf/2006.13922.pdf + * and https://compound.finance/governance/proposals/12 + * + * @param _baseRate The interest rate. + * @param _rateMultiplier The precision multiplier for base rate. + * @param _lowUtilBaseRate The credit utilization rate (CUR) low value. + * @param _lowUtilRateMultiplier The precision multiplier for low util base rate. + * @param _targetLevel The target level. + * @param _kinkLevel The level that interest rates cluster on kinked model. + * @param _maxScaleRate The maximum rate of the scale. + * */ + function setDemandCurve( + uint256 _baseRate, + uint256 _rateMultiplier, + uint256 _lowUtilBaseRate, + uint256 _lowUtilRateMultiplier, + uint256 _targetLevel, + uint256 _kinkLevel, + uint256 _maxScaleRate + ) public onlyAdmin { + require(_rateMultiplier.add(_baseRate) <= WEI_PERCENT_PRECISION, "curve params too high"); + require( + _lowUtilRateMultiplier.add(_lowUtilBaseRate) <= WEI_PERCENT_PRECISION, + "curve params too high" + ); + + require( + _targetLevel <= WEI_PERCENT_PRECISION && _kinkLevel <= WEI_PERCENT_PRECISION, + "levels too high" + ); + + baseRate = _baseRate; + rateMultiplier = _rateMultiplier; + lowUtilBaseRate = _lowUtilBaseRate; + lowUtilRateMultiplier = _lowUtilRateMultiplier; + + targetLevel = _targetLevel; /// 80 ether + kinkLevel = _kinkLevel; /// 90 ether + maxScaleRate = _maxScaleRate; /// 100 ether + } + + /** + * @notice Set the pause flag for a function to true or false. + * + * @dev Combining the hash of "iToken_FunctionPause" string and a function + * selector gets a slot to write a flag for pause state. + * + * @param funcId The ID of a function, the selector. + * @param isPaused true/false value of the flag. + * */ + function toggleFunctionPause( + string memory funcId, /// example: "mint(uint256,uint256)" + bool isPaused + ) public { + bool paused; + require(msg.sender == pauser, "onlyPauser"); + /// keccak256("iToken_FunctionPause") + bytes32 slot = + keccak256( + abi.encodePacked( + bytes4(keccak256(abi.encodePacked(funcId))), + uint256(0xd46a704bc285dbd6ff5ad3863506260b1df02812f4f857c8cc852317a6ac64f2) + ) + ); + assembly { + paused := sload(slot) + } + require(paused != isPaused, "isPaused is already set to that value"); + assembly { + sstore(slot, isPaused) + } + emit ToggledFunctionPaused(funcId, !isPaused, isPaused); + } + + /** + * Set the transaction limit per token address. + * @param addresses The token addresses. + * @param limits The limit denominated in the currency of the token address. + * */ + function setTransactionLimits(address[] memory addresses, uint256[] memory limits) + public + onlyAdmin + { + require(addresses.length == limits.length, "mismatched array lengths"); + for (uint256 i = 0; i < addresses.length; i++) { + transactionLimit[addresses[i]] = limits[i]; + } + emit SetTransactionLimits(addresses, limits); + } + + /** + * @notice Update the loan token parameters. + * @param _name The new name of the loan token. + * @param _symbol The new symbol of the loan token. + * */ + function changeLoanTokenNameAndSymbol(string memory _name, string memory _symbol) + public + onlyAdmin + { + name = _name; + symbol = _symbol; + } } diff --git a/contracts/core/Objects.sol b/contracts/core/Objects.sol index ac4d9ed71..429eb2e28 100644 --- a/contracts/core/Objects.sol +++ b/contracts/core/Objects.sol @@ -19,6 +19,12 @@ import "./objects/LoanInterestStruct.sol"; * This contract inherints and aggregates several structures needed to handle * loans on the protocol. * */ -contract Objects is LoanStruct, LoanParamsStruct, OrderStruct, LenderInterestStruct, LoanInterestStruct { +contract Objects is + LoanStruct, + LoanParamsStruct, + OrderStruct, + LenderInterestStruct, + LoanInterestStruct +{ } diff --git a/contracts/core/Protocol.sol b/contracts/core/Protocol.sol index 1fc2984c1..0638dc42f 100644 --- a/contracts/core/Protocol.sol +++ b/contracts/core/Protocol.sol @@ -20,63 +20,67 @@ import "./State.sol"; * https://eips.ethereum.org/EIPS/eip-1822 * */ contract sovrynProtocol is State { - /** - * @notice Fallback function performs a delegate call - * to the actual implementation address is pointing this proxy. - * Returns whatever the implementation call returns. - * */ - function() external payable { - if (gasleft() <= 2300) { - return; - } + /** + * @notice Fallback function performs a delegate call + * to the actual implementation address is pointing this proxy. + * Returns whatever the implementation call returns. + * */ + function() external payable { + if (gasleft() <= 2300) { + return; + } - address target = logicTargets[msg.sig]; - require(target != address(0), "target not active"); + address target = logicTargets[msg.sig]; + require(target != address(0), "target not active"); - bytes memory data = msg.data; - assembly { - let result := delegatecall(gas, target, add(data, 0x20), mload(data), 0, 0) - let size := returndatasize - let ptr := mload(0x40) - returndatacopy(ptr, 0, size) - switch result - case 0 { - revert(ptr, size) - } - default { - return(ptr, size) - } - } - } + bytes memory data = msg.data; + assembly { + let result := delegatecall(gas, target, add(data, 0x20), mload(data), 0, 0) + let size := returndatasize + let ptr := mload(0x40) + returndatacopy(ptr, 0, size) + switch result + case 0 { + revert(ptr, size) + } + default { + return(ptr, size) + } + } + } - /** - * @notice External owner target initializer. - * @param target The target addresses. - * */ - function replaceContract(address target) external onlyOwner { - (bool success, ) = target.delegatecall(abi.encodeWithSignature("initialize(address)", target)); - require(success, "setup failed"); - } + /** + * @notice External owner target initializer. + * @param target The target addresses. + * */ + function replaceContract(address target) external onlyOwner { + (bool success, ) = + target.delegatecall(abi.encodeWithSignature("initialize(address)", target)); + require(success, "setup failed"); + } - /** - * @notice External owner setter for target addresses. - * @param sigsArr The array of signatures. - * @param targetsArr The array of addresses. - * */ - function setTargets(string[] calldata sigsArr, address[] calldata targetsArr) external onlyOwner { - require(sigsArr.length == targetsArr.length, "count mismatch"); + /** + * @notice External owner setter for target addresses. + * @param sigsArr The array of signatures. + * @param targetsArr The array of addresses. + * */ + function setTargets(string[] calldata sigsArr, address[] calldata targetsArr) + external + onlyOwner + { + require(sigsArr.length == targetsArr.length, "count mismatch"); - for (uint256 i = 0; i < sigsArr.length; i++) { - _setTarget(bytes4(keccak256(abi.encodePacked(sigsArr[i]))), targetsArr[i]); - } - } + for (uint256 i = 0; i < sigsArr.length; i++) { + _setTarget(bytes4(keccak256(abi.encodePacked(sigsArr[i]))), targetsArr[i]); + } + } - /** - * @notice External getter for target addresses. - * @param sig The signature. - * @return The address for a given signature. - * */ - function getTarget(string calldata sig) external view returns (address) { - return logicTargets[bytes4(keccak256(abi.encodePacked(sig)))]; - } + /** + * @notice External getter for target addresses. + * @param sig The signature. + * @return The address for a given signature. + * */ + function getTarget(string calldata sig) external view returns (address) { + return logicTargets[bytes4(keccak256(abi.encodePacked(sig)))]; + } } diff --git a/contracts/core/State.sol b/contracts/core/State.sol index 074ecdeb6..60a09ae8b 100644 --- a/contracts/core/State.sol +++ b/contracts/core/State.sol @@ -21,206 +21,206 @@ import "../interfaces/IWrbtcERC20.sol"; * This contract contains the storage values of the Protocol. * */ contract State is Objects, ReentrancyGuard, Ownable { - using SafeMath for uint256; - using EnumerableAddressSet for EnumerableAddressSet.AddressSet; // enumerable map of addresses - using EnumerableBytes32Set for EnumerableBytes32Set.Bytes32Set; // enumerable map of bytes32 or addresses + using SafeMath for uint256; + using EnumerableAddressSet for EnumerableAddressSet.AddressSet; // enumerable map of addresses + using EnumerableBytes32Set for EnumerableBytes32Set.Bytes32Set; // enumerable map of bytes32 or addresses - /// Handles asset reference price lookups. - address public priceFeeds; + /// Handles asset reference price lookups. + address public priceFeeds; - /// Handles asset swaps using dex liquidity. - address public swapsImpl; + /// Handles asset swaps using dex liquidity. + address public swapsImpl; - /// Contract registry address of the Sovryn swap network. - address public sovrynSwapContractRegistryAddress; + /// Contract registry address of the Sovryn swap network. + address public sovrynSwapContractRegistryAddress; - /// Implementations of protocol functions. - mapping(bytes4 => address) public logicTargets; + /// Implementations of protocol functions. + mapping(bytes4 => address) public logicTargets; - /// Loans: loanId => Loan - mapping(bytes32 => Loan) public loans; + /// Loans: loanId => Loan + mapping(bytes32 => Loan) public loans; - /// Loan parameters: loanParamsId => LoanParams - mapping(bytes32 => LoanParams) public loanParams; + /// Loan parameters: loanParamsId => LoanParams + mapping(bytes32 => LoanParams) public loanParams; - /// lender => orderParamsId => Order - mapping(address => mapping(bytes32 => Order)) public lenderOrders; + /// lender => orderParamsId => Order + mapping(address => mapping(bytes32 => Order)) public lenderOrders; - /// borrower => orderParamsId => Order - mapping(address => mapping(bytes32 => Order)) public borrowerOrders; + /// borrower => orderParamsId => Order + mapping(address => mapping(bytes32 => Order)) public borrowerOrders; - /// loanId => delegated => approved - mapping(bytes32 => mapping(address => bool)) public delegatedManagers; + /// loanId => delegated => approved + mapping(bytes32 => mapping(address => bool)) public delegatedManagers; - /** - *** Interest *** - **/ + /** + *** Interest *** + **/ - /// lender => loanToken => LenderInterest object - mapping(address => mapping(address => LenderInterest)) public lenderInterest; + /// lender => loanToken => LenderInterest object + mapping(address => mapping(address => LenderInterest)) public lenderInterest; - /// loanId => LoanInterest object - mapping(bytes32 => LoanInterest) public loanInterest; + /// loanId => LoanInterest object + mapping(bytes32 => LoanInterest) public loanInterest; - /** - *** Internals *** - **/ + /** + *** Internals *** + **/ - /// Implementations set. - EnumerableBytes32Set.Bytes32Set internal logicTargetsSet; + /// Implementations set. + EnumerableBytes32Set.Bytes32Set internal logicTargetsSet; - /// Active loans set. - EnumerableBytes32Set.Bytes32Set internal activeLoansSet; + /// Active loans set. + EnumerableBytes32Set.Bytes32Set internal activeLoansSet; - /// Lender loans set. - mapping(address => EnumerableBytes32Set.Bytes32Set) internal lenderLoanSets; + /// Lender loans set. + mapping(address => EnumerableBytes32Set.Bytes32Set) internal lenderLoanSets; - /// Borrow loans set. - mapping(address => EnumerableBytes32Set.Bytes32Set) internal borrowerLoanSets; + /// Borrow loans set. + mapping(address => EnumerableBytes32Set.Bytes32Set) internal borrowerLoanSets; - /// User loan params set. - mapping(address => EnumerableBytes32Set.Bytes32Set) internal userLoanParamSets; + /// User loan params set. + mapping(address => EnumerableBytes32Set.Bytes32Set) internal userLoanParamSets; - /// Address controlling fee withdrawals. - address public feesController; + /// Address controlling fee withdrawals. + address public feesController; - /// 10% fee /// Fee taken from lender interest payments. - uint256 public lendingFeePercent = 10**19; + /// 10% fee /// Fee taken from lender interest payments. + uint256 public lendingFeePercent = 10**19; - /// Total interest fees received and not withdrawn per asset. - mapping(address => uint256) public lendingFeeTokensHeld; + /// Total interest fees received and not withdrawn per asset. + mapping(address => uint256) public lendingFeeTokensHeld; - /// Total interest fees withdraw per asset. - /// lifetime fees = lendingFeeTokensHeld + lendingFeeTokensPaid - mapping(address => uint256) public lendingFeeTokensPaid; + /// Total interest fees withdraw per asset. + /// lifetime fees = lendingFeeTokensHeld + lendingFeeTokensPaid + mapping(address => uint256) public lendingFeeTokensPaid; - /// 0.15% fee /// Fee paid for each trade. - uint256 public tradingFeePercent = 15 * 10**16; + /// 0.15% fee /// Fee paid for each trade. + uint256 public tradingFeePercent = 15 * 10**16; - /// Total trading fees received and not withdrawn per asset. - mapping(address => uint256) public tradingFeeTokensHeld; + /// Total trading fees received and not withdrawn per asset. + mapping(address => uint256) public tradingFeeTokensHeld; - /// Total trading fees withdraw per asset - /// lifetime fees = tradingFeeTokensHeld + tradingFeeTokensPaid - mapping(address => uint256) public tradingFeeTokensPaid; + /// Total trading fees withdraw per asset + /// lifetime fees = tradingFeeTokensHeld + tradingFeeTokensPaid + mapping(address => uint256) public tradingFeeTokensPaid; - /// 0.09% fee /// Origination fee paid for each loan. - uint256 public borrowingFeePercent = 9 * 10**16; + /// 0.09% fee /// Origination fee paid for each loan. + uint256 public borrowingFeePercent = 9 * 10**16; - /// Total borrowing fees received and not withdrawn per asset. - mapping(address => uint256) public borrowingFeeTokensHeld; + /// Total borrowing fees received and not withdrawn per asset. + mapping(address => uint256) public borrowingFeeTokensHeld; - /// Total borrowing fees withdraw per asset. - /// lifetime fees = borrowingFeeTokensHeld + borrowingFeeTokensPaid - mapping(address => uint256) public borrowingFeeTokensPaid; + /// Total borrowing fees withdraw per asset. + /// lifetime fees = borrowingFeeTokensHeld + borrowingFeeTokensPaid + mapping(address => uint256) public borrowingFeeTokensPaid; - /// Current protocol token deposit balance. - uint256 public protocolTokenHeld; + /// Current protocol token deposit balance. + uint256 public protocolTokenHeld; - /// Lifetime total payout of protocol token. - uint256 public protocolTokenPaid; + /// Lifetime total payout of protocol token. + uint256 public protocolTokenPaid; - /// 5% fee share in form of SOV /// Fee share for affiliate program. - uint256 public affiliateFeePercent = 5 * 10**18; + /// 5% fee share in form of SOV /// Fee share for affiliate program. + uint256 public affiliateFeePercent = 5 * 10**18; - /// 5% collateral discount /// Discount on collateral for liquidators. - uint256 public liquidationIncentivePercent = 5 * 10**18; + /// 5% collateral discount /// Discount on collateral for liquidators. + uint256 public liquidationIncentivePercent = 5 * 10**18; - /// loanPool => underlying - mapping(address => address) public loanPoolToUnderlying; + /// loanPool => underlying + mapping(address => address) public loanPoolToUnderlying; - /// underlying => loanPool - mapping(address => address) public underlyingToLoanPool; + /// underlying => loanPool + mapping(address => address) public underlyingToLoanPool; - /// Loan pools set. - EnumerableBytes32Set.Bytes32Set internal loanPoolsSet; + /// Loan pools set. + EnumerableBytes32Set.Bytes32Set internal loanPoolsSet; - /// Supported tokens for swaps. - mapping(address => bool) public supportedTokens; + /// Supported tokens for swaps. + mapping(address => bool) public supportedTokens; - /// % disagreement between swap rate and reference rate. - uint256 public maxDisagreement = 5 * 10**18; + /// % disagreement between swap rate and reference rate. + uint256 public maxDisagreement = 5 * 10**18; - /// Used as buffer for swap source amount estimations. - uint256 public sourceBuffer = 10000; + /// Used as buffer for swap source amount estimations. + uint256 public sourceBuffer = 10000; - /// Maximum support swap size in rBTC - uint256 public maxSwapSize = 50 ether; + /// Maximum support swap size in rBTC + uint256 public maxSwapSize = 50 ether; - /// Nonce per borrower. Used for loan id creation. - mapping(address => uint256) public borrowerNonce; + /// Nonce per borrower. Used for loan id creation. + mapping(address => uint256) public borrowerNonce; - /// Rollover transaction costs around 0.0000168 rBTC, it is denominated in wrBTC. - uint256 public rolloverBaseReward = 16800000000000; - uint256 public rolloverFlexFeePercent = 0.1 ether; /// 0.1% + /// Rollover transaction costs around 0.0000168 rBTC, it is denominated in wrBTC. + uint256 public rolloverBaseReward = 16800000000000; + uint256 public rolloverFlexFeePercent = 0.1 ether; /// 0.1% - IWrbtcERC20 public wrbtcToken; - address public protocolTokenAddress; + IWrbtcERC20 public wrbtcToken; + address public protocolTokenAddress; - /// 50% fee rebate - /// potocolToken reward to user, it is worth % of trading/borrowing fee. - uint256 public feeRebatePercent = 50 * 10**18; + /// 50% fee rebate + /// potocolToken reward to user, it is worth % of trading/borrowing fee. + uint256 public feeRebatePercent = 50 * 10**18; - address public admin; + address public admin; - /// For modules interaction. - address public protocolAddress; + /// For modules interaction. + address public protocolAddress; - /** - *** Affiliates *** - **/ + /** + *** Affiliates *** + **/ - /// The flag is set on the user's first trade. - mapping(address => bool) public userNotFirstTradeFlag; + /// The flag is set on the user's first trade. + mapping(address => bool) public userNotFirstTradeFlag; - /// User => referrer (affiliate). - mapping(address => address) public affiliatesUserReferrer; + /// User => referrer (affiliate). + mapping(address => address) public affiliatesUserReferrer; - /// List of referral addresses affiliated to the referrer. - mapping(address => EnumerableAddressSet.AddressSet) internal referralsList; + /// List of referral addresses affiliated to the referrer. + mapping(address => EnumerableAddressSet.AddressSet) internal referralsList; - /// @dev Referral threshold for paying out to the referrer. - /// The referrer reward is being accumulated and locked until the threshold is passed. - uint256 public minReferralsToPayout = 3; + /// @dev Referral threshold for paying out to the referrer. + /// The referrer reward is being accumulated and locked until the threshold is passed. + uint256 public minReferralsToPayout = 3; - /// @dev Total affiliate SOV rewards that held in the protocol - /// (Because the minimum referrals is less than the rule) - mapping(address => uint256) public affiliateRewardsHeld; + /// @dev Total affiliate SOV rewards that held in the protocol + /// (Because the minimum referrals is less than the rule) + mapping(address => uint256) public affiliateRewardsHeld; - /// @dev For affiliates SOV Bonus proccess. - address public sovTokenAddress; - address public lockedSOVAddress; + /// @dev For affiliates SOV Bonus proccess. + address public sovTokenAddress; + address public lockedSOVAddress; - /// @dev 20% fee share of trading token fee. - /// Fee share of trading token fee for affiliate program. - uint256 public affiliateTradingTokenFeePercent = 20 * 10**18; + /// @dev 20% fee share of trading token fee. + /// Fee share of trading token fee for affiliate program. + uint256 public affiliateTradingTokenFeePercent = 20 * 10**18; - /// @dev Addresses of tokens in which commissions were paid to referrers. - mapping(address => EnumerableAddressSet.AddressSet) internal affiliatesReferrerTokensList; + /// @dev Addresses of tokens in which commissions were paid to referrers. + mapping(address => EnumerableAddressSet.AddressSet) internal affiliatesReferrerTokensList; - /// @dev [referrerAddress][tokenAddress] is a referrer's token balance of accrued fees. - mapping(address => mapping(address => uint256)) public affiliatesReferrerBalances; + /// @dev [referrerAddress][tokenAddress] is a referrer's token balance of accrued fees. + mapping(address => mapping(address => uint256)) public affiliatesReferrerBalances; - mapping(address => mapping(address => uint256)) public specialRebates; // Special rate rebates for spesific pair -- if not set, then use the default one - bool public pause; //Flag to pause all protocol modules + mapping(address => mapping(address => uint256)) public specialRebates; // Special rate rebates for spesific pair -- if not set, then use the default one + bool public pause; //Flag to pause all protocol modules - uint256 internal swapExtrernalFeePercent; /// Fee percentage for protocol swap + uint256 internal swapExtrernalFeePercent; /// Fee percentage for protocol swap - /// @dev Defines the portion of the trading rebate rewards (SOV) which is to be paid out in a liquid form in basis points. The rest is vested. The max value is 9999 (means 99.99% liquid, 0.01% vested) - uint256 internal tradingRebateRewardsBasisPoint; + /// @dev Defines the portion of the trading rebate rewards (SOV) which is to be paid out in a liquid form in basis points. The rest is vested. The max value is 9999 (means 99.99% liquid, 0.01% vested) + uint256 internal tradingRebateRewardsBasisPoint; - /** - * @notice Add signature and target to storage. - * @dev Protocol is a proxy and requires a way to add every - * module function dynamically during deployment. - * */ - function _setTarget(bytes4 sig, address target) internal { - logicTargets[sig] = target; + /** + * @notice Add signature and target to storage. + * @dev Protocol is a proxy and requires a way to add every + * module function dynamically during deployment. + * */ + function _setTarget(bytes4 sig, address target) internal { + logicTargets[sig] = target; - if (target != address(0)) { - logicTargetsSet.addBytes32(bytes32(sig)); - } else { - logicTargetsSet.removeBytes32(bytes32(sig)); - } - } + if (target != address(0)) { + logicTargetsSet.addBytes32(bytes32(sig)); + } else { + logicTargetsSet.removeBytes32(bytes32(sig)); + } + } } diff --git a/contracts/core/objects/LenderInterestStruct.sol b/contracts/core/objects/LenderInterestStruct.sol index cdbe51f85..1b146d49f 100644 --- a/contracts/core/objects/LenderInterestStruct.sol +++ b/contracts/core/objects/LenderInterestStruct.sol @@ -13,11 +13,11 @@ pragma solidity 0.5.17; * This contract contains the storage structure of the Lender Interest. * */ contract LenderInterestStruct { - struct LenderInterest { - uint256 principalTotal; /// Total borrowed amount outstanding of asset. - uint256 owedPerDay; /// Interest owed per day for all loans of asset. - uint256 owedTotal; /// Total interest owed for all loans of asset (assuming they go to full term). - uint256 paidTotal; /// Total interest paid so far for asset. - uint256 updatedTimestamp; /// Last update. - } + struct LenderInterest { + uint256 principalTotal; /// Total borrowed amount outstanding of asset. + uint256 owedPerDay; /// Interest owed per day for all loans of asset. + uint256 owedTotal; /// Total interest owed for all loans of asset (assuming they go to full term). + uint256 paidTotal; /// Total interest paid so far for asset. + uint256 updatedTimestamp; /// Last update. + } } diff --git a/contracts/core/objects/LoanInterestStruct.sol b/contracts/core/objects/LoanInterestStruct.sol index fae62041e..d61993cb6 100644 --- a/contracts/core/objects/LoanInterestStruct.sol +++ b/contracts/core/objects/LoanInterestStruct.sol @@ -13,9 +13,9 @@ pragma solidity 0.5.17; * This contract contains the storage structure of the Loan Interest. * */ contract LoanInterestStruct { - struct LoanInterest { - uint256 owedPerDay; /// Interest owed per day for loan. - uint256 depositTotal; /// Total escrowed interest for loan. - uint256 updatedTimestamp; /// Last update. - } + struct LoanInterest { + uint256 owedPerDay; /// Interest owed per day for loan. + uint256 depositTotal; /// Total escrowed interest for loan. + uint256 updatedTimestamp; /// Last update. + } } diff --git a/contracts/core/objects/LoanParamsStruct.sol b/contracts/core/objects/LoanParamsStruct.sol index 6a69224a9..7066cab42 100644 --- a/contracts/core/objects/LoanParamsStruct.sol +++ b/contracts/core/objects/LoanParamsStruct.sol @@ -13,23 +13,23 @@ pragma solidity 0.5.17; * This contract contains the storage structure of the Loan Parameters. * */ contract LoanParamsStruct { - struct LoanParams { - /// @dev ID of loan params object. - bytes32 id; - /// @dev If false, this object has been disabled by the owner and can't - /// be used for future loans. - bool active; - /// @dev Owner of this object. - address owner; - /// @dev The token being loaned. - address loanToken; - /// @dev The required collateral token. - address collateralToken; - /// @dev The minimum allowed initial margin. - uint256 minInitialMargin; - /// @dev An unhealthy loan when current margin is at or below this value. - uint256 maintenanceMargin; - /// @dev The maximum term for new loans (0 means there's no max term). - uint256 maxLoanTerm; - } + struct LoanParams { + /// @dev ID of loan params object. + bytes32 id; + /// @dev If false, this object has been disabled by the owner and can't + /// be used for future loans. + bool active; + /// @dev Owner of this object. + address owner; + /// @dev The token being loaned. + address loanToken; + /// @dev The required collateral token. + address collateralToken; + /// @dev The minimum allowed initial margin. + uint256 minInitialMargin; + /// @dev An unhealthy loan when current margin is at or below this value. + uint256 maintenanceMargin; + /// @dev The maximum term for new loans (0 means there's no max term). + uint256 maxLoanTerm; + } } diff --git a/contracts/core/objects/LoanStruct.sol b/contracts/core/objects/LoanStruct.sol index a511aec71..4926f91a7 100644 --- a/contracts/core/objects/LoanStruct.sol +++ b/contracts/core/objects/LoanStruct.sol @@ -13,18 +13,18 @@ pragma solidity 0.5.17; * This contract contains the storage structure of the Loan Object. * */ contract LoanStruct { - struct Loan { - bytes32 id; /// ID of the loan. - bytes32 loanParamsId; /// The linked loan params ID. - bytes32 pendingTradesId; /// The linked pending trades ID. - bool active; /// If false, the loan has been fully closed. - uint256 principal; /// Total borrowed amount outstanding. - uint256 collateral; /// Total collateral escrowed for the loan. - uint256 startTimestamp; /// Loan start time. - uint256 endTimestamp; /// For active loans, this is the expected loan end time, for in-active loans, is the actual (past) end time. - uint256 startMargin; /// Initial margin when the loan opened. - uint256 startRate; /// Reference rate when the loan opened for converting collateralToken to loanToken. - address borrower; /// Borrower of this loan. - address lender; /// Lender of this loan. - } + struct Loan { + bytes32 id; /// ID of the loan. + bytes32 loanParamsId; /// The linked loan params ID. + bytes32 pendingTradesId; /// The linked pending trades ID. + bool active; /// If false, the loan has been fully closed. + uint256 principal; /// Total borrowed amount outstanding. + uint256 collateral; /// Total collateral escrowed for the loan. + uint256 startTimestamp; /// Loan start time. + uint256 endTimestamp; /// For active loans, this is the expected loan end time, for in-active loans, is the actual (past) end time. + uint256 startMargin; /// Initial margin when the loan opened. + uint256 startRate; /// Reference rate when the loan opened for converting collateralToken to loanToken. + address borrower; /// Borrower of this loan. + address lender; /// Lender of this loan. + } } diff --git a/contracts/core/objects/OrderStruct.sol b/contracts/core/objects/OrderStruct.sol index 841055954..f0de183cc 100644 --- a/contracts/core/objects/OrderStruct.sol +++ b/contracts/core/objects/OrderStruct.sol @@ -13,12 +13,12 @@ pragma solidity 0.5.17; * This contract contains the storage structure of the Loan Order. * */ contract OrderStruct { - struct Order { - uint256 lockedAmount; /// Escrowed amount waiting for a counterparty. - uint256 interestRate; /// Interest rate defined by the creator of this order. - uint256 minLoanTerm; /// Minimum loan term allowed. - uint256 maxLoanTerm; /// Maximum loan term allowed. - uint256 createdTimestamp; /// Timestamp when this order was created. - uint256 expirationTimestamp; /// Timestamp when this order expires. - } + struct Order { + uint256 lockedAmount; /// Escrowed amount waiting for a counterparty. + uint256 interestRate; /// Interest rate defined by the creator of this order. + uint256 minLoanTerm; /// Minimum loan term allowed. + uint256 maxLoanTerm; /// Maximum loan term allowed. + uint256 createdTimestamp; /// Timestamp when this order was created. + uint256 expirationTimestamp; /// Timestamp when this order expires. + } } diff --git a/contracts/escrow/Escrow.sol b/contracts/escrow/Escrow.sol index 3e40d9689..ca6f1090c 100644 --- a/contracts/escrow/Escrow.sol +++ b/contracts/escrow/Escrow.sol @@ -9,274 +9,288 @@ import "../interfaces/IERC20.sol"; * @notice You can use this contract for deposit of SOV tokens for some time and withdraw later. */ contract Escrow { - using SafeMath for uint256; - - /* Storage */ - - /// @notice The total tokens deposited. - /// @dev Used for calculating the reward % share of users related to total deposit. - uint256 public totalDeposit; - /// @notice The release timestamp for the tokens deposited. - uint256 public releaseTime; - /// @notice The amount of token we would be accepting as deposit at max. - uint256 public depositLimit; - - /// @notice The SOV token contract. - IERC20 public SOV; - - /// @notice The multisig contract which handles the fund. - address public multisig; - - /// @notice The user balances. - mapping(address => uint256) userBalances; - - /// @notice The current contract status. - /// @notice Deployed - Deployed the contract. - /// @notice Deposit - Time to deposit in the contract by the users. - /// @notice Holding - Deposit is closed and now the holding period starts. - /// @notice Withdraw - Time to withdraw in the contract by the users. - /// @notice Expired - The contract is now closed completely. - enum Status { Deployed, Deposit, Holding, Withdraw, Expired } - Status public status; - - /* Events */ - - /// @notice Emitted when the contract deposit starts. - event EscrowActivated(); - - /// @notice Emitted when the contract is put in holding state. No new token deposit accepted by User. - event EscrowInHoldingState(); - - /// @notice Emitted when the contract is put in withdraw state. Users can now withdraw tokens. - event EscrowInWithdrawState(); - - /// @notice Emitted when the contract is expired after withdraws are made/total token transfer. - event EscrowFundExpired(); - - /// @notice Emitted when a new multisig is added to the contract. - /// @param _initiator The address which initiated this event to be emitted. - /// @param _newMultisig The address which is added as the new multisig. - /// @dev Can only be initiated by the current multisig. - event NewMultisig(address indexed _initiator, address indexed _newMultisig); - - /// @notice Emitted when the release timestamp is updated. - /// @param _initiator The address which initiated this event to be emitted. - /// @param _releaseTimestamp The updated release timestamp for the withdraw. - event TokenReleaseUpdated(address indexed _initiator, uint256 _releaseTimestamp); - - /// @notice Emitted when the deposit limit is updated. - /// @param _initiator The address which initiated this event to be emitted. - /// @param _depositLimit The updated deposit limit. - event TokenDepositLimitUpdated(address indexed _initiator, uint256 _depositLimit); - - /// @notice Emitted when a new token deposit is done by User. - /// @param _initiator The address which initiated this event to be emitted. - /// @param _amount The amount of token deposited. - event TokenDeposit(address indexed _initiator, uint256 _amount); - - /// @notice Emitted when we reach the token deposit limit. - event DepositLimitReached(); - - /// @notice Emitted when a token withdraw is done by Multisig. - /// @param _initiator The address which initiated this event to be emitted. - /// @param _amount The amount of token withdrawed. - event TokenWithdrawByMultisig(address indexed _initiator, uint256 _amount); - - /// @notice Emitted when a new token deposit is done by Multisig. - /// @param _initiator The address which initiated this event to be emitted. - /// @param _amount The amount of token deposited. - event TokenDepositByMultisig(address indexed _initiator, uint256 _amount); - - /// @notice Emitted when a token withdraw is done by User. - /// @param _initiator The address which initiated this event to be emitted. - /// @param _amount The amount of token withdrawed. - event TokenWithdraw(address indexed _initiator, uint256 _amount); - - /* Modifiers */ - - modifier onlyMultisig() { - require(msg.sender == multisig, "Only Multisig can call this."); - _; - } - - modifier checkStatus(Status s) { - require(status == s, "The contract is not in the right state."); - _; - } - - modifier checkRelease() { - require(releaseTime != 0 && releaseTime <= block.timestamp, "The release time has not started yet."); - _; - } - - /* Functions */ - - /** - * @notice Setup the required parameters. - * @param _SOV The SOV token address. - * @param _multisig The owner of the tokens & contract. - * @param _releaseTime The token release time, zero if undecided. - * @param _depositLimit The amount of tokens we will be accepting. - */ - constructor( - address _SOV, - address _multisig, - uint256 _releaseTime, - uint256 _depositLimit - ) public { - require(_SOV != address(0), "Invalid SOV Address."); - require(_multisig != address(0), "Invalid Multisig Address."); - - SOV = IERC20(_SOV); - multisig = _multisig; - - emit NewMultisig(msg.sender, _multisig); - - releaseTime = _releaseTime; - depositLimit = _depositLimit; - - status = Status.Deployed; - } - - /** - * @notice This function is called once after deployment for starting the deposit action. - * @dev Without calling this function, the contract will not start accepting tokens. - */ - function init() external onlyMultisig checkStatus(Status.Deployed) { - status = Status.Deposit; - - emit EscrowActivated(); - } - - /** - * @notice Update Multisig. - * @param _newMultisig The new owner of the tokens & contract. - */ - function updateMultisig(address _newMultisig) external onlyMultisig { - require(_newMultisig != address(0), "New Multisig address invalid."); - - multisig = _newMultisig; - - emit NewMultisig(msg.sender, _newMultisig); - } - - /** - * @notice Update Release Timestamp. - * @param _newReleaseTime The new release timestamp for token release. - * @dev Zero is also a valid timestamp, if the release time is not scheduled yet. - */ - function updateReleaseTimestamp(uint256 _newReleaseTime) external onlyMultisig { - releaseTime = _newReleaseTime; - - emit TokenReleaseUpdated(msg.sender, _newReleaseTime); - } - - /** - * @notice Update Deposit Limit. - * @param _newDepositLimit The new deposit limit. - * @dev IMPORTANT: Should not decrease than already deposited. - */ - function updateDepositLimit(uint256 _newDepositLimit) external onlyMultisig { - require(_newDepositLimit >= totalDeposit, "Deposit already higher than the limit trying to be set."); - depositLimit = _newDepositLimit; - - emit TokenDepositLimitUpdated(msg.sender, _newDepositLimit); - } - - /** - * @notice Deposit tokens to this contract by User. - * @param _amount the amount of tokens deposited. - * @dev The contract has to be approved by the user inorder for this function to work. - * These tokens can be withdrawn/transferred during Holding State by the Multisig. - */ - function depositTokens(uint256 _amount) external checkStatus(Status.Deposit) { - require(_amount > 0, "Amount needs to be bigger than zero."); - uint256 amount = _amount; - - if (totalDeposit.add(_amount) >= depositLimit) { - amount = depositLimit.sub(totalDeposit); - emit DepositLimitReached(); - } - - bool txStatus = SOV.transferFrom(msg.sender, address(this), amount); - require(txStatus, "Token transfer was not successful."); - - userBalances[msg.sender] = userBalances[msg.sender].add(amount); - totalDeposit = totalDeposit.add(amount); - - emit TokenDeposit(msg.sender, amount); - } - - /** - * @notice Update contract state to Holding. - * @dev Once called, the contract no longer accepts any more deposits. - * The multisig can now withdraw tokens from the contract after the contract is in Holding State. - */ - function changeStateToHolding() external onlyMultisig checkStatus(Status.Deposit) { - status = Status.Holding; - - emit EscrowInHoldingState(); - } - - /** - * @notice Withdraws all token from the contract by Multisig. - * @param _receiverAddress The address where the tokens has to be transferred. Zero address if the withdraw is to be done in Multisig. - * @dev Can only be called after the token state is changed to Holding. - */ - function withdrawTokensByMultisig(address _receiverAddress) external onlyMultisig checkStatus(Status.Holding) { - address receiverAddress = msg.sender; - if (_receiverAddress != address(0)) { - receiverAddress = _receiverAddress; - } - - uint256 value = SOV.balanceOf(address(this)); - /// Sending the amount to multisig. - bool txStatus = SOV.transfer(receiverAddress, value); - require(txStatus, "Token transfer was not successful. Check receiver address."); - - emit TokenWithdrawByMultisig(msg.sender, value); - } - - /** - * @notice Deposit tokens to this contract by the Multisig. - * @param _amount the amount of tokens deposited. - * @dev The contract has to be approved by the multisig inorder for this function to work. - * Once the token deposit is higher than the total deposits done, the contract state is changed to Withdraw. - */ - function depositTokensByMultisig(uint256 _amount) external onlyMultisig checkStatus(Status.Holding) { - require(_amount > 0, "Amount needs to be bigger than zero."); - - bool txStatus = SOV.transferFrom(msg.sender, address(this), _amount); - require(txStatus, "Token transfer was not successful."); - - emit TokenDepositByMultisig(msg.sender, _amount); - - if (SOV.balanceOf(address(this)) >= totalDeposit) { - status = Status.Withdraw; - emit EscrowInWithdrawState(); - } - } - - /** - * @notice Withdraws token from the contract by User. - * @dev Only works after the contract state is in Withdraw. - */ - function withdrawTokens() public checkRelease checkStatus(Status.Withdraw) { - uint256 amount = userBalances[msg.sender]; - userBalances[msg.sender] = 0; - bool txStatus = SOV.transfer(msg.sender, amount); - require(txStatus, "Token transfer was not successful. Check receiver address."); - - emit TokenWithdraw(msg.sender, amount); - } - - /* Getter Functions */ - - /** - * @notice Function to read the current token balance of a particular user. - * @return _addr The user address whose balance has to be checked. - */ - function getUserBalance(address _addr) external view returns (uint256 balance) { - return userBalances[_addr]; - } + using SafeMath for uint256; + + /* Storage */ + + /// @notice The total tokens deposited. + /// @dev Used for calculating the reward % share of users related to total deposit. + uint256 public totalDeposit; + /// @notice The release timestamp for the tokens deposited. + uint256 public releaseTime; + /// @notice The amount of token we would be accepting as deposit at max. + uint256 public depositLimit; + + /// @notice The SOV token contract. + IERC20 public SOV; + + /// @notice The multisig contract which handles the fund. + address public multisig; + + /// @notice The user balances. + mapping(address => uint256) userBalances; + + /// @notice The current contract status. + /// @notice Deployed - Deployed the contract. + /// @notice Deposit - Time to deposit in the contract by the users. + /// @notice Holding - Deposit is closed and now the holding period starts. + /// @notice Withdraw - Time to withdraw in the contract by the users. + /// @notice Expired - The contract is now closed completely. + enum Status { Deployed, Deposit, Holding, Withdraw, Expired } + Status public status; + + /* Events */ + + /// @notice Emitted when the contract deposit starts. + event EscrowActivated(); + + /// @notice Emitted when the contract is put in holding state. No new token deposit accepted by User. + event EscrowInHoldingState(); + + /// @notice Emitted when the contract is put in withdraw state. Users can now withdraw tokens. + event EscrowInWithdrawState(); + + /// @notice Emitted when the contract is expired after withdraws are made/total token transfer. + event EscrowFundExpired(); + + /// @notice Emitted when a new multisig is added to the contract. + /// @param _initiator The address which initiated this event to be emitted. + /// @param _newMultisig The address which is added as the new multisig. + /// @dev Can only be initiated by the current multisig. + event NewMultisig(address indexed _initiator, address indexed _newMultisig); + + /// @notice Emitted when the release timestamp is updated. + /// @param _initiator The address which initiated this event to be emitted. + /// @param _releaseTimestamp The updated release timestamp for the withdraw. + event TokenReleaseUpdated(address indexed _initiator, uint256 _releaseTimestamp); + + /// @notice Emitted when the deposit limit is updated. + /// @param _initiator The address which initiated this event to be emitted. + /// @param _depositLimit The updated deposit limit. + event TokenDepositLimitUpdated(address indexed _initiator, uint256 _depositLimit); + + /// @notice Emitted when a new token deposit is done by User. + /// @param _initiator The address which initiated this event to be emitted. + /// @param _amount The amount of token deposited. + event TokenDeposit(address indexed _initiator, uint256 _amount); + + /// @notice Emitted when we reach the token deposit limit. + event DepositLimitReached(); + + /// @notice Emitted when a token withdraw is done by Multisig. + /// @param _initiator The address which initiated this event to be emitted. + /// @param _amount The amount of token withdrawed. + event TokenWithdrawByMultisig(address indexed _initiator, uint256 _amount); + + /// @notice Emitted when a new token deposit is done by Multisig. + /// @param _initiator The address which initiated this event to be emitted. + /// @param _amount The amount of token deposited. + event TokenDepositByMultisig(address indexed _initiator, uint256 _amount); + + /// @notice Emitted when a token withdraw is done by User. + /// @param _initiator The address which initiated this event to be emitted. + /// @param _amount The amount of token withdrawed. + event TokenWithdraw(address indexed _initiator, uint256 _amount); + + /* Modifiers */ + + modifier onlyMultisig() { + require(msg.sender == multisig, "Only Multisig can call this."); + _; + } + + modifier checkStatus(Status s) { + require(status == s, "The contract is not in the right state."); + _; + } + + modifier checkRelease() { + require( + releaseTime != 0 && releaseTime <= block.timestamp, + "The release time has not started yet." + ); + _; + } + + /* Functions */ + + /** + * @notice Setup the required parameters. + * @param _SOV The SOV token address. + * @param _multisig The owner of the tokens & contract. + * @param _releaseTime The token release time, zero if undecided. + * @param _depositLimit The amount of tokens we will be accepting. + */ + constructor( + address _SOV, + address _multisig, + uint256 _releaseTime, + uint256 _depositLimit + ) public { + require(_SOV != address(0), "Invalid SOV Address."); + require(_multisig != address(0), "Invalid Multisig Address."); + + SOV = IERC20(_SOV); + multisig = _multisig; + + emit NewMultisig(msg.sender, _multisig); + + releaseTime = _releaseTime; + depositLimit = _depositLimit; + + status = Status.Deployed; + } + + /** + * @notice This function is called once after deployment for starting the deposit action. + * @dev Without calling this function, the contract will not start accepting tokens. + */ + function init() external onlyMultisig checkStatus(Status.Deployed) { + status = Status.Deposit; + + emit EscrowActivated(); + } + + /** + * @notice Update Multisig. + * @param _newMultisig The new owner of the tokens & contract. + */ + function updateMultisig(address _newMultisig) external onlyMultisig { + require(_newMultisig != address(0), "New Multisig address invalid."); + + multisig = _newMultisig; + + emit NewMultisig(msg.sender, _newMultisig); + } + + /** + * @notice Update Release Timestamp. + * @param _newReleaseTime The new release timestamp for token release. + * @dev Zero is also a valid timestamp, if the release time is not scheduled yet. + */ + function updateReleaseTimestamp(uint256 _newReleaseTime) external onlyMultisig { + releaseTime = _newReleaseTime; + + emit TokenReleaseUpdated(msg.sender, _newReleaseTime); + } + + /** + * @notice Update Deposit Limit. + * @param _newDepositLimit The new deposit limit. + * @dev IMPORTANT: Should not decrease than already deposited. + */ + function updateDepositLimit(uint256 _newDepositLimit) external onlyMultisig { + require( + _newDepositLimit >= totalDeposit, + "Deposit already higher than the limit trying to be set." + ); + depositLimit = _newDepositLimit; + + emit TokenDepositLimitUpdated(msg.sender, _newDepositLimit); + } + + /** + * @notice Deposit tokens to this contract by User. + * @param _amount the amount of tokens deposited. + * @dev The contract has to be approved by the user inorder for this function to work. + * These tokens can be withdrawn/transferred during Holding State by the Multisig. + */ + function depositTokens(uint256 _amount) external checkStatus(Status.Deposit) { + require(_amount > 0, "Amount needs to be bigger than zero."); + uint256 amount = _amount; + + if (totalDeposit.add(_amount) >= depositLimit) { + amount = depositLimit.sub(totalDeposit); + emit DepositLimitReached(); + } + + bool txStatus = SOV.transferFrom(msg.sender, address(this), amount); + require(txStatus, "Token transfer was not successful."); + + userBalances[msg.sender] = userBalances[msg.sender].add(amount); + totalDeposit = totalDeposit.add(amount); + + emit TokenDeposit(msg.sender, amount); + } + + /** + * @notice Update contract state to Holding. + * @dev Once called, the contract no longer accepts any more deposits. + * The multisig can now withdraw tokens from the contract after the contract is in Holding State. + */ + function changeStateToHolding() external onlyMultisig checkStatus(Status.Deposit) { + status = Status.Holding; + + emit EscrowInHoldingState(); + } + + /** + * @notice Withdraws all token from the contract by Multisig. + * @param _receiverAddress The address where the tokens has to be transferred. Zero address if the withdraw is to be done in Multisig. + * @dev Can only be called after the token state is changed to Holding. + */ + function withdrawTokensByMultisig(address _receiverAddress) + external + onlyMultisig + checkStatus(Status.Holding) + { + address receiverAddress = msg.sender; + if (_receiverAddress != address(0)) { + receiverAddress = _receiverAddress; + } + + uint256 value = SOV.balanceOf(address(this)); + /// Sending the amount to multisig. + bool txStatus = SOV.transfer(receiverAddress, value); + require(txStatus, "Token transfer was not successful. Check receiver address."); + + emit TokenWithdrawByMultisig(msg.sender, value); + } + + /** + * @notice Deposit tokens to this contract by the Multisig. + * @param _amount the amount of tokens deposited. + * @dev The contract has to be approved by the multisig inorder for this function to work. + * Once the token deposit is higher than the total deposits done, the contract state is changed to Withdraw. + */ + function depositTokensByMultisig(uint256 _amount) + external + onlyMultisig + checkStatus(Status.Holding) + { + require(_amount > 0, "Amount needs to be bigger than zero."); + + bool txStatus = SOV.transferFrom(msg.sender, address(this), _amount); + require(txStatus, "Token transfer was not successful."); + + emit TokenDepositByMultisig(msg.sender, _amount); + + if (SOV.balanceOf(address(this)) >= totalDeposit) { + status = Status.Withdraw; + emit EscrowInWithdrawState(); + } + } + + /** + * @notice Withdraws token from the contract by User. + * @dev Only works after the contract state is in Withdraw. + */ + function withdrawTokens() public checkRelease checkStatus(Status.Withdraw) { + uint256 amount = userBalances[msg.sender]; + userBalances[msg.sender] = 0; + bool txStatus = SOV.transfer(msg.sender, amount); + require(txStatus, "Token transfer was not successful. Check receiver address."); + + emit TokenWithdraw(msg.sender, amount); + } + + /* Getter Functions */ + + /** + * @notice Function to read the current token balance of a particular user. + * @return _addr The user address whose balance has to be checked. + */ + function getUserBalance(address _addr) external view returns (uint256 balance) { + return userBalances[_addr]; + } } diff --git a/contracts/escrow/EscrowReward.sol b/contracts/escrow/EscrowReward.sol index c7eb58d7e..e549da1e4 100644 --- a/contracts/escrow/EscrowReward.sol +++ b/contracts/escrow/EscrowReward.sol @@ -9,112 +9,115 @@ import "../locked/ILockedSOV.sol"; * @notice Multisig can use this contract for depositing of Reward tokens based on the total token deposit. */ contract EscrowReward is Escrow { - using SafeMath for uint256; - - /* Storage */ - - /// @notice The total reward tokens deposited. - /// @dev Used for calculating the reward % share of users related to total deposit. - uint256 public totalRewardDeposit; - - /// @notice The Locked SOV contract. - ILockedSOV public lockedSOV; - - /* Events */ - - /// @notice Emitted when the Locked SOV Contract address is updated. - /// @param _initiator The address which initiated this event to be emitted. - /// @param _lockedSOV The address of the Locked SOV Contract. - event LockedSOVUpdated(address indexed _initiator, address indexed _lockedSOV); - - /// @notice Emitted when a new reward token deposit is done by Multisig. - /// @param _initiator The address which initiated this event to be emitted. - /// @param _amount The amount of token deposited. - event RewardDepositByMultisig(address indexed _initiator, uint256 _amount); - - /// @notice Emitted when a Reward token withdraw is done by User. - /// @param _initiator The address which initiated this event to be emitted. - /// @param _amount The amount of token withdrawed. - event RewardTokenWithdraw(address indexed _initiator, uint256 _amount); - - /* Functions */ - - /** - * @notice Setup the required parameters. - * @param _lockedSOV The Locked SOV Contract address. - * @param _SOV The SOV token address. - * @param _multisig The owner of the tokens & contract. - * @param _releaseTime The token release time, zero if undecided. - * @param _depositLimit The amount of tokens we will be accepting. - */ - constructor( - address _lockedSOV, - address _SOV, - address _multisig, - uint256 _releaseTime, - uint256 _depositLimit - ) public Escrow(_SOV, _multisig, _releaseTime, _depositLimit) { - if (_lockedSOV != address(0)) { - lockedSOV = ILockedSOV(_lockedSOV); - } - } - - /** - * @notice Set the Locked SOV Contract Address if not already done. - * @param _lockedSOV The Locked SOV Contract address. - */ - function updateLockedSOV(address _lockedSOV) external onlyMultisig { - require(_lockedSOV != address(0), "Invalid Reward Token Address."); - - lockedSOV = ILockedSOV(_lockedSOV); - - emit LockedSOVUpdated(msg.sender, _lockedSOV); - } - - /** - * @notice Deposit tokens to this contract by the Multisig. - * @param _amount the amount of tokens deposited. - * @dev The contract has to be approved by the multisig inorder for this function to work. - */ - function depositRewardByMultisig(uint256 _amount) external onlyMultisig { - require(status != Status.Withdraw, "Reward Token deposit is only allowed before User Withdraw starts."); - require(_amount > 0, "Amount needs to be bigger than zero."); - - bool txStatus = SOV.transferFrom(msg.sender, address(this), _amount); - require(txStatus, "Token transfer was not successful."); - - totalRewardDeposit = totalRewardDeposit.add(_amount); - txStatus = SOV.approve(address(lockedSOV), totalRewardDeposit); - require(txStatus, "Token Approval was not successful."); - - emit RewardDepositByMultisig(msg.sender, _amount); - } - - /** - * @notice Withdraws token and reward from the contract by User. Reward is gone to lockedSOV contract for future vesting. - * @dev Only works after the contract state is in Withdraw. - */ - function withdrawTokensAndReward() external checkRelease checkStatus(Status.Withdraw) { - // Reward calculation have to be done initially as the User Balance is zeroed out . - uint256 reward = userBalances[msg.sender].mul(totalRewardDeposit).div(totalDeposit); - withdrawTokens(); - - lockedSOV.depositSOV(msg.sender, reward); - - emit RewardTokenWithdraw(msg.sender, reward); - } - - /* Getter Functions */ - - /** - * @notice Function to read the reward a particular user can get. - * @param _addr The address of the user whose reward is to be read. - * @return reward The reward received by the user. - */ - function getReward(address _addr) external view returns (uint256 reward) { - if (userBalances[_addr].mul(totalRewardDeposit) == 0) { - return 0; - } - return userBalances[_addr].mul(totalRewardDeposit).div(totalDeposit); - } + using SafeMath for uint256; + + /* Storage */ + + /// @notice The total reward tokens deposited. + /// @dev Used for calculating the reward % share of users related to total deposit. + uint256 public totalRewardDeposit; + + /// @notice The Locked SOV contract. + ILockedSOV public lockedSOV; + + /* Events */ + + /// @notice Emitted when the Locked SOV Contract address is updated. + /// @param _initiator The address which initiated this event to be emitted. + /// @param _lockedSOV The address of the Locked SOV Contract. + event LockedSOVUpdated(address indexed _initiator, address indexed _lockedSOV); + + /// @notice Emitted when a new reward token deposit is done by Multisig. + /// @param _initiator The address which initiated this event to be emitted. + /// @param _amount The amount of token deposited. + event RewardDepositByMultisig(address indexed _initiator, uint256 _amount); + + /// @notice Emitted when a Reward token withdraw is done by User. + /// @param _initiator The address which initiated this event to be emitted. + /// @param _amount The amount of token withdrawed. + event RewardTokenWithdraw(address indexed _initiator, uint256 _amount); + + /* Functions */ + + /** + * @notice Setup the required parameters. + * @param _lockedSOV The Locked SOV Contract address. + * @param _SOV The SOV token address. + * @param _multisig The owner of the tokens & contract. + * @param _releaseTime The token release time, zero if undecided. + * @param _depositLimit The amount of tokens we will be accepting. + */ + constructor( + address _lockedSOV, + address _SOV, + address _multisig, + uint256 _releaseTime, + uint256 _depositLimit + ) public Escrow(_SOV, _multisig, _releaseTime, _depositLimit) { + if (_lockedSOV != address(0)) { + lockedSOV = ILockedSOV(_lockedSOV); + } + } + + /** + * @notice Set the Locked SOV Contract Address if not already done. + * @param _lockedSOV The Locked SOV Contract address. + */ + function updateLockedSOV(address _lockedSOV) external onlyMultisig { + require(_lockedSOV != address(0), "Invalid Reward Token Address."); + + lockedSOV = ILockedSOV(_lockedSOV); + + emit LockedSOVUpdated(msg.sender, _lockedSOV); + } + + /** + * @notice Deposit tokens to this contract by the Multisig. + * @param _amount the amount of tokens deposited. + * @dev The contract has to be approved by the multisig inorder for this function to work. + */ + function depositRewardByMultisig(uint256 _amount) external onlyMultisig { + require( + status != Status.Withdraw, + "Reward Token deposit is only allowed before User Withdraw starts." + ); + require(_amount > 0, "Amount needs to be bigger than zero."); + + bool txStatus = SOV.transferFrom(msg.sender, address(this), _amount); + require(txStatus, "Token transfer was not successful."); + + totalRewardDeposit = totalRewardDeposit.add(_amount); + txStatus = SOV.approve(address(lockedSOV), totalRewardDeposit); + require(txStatus, "Token Approval was not successful."); + + emit RewardDepositByMultisig(msg.sender, _amount); + } + + /** + * @notice Withdraws token and reward from the contract by User. Reward is gone to lockedSOV contract for future vesting. + * @dev Only works after the contract state is in Withdraw. + */ + function withdrawTokensAndReward() external checkRelease checkStatus(Status.Withdraw) { + // Reward calculation have to be done initially as the User Balance is zeroed out . + uint256 reward = userBalances[msg.sender].mul(totalRewardDeposit).div(totalDeposit); + withdrawTokens(); + + lockedSOV.depositSOV(msg.sender, reward); + + emit RewardTokenWithdraw(msg.sender, reward); + } + + /* Getter Functions */ + + /** + * @notice Function to read the reward a particular user can get. + * @param _addr The address of the user whose reward is to be read. + * @return reward The reward received by the user. + */ + function getReward(address _addr) external view returns (uint256 reward) { + if (userBalances[_addr].mul(totalRewardDeposit) == 0) { + return 0; + } + return userBalances[_addr].mul(totalRewardDeposit).div(totalDeposit); + } } diff --git a/contracts/events/AffiliatesEvents.sol b/contracts/events/AffiliatesEvents.sol index 367c5a0e2..2aa207490 100644 --- a/contracts/events/AffiliatesEvents.sol +++ b/contracts/events/AffiliatesEvents.sol @@ -8,37 +8,42 @@ pragma solidity 0.5.17; import "./ModulesCommonEvents.sol"; contract AffiliatesEvents is ModulesCommonEvents { - event SetAffiliatesReferrer(address indexed user, address indexed referrer); - - event SetAffiliatesReferrerFail(address indexed user, address indexed referrer, bool alreadySet, bool userNotFirstTrade); - - event SetUserNotFirstTradeFlag(address indexed user); - - event PayTradingFeeToAffiliate( - address indexed referrer, - address trader, - address indexed token, - bool indexed isHeld, - uint256 tradingFeeTokenAmount, - uint256 tokenBonusAmount, - uint256 sovBonusAmount, - uint256 sovBonusAmountPaid - ); - - event PayTradingFeeToAffiliateFail( - address indexed referrer, - address trader, - address indexed token, - uint256 tradingFeeTokenAmount, - uint256 tokenBonusAmount, - uint256 sovBonusAmount, - uint256 sovBonusAmountTryingToPaid - ); - - event WithdrawAffiliatesReferrerTokenFees( - address indexed referrer, - address indexed receiver, - address indexed tokenAddress, - uint256 amount - ); + event SetAffiliatesReferrer(address indexed user, address indexed referrer); + + event SetAffiliatesReferrerFail( + address indexed user, + address indexed referrer, + bool alreadySet, + bool userNotFirstTrade + ); + + event SetUserNotFirstTradeFlag(address indexed user); + + event PayTradingFeeToAffiliate( + address indexed referrer, + address trader, + address indexed token, + bool indexed isHeld, + uint256 tradingFeeTokenAmount, + uint256 tokenBonusAmount, + uint256 sovBonusAmount, + uint256 sovBonusAmountPaid + ); + + event PayTradingFeeToAffiliateFail( + address indexed referrer, + address trader, + address indexed token, + uint256 tradingFeeTokenAmount, + uint256 tokenBonusAmount, + uint256 sovBonusAmount, + uint256 sovBonusAmountTryingToPaid + ); + + event WithdrawAffiliatesReferrerTokenFees( + address indexed referrer, + address indexed receiver, + address indexed tokenAddress, + uint256 amount + ); } diff --git a/contracts/events/FeesEvents.sol b/contracts/events/FeesEvents.sol index ababd6047..12eab99aa 100644 --- a/contracts/events/FeesEvents.sol +++ b/contracts/events/FeesEvents.sol @@ -13,27 +13,37 @@ pragma solidity 0.5.17; * This contract contains the events for fee payments. * */ contract FeesEvents { - event PayLendingFee(address indexed payer, address indexed token, uint256 amount); + event PayLendingFee(address indexed payer, address indexed token, uint256 amount); - event PayTradingFee(address indexed payer, address indexed token, bytes32 indexed loanId, uint256 amount); + event PayTradingFee( + address indexed payer, + address indexed token, + bytes32 indexed loanId, + uint256 amount + ); - event PayBorrowingFee(address indexed payer, address indexed token, bytes32 indexed loanId, uint256 amount); + event PayBorrowingFee( + address indexed payer, + address indexed token, + bytes32 indexed loanId, + uint256 amount + ); - event EarnReward( - address indexed receiver, - address indexed token, - bytes32 indexed loanId, - uint256 feeRebatePercent, - uint256 amount, - uint256 basisPoint - ); + event EarnReward( + address indexed receiver, + address indexed token, + bytes32 indexed loanId, + uint256 feeRebatePercent, + uint256 amount, + uint256 basisPoint + ); - event EarnRewardFail( - address indexed receiver, - address indexed token, - bytes32 indexed loanId, - uint256 feeRebatePercent, - uint256 amount, - uint256 basisPoint - ); + event EarnRewardFail( + address indexed receiver, + address indexed token, + bytes32 indexed loanId, + uint256 feeRebatePercent, + uint256 amount, + uint256 basisPoint + ); } diff --git a/contracts/events/LoanClosingsEvents.sol b/contracts/events/LoanClosingsEvents.sol index b020ce5f3..1ea5f7aef 100644 --- a/contracts/events/LoanClosingsEvents.sol +++ b/contracts/events/LoanClosingsEvents.sol @@ -15,58 +15,58 @@ import "./ModulesCommonEvents.sol"; * This contract contains the events for loan closing operations. * */ contract LoanClosingsEvents is ModulesCommonEvents { - /// topic0: 0x6349c1a02ec126f7f4fc6e6837e1859006e90e9901635c442d29271e77b96fb6 - event CloseWithDeposit( - address indexed user, - address indexed lender, - bytes32 indexed loanId, - address closer, - address loanToken, - address collateralToken, - uint256 repayAmount, - uint256 collateralWithdrawAmount, - uint256 collateralToLoanRate, - uint256 currentMargin - ); + /// topic0: 0x6349c1a02ec126f7f4fc6e6837e1859006e90e9901635c442d29271e77b96fb6 + event CloseWithDeposit( + address indexed user, + address indexed lender, + bytes32 indexed loanId, + address closer, + address loanToken, + address collateralToken, + uint256 repayAmount, + uint256 collateralWithdrawAmount, + uint256 collateralToLoanRate, + uint256 currentMargin + ); - /// topic0: 0x2ed7b29b4ca95cf3bb9a44f703872a66e6aa5e8f07b675fa9a5c124a1e5d7352 - event CloseWithSwap( - address indexed user, - address indexed lender, - bytes32 indexed loanId, - address collateralToken, - address loanToken, - address closer, - uint256 positionCloseSize, - uint256 loanCloseAmount, - uint256 exitPrice, // one unit of collateralToken, denominated in loanToken - uint256 currentLeverage - ); + /// topic0: 0x2ed7b29b4ca95cf3bb9a44f703872a66e6aa5e8f07b675fa9a5c124a1e5d7352 + event CloseWithSwap( + address indexed user, + address indexed lender, + bytes32 indexed loanId, + address collateralToken, + address loanToken, + address closer, + uint256 positionCloseSize, + uint256 loanCloseAmount, + uint256 exitPrice, // one unit of collateralToken, denominated in loanToken + uint256 currentLeverage + ); - /// topic0: 0x46fa03303782eb2f686515f6c0100f9a62dabe587b0d3f5a4fc0c822d6e532d3 - event Liquidate( - address indexed user, - address indexed liquidator, - bytes32 indexed loanId, - address lender, - address loanToken, - address collateralToken, - uint256 repayAmount, - uint256 collateralWithdrawAmount, - uint256 collateralToLoanRate, - uint256 currentMargin - ); + /// topic0: 0x46fa03303782eb2f686515f6c0100f9a62dabe587b0d3f5a4fc0c822d6e532d3 + event Liquidate( + address indexed user, + address indexed liquidator, + bytes32 indexed loanId, + address lender, + address loanToken, + address collateralToken, + uint256 repayAmount, + uint256 collateralWithdrawAmount, + uint256 collateralToLoanRate, + uint256 currentMargin + ); - event Rollover( - address indexed user, - address indexed lender, - bytes32 indexed loanId, - uint256 principal, - uint256 collateral, - uint256 endTimestamp, - address rewardReceiver, - uint256 reward - ); + event Rollover( + address indexed user, + address indexed lender, + bytes32 indexed loanId, + uint256 principal, + uint256 collateral, + uint256 endTimestamp, + address rewardReceiver, + uint256 reward + ); - event swapExcess(bool shouldRefund, uint256 amount, uint256 amountInRbtc, uint256 threshold); + event swapExcess(bool shouldRefund, uint256 amount, uint256 amountInRbtc, uint256 threshold); } diff --git a/contracts/events/LoanMaintenanceEvents.sol b/contracts/events/LoanMaintenanceEvents.sol index a1b5e6cee..5f498f046 100644 --- a/contracts/events/LoanMaintenanceEvents.sol +++ b/contracts/events/LoanMaintenanceEvents.sol @@ -10,5 +10,5 @@ import "./ModulesCommonEvents.sol"; * This contract contains the events for loan maintenance operations. * */ contract LoanMaintenanceEvents is ModulesCommonEvents { - event DepositCollateral(bytes32 indexed loanId, uint256 depositAmount, uint256 rate); + event DepositCollateral(bytes32 indexed loanId, uint256 depositAmount, uint256 rate); } diff --git a/contracts/events/LoanOpeningsEvents.sol b/contracts/events/LoanOpeningsEvents.sol index 79f5eac19..bd629a3b2 100644 --- a/contracts/events/LoanOpeningsEvents.sol +++ b/contracts/events/LoanOpeningsEvents.sol @@ -15,37 +15,42 @@ import "./ModulesCommonEvents.sol"; * This contract contains the events for loan openings operations. * */ contract LoanOpeningsEvents is ModulesCommonEvents { - /// topic0: 0x7bd8cbb7ba34b33004f3deda0fd36c92fc0360acbd97843360037b467a538f90 - event Borrow( - address indexed user, - address indexed lender, - bytes32 indexed loanId, - address loanToken, - address collateralToken, - uint256 newPrincipal, - uint256 newCollateral, - uint256 interestRate, - uint256 interestDuration, - uint256 collateralToLoanRate, - uint256 currentMargin - ); + /// topic0: 0x7bd8cbb7ba34b33004f3deda0fd36c92fc0360acbd97843360037b467a538f90 + event Borrow( + address indexed user, + address indexed lender, + bytes32 indexed loanId, + address loanToken, + address collateralToken, + uint256 newPrincipal, + uint256 newCollateral, + uint256 interestRate, + uint256 interestDuration, + uint256 collateralToLoanRate, + uint256 currentMargin + ); - /// topic0: 0xf640c1cfe1a912a0b0152b5a542e5c2403142eed75b06cde526cee54b1580e5c - event Trade( - address indexed user, - address indexed lender, - bytes32 indexed loanId, - address collateralToken, - address loanToken, - uint256 positionSize, - uint256 borrowedAmount, - uint256 interestRate, - uint256 settlementDate, - uint256 entryPrice, /// one unit of collateralToken, denominated in loanToken - uint256 entryLeverage, - uint256 currentLeverage - ); + /// topic0: 0xf640c1cfe1a912a0b0152b5a542e5c2403142eed75b06cde526cee54b1580e5c + event Trade( + address indexed user, + address indexed lender, + bytes32 indexed loanId, + address collateralToken, + address loanToken, + uint256 positionSize, + uint256 borrowedAmount, + uint256 interestRate, + uint256 settlementDate, + uint256 entryPrice, /// one unit of collateralToken, denominated in loanToken + uint256 entryLeverage, + uint256 currentLeverage + ); - /// topic0: 0x0eef4f90457a741c97d76fcf13fa231fefdcc7649bdb3cb49157c37111c98433 - event DelegatedManagerSet(bytes32 indexed loanId, address indexed delegator, address indexed delegated, bool isActive); + /// topic0: 0x0eef4f90457a741c97d76fcf13fa231fefdcc7649bdb3cb49157c37111c98433 + event DelegatedManagerSet( + bytes32 indexed loanId, + address indexed delegator, + address indexed delegated, + bool isActive + ); } diff --git a/contracts/events/LoanSettingsEvents.sol b/contracts/events/LoanSettingsEvents.sol index 038e63896..df0e43306 100644 --- a/contracts/events/LoanSettingsEvents.sol +++ b/contracts/events/LoanSettingsEvents.sol @@ -15,25 +15,25 @@ import "./ModulesCommonEvents.sol"; * This contract contains the events for loan settings operations. * */ contract LoanSettingsEvents is ModulesCommonEvents { - event LoanParamsSetup( - bytes32 indexed id, - address owner, - address indexed loanToken, - address indexed collateralToken, - uint256 minInitialMargin, - uint256 maintenanceMargin, - uint256 maxLoanTerm - ); - event LoanParamsIdSetup(bytes32 indexed id, address indexed owner); + event LoanParamsSetup( + bytes32 indexed id, + address owner, + address indexed loanToken, + address indexed collateralToken, + uint256 minInitialMargin, + uint256 maintenanceMargin, + uint256 maxLoanTerm + ); + event LoanParamsIdSetup(bytes32 indexed id, address indexed owner); - event LoanParamsDisabled( - bytes32 indexed id, - address owner, - address indexed loanToken, - address indexed collateralToken, - uint256 minInitialMargin, - uint256 maintenanceMargin, - uint256 maxLoanTerm - ); - event LoanParamsIdDisabled(bytes32 indexed id, address indexed owner); + event LoanParamsDisabled( + bytes32 indexed id, + address owner, + address indexed loanToken, + address indexed collateralToken, + uint256 minInitialMargin, + uint256 maintenanceMargin, + uint256 maxLoanTerm + ); + event LoanParamsIdDisabled(bytes32 indexed id, address indexed owner); } diff --git a/contracts/events/ModulesCommonEvents.sol b/contracts/events/ModulesCommonEvents.sol index 0fa7054e5..8271d8375 100644 --- a/contracts/events/ModulesCommonEvents.sol +++ b/contracts/events/ModulesCommonEvents.sol @@ -6,9 +6,9 @@ pragma solidity 0.5.17; **/ contract ModulesCommonEvents { - event ProtocolModuleContractReplaced( - address indexed prevModuleContractAddress, - address indexed newModuleContractAddress, - bytes32 indexed module - ); + event ProtocolModuleContractReplaced( + address indexed prevModuleContractAddress, + address indexed newModuleContractAddress, + bytes32 indexed module + ); } diff --git a/contracts/events/ProtocolSettingsEvents.sol b/contracts/events/ProtocolSettingsEvents.sol index c90e3294c..8fbaab1c2 100644 --- a/contracts/events/ProtocolSettingsEvents.sol +++ b/contracts/events/ProtocolSettingsEvents.sol @@ -15,81 +15,148 @@ import "./ModulesCommonEvents.sol"; * This contract contains the events for protocol settings operations. * */ contract ProtocolSettingsEvents is ModulesCommonEvents { - event SetPriceFeedContract(address indexed sender, address oldValue, address newValue); - - event SetSwapsImplContract(address indexed sender, address oldValue, address newValue); - - event SetLoanPool(address indexed sender, address indexed loanPool, address indexed underlying); - - event SetSupportedTokens(address indexed sender, address indexed token, bool isActive); - - event SetLendingFeePercent(address indexed sender, uint256 oldValue, uint256 newValue); - - event SetTradingFeePercent(address indexed sender, uint256 oldValue, uint256 newValue); - - event SetBorrowingFeePercent(address indexed sender, uint256 oldValue, uint256 newValue); - - event SetSwapExternalFeePercent(address indexed sender, uint256 oldValue, uint256 newValue); - - event SetAffiliateFeePercent(address indexed sender, uint256 oldValue, uint256 newValue); - - event SetAffiliateTradingTokenFeePercent(address indexed sender, uint256 oldValue, uint256 newValue); - - event SetLiquidationIncentivePercent(address indexed sender, uint256 oldValue, uint256 newValue); - - event SetMaxSwapSize(address indexed sender, uint256 oldValue, uint256 newValue); - - event SetFeesController(address indexed sender, address indexed oldController, address indexed newController); - - event SetWrbtcToken(address indexed sender, address indexed oldWethToken, address indexed newWethToken); - - event SetSovrynSwapContractRegistryAddress( - address indexed sender, - address indexed oldSovrynSwapContractRegistryAddress, - address indexed newSovrynSwapContractRegistryAddress - ); - - event SetProtocolTokenAddress(address indexed sender, address indexed oldProtocolToken, address indexed newProtocolToken); - - event WithdrawFees( - address indexed sender, - address indexed token, - address indexed receiver, - uint256 lendingAmount, - uint256 tradingAmount, - uint256 borrowingAmount, - uint256 wRBTCConverted - ); - - event WithdrawLendingFees(address indexed sender, address indexed token, address indexed receiver, uint256 amount); - - event WithdrawTradingFees(address indexed sender, address indexed token, address indexed receiver, uint256 amount); - - event WithdrawBorrowingFees(address indexed sender, address indexed token, address indexed receiver, uint256 amount); - - event SetRolloverBaseReward(address indexed sender, uint256 oldValue, uint256 newValue); - - event SetRebatePercent(address indexed sender, uint256 oldRebatePercent, uint256 newRebatePercent); - - event SetSpecialRebates( - address indexed sender, - address indexed sourceToken, - address indexed destToken, - uint256 oldSpecialRebatesPercent, - uint256 newSpecialRebatesPercent - ); - - event SetProtocolAddress(address indexed sender, address indexed oldProtocol, address indexed newProtocol); - - event SetMinReferralsToPayoutAffiliates(address indexed sender, uint256 oldMinReferrals, uint256 newMinReferrals); - - event SetSOVTokenAddress(address indexed sender, address indexed oldTokenAddress, address indexed newTokenAddress); - - event SetLockedSOVAddress(address indexed sender, address indexed oldAddress, address indexed newAddress); - - event TogglePaused(address indexed sender, bool indexed oldFlag, bool indexed newFlag); - - event SetTradingRebateRewardsBasisPoint(address indexed sender, uint256 oldBasisPoint, uint256 newBasisPoint); - - event SetRolloverFlexFeePercent(address indexed sender, uint256 oldRolloverFlexFeePercent, uint256 newRolloverFlexFeePercent); + event SetPriceFeedContract(address indexed sender, address oldValue, address newValue); + + event SetSwapsImplContract(address indexed sender, address oldValue, address newValue); + + event SetLoanPool( + address indexed sender, + address indexed loanPool, + address indexed underlying + ); + + event SetSupportedTokens(address indexed sender, address indexed token, bool isActive); + + event SetLendingFeePercent(address indexed sender, uint256 oldValue, uint256 newValue); + + event SetTradingFeePercent(address indexed sender, uint256 oldValue, uint256 newValue); + + event SetBorrowingFeePercent(address indexed sender, uint256 oldValue, uint256 newValue); + + event SetSwapExternalFeePercent(address indexed sender, uint256 oldValue, uint256 newValue); + + event SetAffiliateFeePercent(address indexed sender, uint256 oldValue, uint256 newValue); + + event SetAffiliateTradingTokenFeePercent( + address indexed sender, + uint256 oldValue, + uint256 newValue + ); + + event SetLiquidationIncentivePercent( + address indexed sender, + uint256 oldValue, + uint256 newValue + ); + + event SetMaxSwapSize(address indexed sender, uint256 oldValue, uint256 newValue); + + event SetFeesController( + address indexed sender, + address indexed oldController, + address indexed newController + ); + + event SetWrbtcToken( + address indexed sender, + address indexed oldWethToken, + address indexed newWethToken + ); + + event SetSovrynSwapContractRegistryAddress( + address indexed sender, + address indexed oldSovrynSwapContractRegistryAddress, + address indexed newSovrynSwapContractRegistryAddress + ); + + event SetProtocolTokenAddress( + address indexed sender, + address indexed oldProtocolToken, + address indexed newProtocolToken + ); + + event WithdrawFees( + address indexed sender, + address indexed token, + address indexed receiver, + uint256 lendingAmount, + uint256 tradingAmount, + uint256 borrowingAmount, + uint256 wRBTCConverted + ); + + event WithdrawLendingFees( + address indexed sender, + address indexed token, + address indexed receiver, + uint256 amount + ); + + event WithdrawTradingFees( + address indexed sender, + address indexed token, + address indexed receiver, + uint256 amount + ); + + event WithdrawBorrowingFees( + address indexed sender, + address indexed token, + address indexed receiver, + uint256 amount + ); + + event SetRolloverBaseReward(address indexed sender, uint256 oldValue, uint256 newValue); + + event SetRebatePercent( + address indexed sender, + uint256 oldRebatePercent, + uint256 newRebatePercent + ); + + event SetSpecialRebates( + address indexed sender, + address indexed sourceToken, + address indexed destToken, + uint256 oldSpecialRebatesPercent, + uint256 newSpecialRebatesPercent + ); + + event SetProtocolAddress( + address indexed sender, + address indexed oldProtocol, + address indexed newProtocol + ); + + event SetMinReferralsToPayoutAffiliates( + address indexed sender, + uint256 oldMinReferrals, + uint256 newMinReferrals + ); + + event SetSOVTokenAddress( + address indexed sender, + address indexed oldTokenAddress, + address indexed newTokenAddress + ); + + event SetLockedSOVAddress( + address indexed sender, + address indexed oldAddress, + address indexed newAddress + ); + + event TogglePaused(address indexed sender, bool indexed oldFlag, bool indexed newFlag); + + event SetTradingRebateRewardsBasisPoint( + address indexed sender, + uint256 oldBasisPoint, + uint256 newBasisPoint + ); + + event SetRolloverFlexFeePercent( + address indexed sender, + uint256 oldRolloverFlexFeePercent, + uint256 newRolloverFlexFeePercent + ); } diff --git a/contracts/events/SwapsEvents.sol b/contracts/events/SwapsEvents.sol index 852dc1c0c..cf8ec5474 100644 --- a/contracts/events/SwapsEvents.sol +++ b/contracts/events/SwapsEvents.sol @@ -15,20 +15,20 @@ import "./ModulesCommonEvents.sol"; * This contract contains the events for swap operations. * */ contract SwapsEvents is ModulesCommonEvents { - event LoanSwap( - bytes32 indexed loanId, - address indexed sourceToken, - address indexed destToken, - address borrower, - uint256 sourceAmount, - uint256 destAmount - ); + event LoanSwap( + bytes32 indexed loanId, + address indexed sourceToken, + address indexed destToken, + address borrower, + uint256 sourceAmount, + uint256 destAmount + ); - event ExternalSwap( - address indexed user, - address indexed sourceToken, - address indexed destToken, - uint256 sourceAmount, - uint256 destAmount - ); + event ExternalSwap( + address indexed user, + address indexed sourceToken, + address indexed destToken, + uint256 sourceAmount, + uint256 destAmount + ); } diff --git a/contracts/farm/ILiquidityMining.sol b/contracts/farm/ILiquidityMining.sol index 084e8e02c..5c3f555f5 100644 --- a/contracts/farm/ILiquidityMining.sol +++ b/contracts/farm/ILiquidityMining.sol @@ -1,13 +1,16 @@ pragma solidity 0.5.17; interface ILiquidityMining { - function withdraw( - address _poolToken, - uint256 _amount, - address _user - ) external; + function withdraw( + address _poolToken, + uint256 _amount, + address _user + ) external; - function onTokensDeposited(address _user, uint256 _amount) external; + function onTokensDeposited(address _user, uint256 _amount) external; - function getUserPoolTokenBalance(address _poolToken, address _user) external view returns (uint256); + function getUserPoolTokenBalance(address _poolToken, address _user) + external + view + returns (uint256); } diff --git a/contracts/farm/LiquidityMining.sol b/contracts/farm/LiquidityMining.sol index 7e80c8652..81f655163 100644 --- a/contracts/farm/LiquidityMining.sol +++ b/contracts/farm/LiquidityMining.sol @@ -8,673 +8,729 @@ import "./LiquidityMiningStorage.sol"; import "./ILiquidityMining.sol"; contract LiquidityMining is ILiquidityMining, LiquidityMiningStorage { - using SafeMath for uint256; - using SafeERC20 for IERC20; - - /* Constants */ - - uint256 public constant PRECISION = 1e12; - // Bonus multiplier for early liquidity providers. - // During bonus period each passed block will be calculated like N passed blocks, where N = BONUS_MULTIPLIER - uint256 public constant BONUS_BLOCK_MULTIPLIER = 10; - - uint256 public constant SECONDS_PER_BLOCK = 30; - - /* Events */ - - event SOVTransferred(address indexed receiver, uint256 amount); - event PoolTokenAdded(address indexed user, address indexed poolToken, uint256 allocationPoint); - event PoolTokenUpdated(address indexed user, address indexed poolToken, uint256 newAllocationPoint, uint256 oldAllocationPoint); - event Deposit(address indexed user, address indexed poolToken, uint256 amount); - event RewardClaimed(address indexed user, address indexed poolToken, uint256 amount); - event Withdraw(address indexed user, address indexed poolToken, uint256 amount); - event EmergencyWithdraw(address indexed user, address indexed poolToken, uint256 amount, uint256 accumulatedReward); - - /* Functions */ - - /** - * @notice Initialize mining. - * - * @param _SOV The SOV token. - * @param _rewardTokensPerBlock The number of reward tokens per block. - * @param _startDelayBlocks The number of blocks should be passed to start - * mining. - * @param _numberOfBonusBlocks The number of blocks when each block will - * be calculated as N blocks (BONUS_BLOCK_MULTIPLIER). - * @param _lockedSOV The contract instance address of the lockedSOV vault. - * SOV rewards are not paid directly to liquidity providers. Instead they - * are deposited into a lockedSOV vault contract. - * @param _unlockedImmediatelyPercent The % which determines how much will be unlocked immediately. - */ - function initialize( - IERC20 _SOV, - uint256 _rewardTokensPerBlock, - uint256 _startDelayBlocks, - uint256 _numberOfBonusBlocks, - address _wrapper, - ILockedSOV _lockedSOV, - uint256 _unlockedImmediatelyPercent - ) external onlyAuthorized { - /// @dev Non-idempotent function. Must be called just once. - require(address(SOV) == address(0), "Already initialized"); - require(address(_SOV) != address(0), "Invalid token address"); - require(_startDelayBlocks > 0, "Invalid start block"); - require(_unlockedImmediatelyPercent < 10000, "Unlocked immediately percent has to be less than 10000."); - - SOV = _SOV; - rewardTokensPerBlock = _rewardTokensPerBlock; - startBlock = block.number + _startDelayBlocks; - bonusEndBlock = startBlock + _numberOfBonusBlocks; - wrapper = _wrapper; - lockedSOV = _lockedSOV; - unlockedImmediatelyPercent = _unlockedImmediatelyPercent; - } - - /** - * @notice Sets lockedSOV contract. - * @param _lockedSOV The contract instance address of the lockedSOV vault. - */ - function setLockedSOV(ILockedSOV _lockedSOV) external onlyAuthorized { - require(address(_lockedSOV) != address(0), "Invalid lockedSOV Address."); - lockedSOV = _lockedSOV; - } - - /** - * @notice Sets unlocked immediately percent. - * @param _unlockedImmediatelyPercent The % which determines how much will be unlocked immediately. - * @dev @dev 10000 is 100% - */ - function setUnlockedImmediatelyPercent(uint256 _unlockedImmediatelyPercent) external onlyAuthorized { - require(_unlockedImmediatelyPercent < 10000, "Unlocked immediately percent has to be less than 10000."); - unlockedImmediatelyPercent = _unlockedImmediatelyPercent; - } - - /** - * @notice sets wrapper proxy contract - * @dev can be set to zero address to remove wrapper - */ - function setWrapper(address _wrapper) external onlyAuthorized { - wrapper = _wrapper; - } - - /** - * @notice stops mining by setting end block - */ - function stopMining() external onlyAuthorized { - require(endBlock == 0, "Already stopped"); - - endBlock = block.number; - } - - /** - * @notice Transfers SOV tokens to given address. - * Owner use this function to withdraw SOV from LM contract - * into another account. - * @param _receiver The address of the SOV receiver. - * @param _amount The amount to be transferred. - * */ - function transferSOV(address _receiver, uint256 _amount) external onlyAuthorized { - require(_receiver != address(0), "Receiver address invalid"); - require(_amount != 0, "Amount invalid"); - - /// @dev Do not transfer more SOV than available. - uint256 SOVBal = SOV.balanceOf(address(this)); - if (_amount > SOVBal) { - _amount = SOVBal; - } - - /// @dev The actual transfer. - require(SOV.transfer(_receiver, _amount), "Transfer failed"); - - /// @dev Event log. - emit SOVTransferred(_receiver, _amount); - } - - /** - * @notice Get the missed SOV balance of LM contract. - * - * @return The amount of SOV tokens according to totalUsersBalance - * in excess of actual SOV balance of the LM contract. - * */ - function getMissedBalance() external view returns (uint256) { - uint256 balance = SOV.balanceOf(address(this)); - return balance >= totalUsersBalance ? 0 : totalUsersBalance.sub(balance); - } - - /** - * @notice adds a new lp to the pool. Can only be called by the owner or an admin - * @param _poolToken the address of pool token - * @param _allocationPoint the allocation point (weight) for the given pool - * @param _withUpdate the flag whether we need to update all pools - */ - function add( - address _poolToken, - uint96 _allocationPoint, - bool _withUpdate - ) external onlyAuthorized { - require(_allocationPoint > 0, "Invalid allocation point"); - require(_poolToken != address(0), "Invalid token address"); - require(poolIdList[_poolToken] == 0, "Token already added"); - - if (_withUpdate) { - updateAllPools(); - } - - uint256 lastRewardBlock = block.number > startBlock ? block.number : startBlock; - totalAllocationPoint = totalAllocationPoint.add(_allocationPoint); - - poolInfoList.push( - PoolInfo({ - poolToken: IERC20(_poolToken), - allocationPoint: _allocationPoint, - lastRewardBlock: lastRewardBlock, - accumulatedRewardPerShare: 0 - }) - ); - //indexing starts from 1 in order to check whether token was already added - poolIdList[_poolToken] = poolInfoList.length; - - emit PoolTokenAdded(msg.sender, _poolToken, _allocationPoint); - } - - /** - * @notice updates the given pool's reward tokens allocation point - * @param _poolToken the address of pool token - * @param _allocationPoint the allocation point (weight) for the given pool - * @param _updateAllFlag the flag whether we need to update all pools - */ - function update( - address _poolToken, - uint96 _allocationPoint, - bool _updateAllFlag - ) external onlyAuthorized { - if (_updateAllFlag) { - updateAllPools(); - } else { - updatePool(_poolToken); - } - _updateToken(_poolToken, _allocationPoint); - } - - function _updateToken(address _poolToken, uint96 _allocationPoint) internal { - uint256 poolId = _getPoolId(_poolToken); - - uint256 previousAllocationPoint = poolInfoList[poolId].allocationPoint; - totalAllocationPoint = totalAllocationPoint.sub(previousAllocationPoint).add(_allocationPoint); - poolInfoList[poolId].allocationPoint = _allocationPoint; - - emit PoolTokenUpdated(msg.sender, _poolToken, _allocationPoint, previousAllocationPoint); - } - - /** - * @notice updates the given pools' reward tokens allocation points - * @param _poolTokens array of addresses of pool tokens - * @param _allocationPoints array of allocation points (weight) for the given pools - * @param _updateAllFlag the flag whether we need to update all pools - */ - function updateTokens( - address[] calldata _poolTokens, - uint96[] calldata _allocationPoints, - bool _updateAllFlag - ) external onlyAuthorized { - require(_poolTokens.length == _allocationPoints.length, "Arrays mismatch"); - - if (_updateAllFlag) { - updateAllPools(); - } - uint256 length = _poolTokens.length; - for (uint256 i = 0; i < length; i++) { - if (!_updateAllFlag) { - updatePool(_poolTokens[i]); - } - _updateToken(_poolTokens[i], _allocationPoints[i]); - } - } - - /** - * @notice returns reward multiplier over the given _from to _to block - * @param _from the first block for a calculation - * @param _to the last block for a calculation - */ - function _getPassedBlocksWithBonusMultiplier(uint256 _from, uint256 _to) internal view returns (uint256) { - if (_from < startBlock) { - _from = startBlock; - } - if (endBlock > 0 && _to > endBlock) { - _to = endBlock; - } - if (_to <= bonusEndBlock) { - return _to.sub(_from).mul(BONUS_BLOCK_MULTIPLIER); - } else if (_from >= bonusEndBlock) { - return _to.sub(_from); - } else { - return bonusEndBlock.sub(_from).mul(BONUS_BLOCK_MULTIPLIER).add(_to.sub(bonusEndBlock)); - } - } - - function _getUserAccumulatedReward(uint256 _poolId, address _user) internal view returns (uint256) { - PoolInfo storage pool = poolInfoList[_poolId]; - UserInfo storage user = userInfoMap[_poolId][_user]; - - uint256 accumulatedRewardPerShare = pool.accumulatedRewardPerShare; - uint256 poolTokenBalance = pool.poolToken.balanceOf(address(this)); - if (block.number > pool.lastRewardBlock && poolTokenBalance != 0) { - (, uint256 accumulatedRewardPerShare_) = _getPoolAccumulatedReward(pool); - accumulatedRewardPerShare = accumulatedRewardPerShare.add(accumulatedRewardPerShare_); - } - return user.amount.mul(accumulatedRewardPerShare).div(PRECISION).sub(user.rewardDebt); - } - - /** - * @notice returns accumulated reward - * @param _poolToken the address of pool token - * @param _user the user address - */ - function getUserAccumulatedReward(address _poolToken, address _user) external view returns (uint256) { - uint256 poolId = _getPoolId(_poolToken); - return _getUserAccumulatedReward(poolId, _user); - } - - /** - * @notice returns estimated reward - * @param _poolToken the address of pool token - * @param _amount the amount of tokens to be deposited - * @param _duration the duration of liquidity providing in seconds - */ - function getEstimatedReward( - address _poolToken, - uint256 _amount, - uint256 _duration - ) external view returns (uint256) { - uint256 poolId = _getPoolId(_poolToken); - PoolInfo storage pool = poolInfoList[poolId]; - uint256 start = block.number; - uint256 end = start.add(_duration.div(SECONDS_PER_BLOCK)); - (, uint256 accumulatedRewardPerShare) = _getPoolAccumulatedReward(pool, _amount, start, end); - return _amount.mul(accumulatedRewardPerShare).div(PRECISION); - } - - /** - * @notice Updates reward variables for all pools. - * @dev Be careful of gas spending! - */ - function updateAllPools() public { - uint256 length = poolInfoList.length; - for (uint256 i = 0; i < length; i++) { - _updatePool(i); - } - } - - /** - * @notice Updates reward variables of the given pool to be up-to-date - * @param _poolToken the address of pool token - */ - function updatePool(address _poolToken) public { - uint256 poolId = _getPoolId(_poolToken); - _updatePool(poolId); - } - - function _updatePool(uint256 _poolId) internal { - PoolInfo storage pool = poolInfoList[_poolId]; - - //this pool has been updated recently - if (block.number <= pool.lastRewardBlock) { - return; - } - - uint256 poolTokenBalance = pool.poolToken.balanceOf(address(this)); - if (poolTokenBalance == 0) { - pool.lastRewardBlock = block.number; - return; - } - - (uint256 accumulatedReward_, uint256 accumulatedRewardPerShare_) = _getPoolAccumulatedReward(pool); - pool.accumulatedRewardPerShare = pool.accumulatedRewardPerShare.add(accumulatedRewardPerShare_); - pool.lastRewardBlock = block.number; - - totalUsersBalance = totalUsersBalance.add(accumulatedReward_); - } - - function _getPoolAccumulatedReward(PoolInfo storage _pool) internal view returns (uint256, uint256) { - return _getPoolAccumulatedReward(_pool, 0, _pool.lastRewardBlock, block.number); - } - - function _getPoolAccumulatedReward( - PoolInfo storage _pool, - uint256 _additionalAmount, - uint256 _startBlock, - uint256 _endBlock - ) internal view returns (uint256, uint256) { - uint256 passedBlocks = _getPassedBlocksWithBonusMultiplier(_startBlock, _endBlock); - uint256 accumulatedReward = passedBlocks.mul(rewardTokensPerBlock).mul(_pool.allocationPoint).div(totalAllocationPoint); - - uint256 poolTokenBalance = _pool.poolToken.balanceOf(address(this)); - poolTokenBalance = poolTokenBalance.add(_additionalAmount); - uint256 accumulatedRewardPerShare = accumulatedReward.mul(PRECISION).div(poolTokenBalance); - return (accumulatedReward, accumulatedRewardPerShare); - } - - /** - * @notice deposits pool tokens - * @param _poolToken the address of pool token - * @param _amount the amount of pool tokens - * @param _user the address of user, tokens will be deposited to it or to msg.sender - */ - function deposit( - address _poolToken, - uint256 _amount, - address _user - ) external { - _deposit(_poolToken, _amount, _user, false); - } - - /** - * @notice if the lending pools directly mint/transfer tokens to this address, process it like a user deposit - * @dev only callable by the pool which issues the tokens - * @param _user the user address - * @param _amount the minted amount - */ - function onTokensDeposited(address _user, uint256 _amount) external { - //the msg.sender is the pool token. if the msg.sender is not a valid pool token, _deposit will revert - _deposit(msg.sender, _amount, _user, true); - } - - /** - * @notice internal function for depositing pool tokens - * @param _poolToken the address of pool token - * @param _amount the amount of pool tokens - * @param _user the address of user, tokens will be deposited to it - * @param alreadyTransferred true if the pool tokens have already been transferred - */ - function _deposit( - address _poolToken, - uint256 _amount, - address _user, - bool alreadyTransferred - ) internal { - require(poolIdList[_poolToken] != 0, "Pool token not found"); - address userAddress = _user != address(0) ? _user : msg.sender; - - uint256 poolId = _getPoolId(_poolToken); - PoolInfo storage pool = poolInfoList[poolId]; - UserInfo storage user = userInfoMap[poolId][userAddress]; - - _updatePool(poolId); - //sends reward directly to the user - _updateReward(pool, user); - - if (_amount > 0) { - //receives pool tokens from msg.sender, it can be user or WrapperProxy contract - if (!alreadyTransferred) pool.poolToken.safeTransferFrom(address(msg.sender), address(this), _amount); - user.amount = user.amount.add(_amount); - } - _updateRewardDebt(pool, user); - emit Deposit(userAddress, _poolToken, _amount); - } - - /** - * @notice transfers reward tokens - * @param _poolToken the address of pool token - * @param _user the address of user to claim reward from (can be passed only by wrapper contract) - */ - function claimReward(address _poolToken, address _user) external { - address userAddress = _getUserAddress(_user); - - uint256 poolId = _getPoolId(_poolToken); - _claimReward(poolId, userAddress, true); - } - - function _claimReward( - uint256 _poolId, - address _userAddress, - bool _isStakingTokens - ) internal { - PoolInfo storage pool = poolInfoList[_poolId]; - UserInfo storage user = userInfoMap[_poolId][_userAddress]; - - _updatePool(_poolId); - _updateReward(pool, user); - _transferReward(address(pool.poolToken), user, _userAddress, _isStakingTokens, true); - _updateRewardDebt(pool, user); - } - - /** - * @notice transfers reward tokens from all pools - * @param _user the address of user to claim reward from (can be passed only by wrapper contract) - */ - function claimRewardFromAllPools(address _user) external { - address userAddress = _getUserAddress(_user); - - uint256 length = poolInfoList.length; - for (uint256 i = 0; i < length; i++) { - uint256 poolId = i; - _claimReward(poolId, userAddress, false); - } - lockedSOV.withdrawAndStakeTokensFrom(userAddress); - } - - /** - * @notice withdraws pool tokens and transfers reward tokens - * @param _poolToken the address of pool token - * @param _amount the amount of pool tokens - * @param _user the user address will be used to process a withdrawal (can be passed only by wrapper contract) - */ - function withdraw( - address _poolToken, - uint256 _amount, - address _user - ) external { - require(poolIdList[_poolToken] != 0, "Pool token not found"); - address userAddress = _getUserAddress(_user); - - uint256 poolId = _getPoolId(_poolToken); - PoolInfo storage pool = poolInfoList[poolId]; - UserInfo storage user = userInfoMap[poolId][userAddress]; - require(user.amount >= _amount, "Not enough balance"); - - _updatePool(poolId); - _updateReward(pool, user); - _transferReward(_poolToken, user, userAddress, false, false); - - user.amount = user.amount.sub(_amount); - - //msg.sender is wrapper -> send to wrapper - if (msg.sender == wrapper) { - pool.poolToken.safeTransfer(address(msg.sender), _amount); - } - //msg.sender is user or pool token (lending pool) -> send to user - else { - pool.poolToken.safeTransfer(userAddress, _amount); - } - - _updateRewardDebt(pool, user); - emit Withdraw(userAddress, _poolToken, _amount); - } - - function _getUserAddress(address _user) internal view returns (address) { - address userAddress = msg.sender; - if (_user != address(0)) { - //only wrapper can pass _user parameter - require(msg.sender == wrapper || poolIdList[msg.sender] != 0, "only wrapper or pools may withdraw for a user"); - userAddress = _user; - } - return userAddress; - } - - function _updateReward(PoolInfo storage pool, UserInfo storage user) internal { - //update user accumulated reward - if (user.amount > 0) { - //add reward for the previous amount of deposited tokens - uint256 accumulatedReward = user.amount.mul(pool.accumulatedRewardPerShare).div(PRECISION).sub(user.rewardDebt); - user.accumulatedReward = user.accumulatedReward.add(accumulatedReward); - } - } - - function _updateRewardDebt(PoolInfo storage pool, UserInfo storage user) internal { - //reward accumulated before amount update (should be subtracted during next reward calculation) - user.rewardDebt = user.amount.mul(pool.accumulatedRewardPerShare).div(PRECISION); - } - - /** - * @notice Send reward in SOV to the lockedSOV vault. - * @param _user The user info, to get its reward share. - * @param _userAddress The address of the user, to send SOV in its behalf. - * @param _isStakingTokens The flag whether we need to stake tokens - * @param _isCheckingBalance The flag whether we need to throw error or don't process reward if SOV balance isn't enough - */ - function _transferReward( - address _poolToken, - UserInfo storage _user, - address _userAddress, - bool _isStakingTokens, - bool _isCheckingBalance - ) internal { - uint256 userAccumulatedReward = _user.accumulatedReward; - - /// @dev Transfer if enough SOV balance on this LM contract. - uint256 balance = SOV.balanceOf(address(this)); - if (balance >= userAccumulatedReward) { - totalUsersBalance = totalUsersBalance.sub(userAccumulatedReward); - _user.accumulatedReward = 0; - - /// @dev Instead of transferring the reward to the LP (user), - /// deposit it into lockedSOV vault contract, but first - /// SOV deposit must be approved to move the SOV tokens - /// from this LM contract into the lockedSOV vault. - require(SOV.approve(address(lockedSOV), userAccumulatedReward), "Approve failed"); - lockedSOV.deposit(_userAddress, userAccumulatedReward, unlockedImmediatelyPercent); - - if (_isStakingTokens) { - lockedSOV.withdrawAndStakeTokensFrom(_userAddress); - } - - /// @dev Event log. - emit RewardClaimed(_userAddress, _poolToken, userAccumulatedReward); - } else { - require(!_isCheckingBalance, "Claiming reward failed"); - } - } - - /** - * @notice withdraws pool tokens without transferring reward tokens - * @param _poolToken the address of pool token - * @dev EMERGENCY ONLY - */ - function emergencyWithdraw(address _poolToken) external { - uint256 poolId = _getPoolId(_poolToken); - PoolInfo storage pool = poolInfoList[poolId]; - UserInfo storage user = userInfoMap[poolId][msg.sender]; - - _updatePool(poolId); - _updateReward(pool, user); - - totalUsersBalance = totalUsersBalance.sub(user.accumulatedReward); - uint256 userAmount = user.amount; - uint256 userAccumulatedReward = user.accumulatedReward; - user.amount = 0; - user.rewardDebt = 0; - user.accumulatedReward = 0; - pool.poolToken.safeTransfer(address(msg.sender), userAmount); - - _updateRewardDebt(pool, user); - - emit EmergencyWithdraw(msg.sender, _poolToken, userAmount, userAccumulatedReward); - } - - /** - * @notice returns pool id - * @param _poolToken the address of pool token - */ - function getPoolId(address _poolToken) external view returns (uint256) { - return _getPoolId(_poolToken); - } - - function _getPoolId(address _poolToken) internal view returns (uint256) { - uint256 poolId = poolIdList[_poolToken]; - require(poolId > 0, "Pool token not found"); - return poolId - 1; - } - - /** - * @notice returns count of pool tokens - */ - function getPoolLength() external view returns (uint256) { - return poolInfoList.length; - } - - /** - * @notice returns list of pool token's info - */ - function getPoolInfoList() external view returns (PoolInfo[] memory) { - return poolInfoList; - } - - /** - * @notice returns pool info for the given token - * @param _poolToken the address of pool token - */ - function getPoolInfo(address _poolToken) external view returns (PoolInfo memory) { - uint256 poolId = _getPoolId(_poolToken); - return poolInfoList[poolId]; - } - - /** - * @notice returns list of [amount, accumulatedReward] for the given user for each pool token - * @param _user the address of the user - */ - function getUserBalanceList(address _user) external view returns (uint256[2][] memory) { - uint256 length = poolInfoList.length; - uint256[2][] memory userBalanceList = new uint256[2][](length); - for (uint256 i = 0; i < length; i++) { - userBalanceList[i][0] = userInfoMap[i][_user].amount; - userBalanceList[i][1] = _getUserAccumulatedReward(i, _user); - } - return userBalanceList; - } - - /** - * @notice returns UserInfo for the given pool and user - * @param _poolToken the address of pool token - * @param _user the address of the user - */ - function getUserInfo(address _poolToken, address _user) public view returns (UserInfo memory) { - uint256 poolId = _getPoolId(_poolToken); - return userInfoMap[poolId][_user]; - } - - /** - * @notice returns list of UserInfo for the given user for each pool token - * @param _user the address of the user - */ - function getUserInfoList(address _user) external view returns (UserInfo[] memory) { - uint256 length = poolInfoList.length; - UserInfo[] memory userInfoList = new UserInfo[](length); - for (uint256 i = 0; i < length; i++) { - userInfoList[i] = userInfoMap[i][_user]; - } - return userInfoList; - } - - /** - * @notice returns accumulated reward for the given user for each pool token - * @param _user the address of the user - */ - function getUserAccumulatedRewardList(address _user) external view returns (uint256[] memory) { - uint256 length = poolInfoList.length; - uint256[] memory rewardList = new uint256[](length); - for (uint256 i = 0; i < length; i++) { - rewardList[i] = _getUserAccumulatedReward(i, _user); - } - return rewardList; - } - - /** - * @notice returns the pool token balance a user has on the contract - * @param _poolToken the address of pool token - * @param _user the address of the user - */ - function getUserPoolTokenBalance(address _poolToken, address _user) external view returns (uint256) { - UserInfo memory ui = getUserInfo(_poolToken, _user); - return ui.amount; - } + using SafeMath for uint256; + using SafeERC20 for IERC20; + + /* Constants */ + + uint256 public constant PRECISION = 1e12; + // Bonus multiplier for early liquidity providers. + // During bonus period each passed block will be calculated like N passed blocks, where N = BONUS_MULTIPLIER + uint256 public constant BONUS_BLOCK_MULTIPLIER = 10; + + uint256 public constant SECONDS_PER_BLOCK = 30; + + /* Events */ + + event SOVTransferred(address indexed receiver, uint256 amount); + event PoolTokenAdded(address indexed user, address indexed poolToken, uint256 allocationPoint); + event PoolTokenUpdated( + address indexed user, + address indexed poolToken, + uint256 newAllocationPoint, + uint256 oldAllocationPoint + ); + event Deposit(address indexed user, address indexed poolToken, uint256 amount); + event RewardClaimed(address indexed user, address indexed poolToken, uint256 amount); + event Withdraw(address indexed user, address indexed poolToken, uint256 amount); + event EmergencyWithdraw( + address indexed user, + address indexed poolToken, + uint256 amount, + uint256 accumulatedReward + ); + + /* Functions */ + + /** + * @notice Initialize mining. + * + * @param _SOV The SOV token. + * @param _rewardTokensPerBlock The number of reward tokens per block. + * @param _startDelayBlocks The number of blocks should be passed to start + * mining. + * @param _numberOfBonusBlocks The number of blocks when each block will + * be calculated as N blocks (BONUS_BLOCK_MULTIPLIER). + * @param _lockedSOV The contract instance address of the lockedSOV vault. + * SOV rewards are not paid directly to liquidity providers. Instead they + * are deposited into a lockedSOV vault contract. + * @param _unlockedImmediatelyPercent The % which determines how much will be unlocked immediately. + */ + function initialize( + IERC20 _SOV, + uint256 _rewardTokensPerBlock, + uint256 _startDelayBlocks, + uint256 _numberOfBonusBlocks, + address _wrapper, + ILockedSOV _lockedSOV, + uint256 _unlockedImmediatelyPercent + ) external onlyAuthorized { + /// @dev Non-idempotent function. Must be called just once. + require(address(SOV) == address(0), "Already initialized"); + require(address(_SOV) != address(0), "Invalid token address"); + require(_startDelayBlocks > 0, "Invalid start block"); + require( + _unlockedImmediatelyPercent < 10000, + "Unlocked immediately percent has to be less than 10000." + ); + + SOV = _SOV; + rewardTokensPerBlock = _rewardTokensPerBlock; + startBlock = block.number + _startDelayBlocks; + bonusEndBlock = startBlock + _numberOfBonusBlocks; + wrapper = _wrapper; + lockedSOV = _lockedSOV; + unlockedImmediatelyPercent = _unlockedImmediatelyPercent; + } + + /** + * @notice Sets lockedSOV contract. + * @param _lockedSOV The contract instance address of the lockedSOV vault. + */ + function setLockedSOV(ILockedSOV _lockedSOV) external onlyAuthorized { + require(address(_lockedSOV) != address(0), "Invalid lockedSOV Address."); + lockedSOV = _lockedSOV; + } + + /** + * @notice Sets unlocked immediately percent. + * @param _unlockedImmediatelyPercent The % which determines how much will be unlocked immediately. + * @dev @dev 10000 is 100% + */ + function setUnlockedImmediatelyPercent(uint256 _unlockedImmediatelyPercent) + external + onlyAuthorized + { + require( + _unlockedImmediatelyPercent < 10000, + "Unlocked immediately percent has to be less than 10000." + ); + unlockedImmediatelyPercent = _unlockedImmediatelyPercent; + } + + /** + * @notice sets wrapper proxy contract + * @dev can be set to zero address to remove wrapper + */ + function setWrapper(address _wrapper) external onlyAuthorized { + wrapper = _wrapper; + } + + /** + * @notice stops mining by setting end block + */ + function stopMining() external onlyAuthorized { + require(endBlock == 0, "Already stopped"); + + endBlock = block.number; + } + + /** + * @notice Transfers SOV tokens to given address. + * Owner use this function to withdraw SOV from LM contract + * into another account. + * @param _receiver The address of the SOV receiver. + * @param _amount The amount to be transferred. + * */ + function transferSOV(address _receiver, uint256 _amount) external onlyAuthorized { + require(_receiver != address(0), "Receiver address invalid"); + require(_amount != 0, "Amount invalid"); + + /// @dev Do not transfer more SOV than available. + uint256 SOVBal = SOV.balanceOf(address(this)); + if (_amount > SOVBal) { + _amount = SOVBal; + } + + /// @dev The actual transfer. + require(SOV.transfer(_receiver, _amount), "Transfer failed"); + + /// @dev Event log. + emit SOVTransferred(_receiver, _amount); + } + + /** + * @notice Get the missed SOV balance of LM contract. + * + * @return The amount of SOV tokens according to totalUsersBalance + * in excess of actual SOV balance of the LM contract. + * */ + function getMissedBalance() external view returns (uint256) { + uint256 balance = SOV.balanceOf(address(this)); + return balance >= totalUsersBalance ? 0 : totalUsersBalance.sub(balance); + } + + /** + * @notice adds a new lp to the pool. Can only be called by the owner or an admin + * @param _poolToken the address of pool token + * @param _allocationPoint the allocation point (weight) for the given pool + * @param _withUpdate the flag whether we need to update all pools + */ + function add( + address _poolToken, + uint96 _allocationPoint, + bool _withUpdate + ) external onlyAuthorized { + require(_allocationPoint > 0, "Invalid allocation point"); + require(_poolToken != address(0), "Invalid token address"); + require(poolIdList[_poolToken] == 0, "Token already added"); + + if (_withUpdate) { + updateAllPools(); + } + + uint256 lastRewardBlock = block.number > startBlock ? block.number : startBlock; + totalAllocationPoint = totalAllocationPoint.add(_allocationPoint); + + poolInfoList.push( + PoolInfo({ + poolToken: IERC20(_poolToken), + allocationPoint: _allocationPoint, + lastRewardBlock: lastRewardBlock, + accumulatedRewardPerShare: 0 + }) + ); + //indexing starts from 1 in order to check whether token was already added + poolIdList[_poolToken] = poolInfoList.length; + + emit PoolTokenAdded(msg.sender, _poolToken, _allocationPoint); + } + + /** + * @notice updates the given pool's reward tokens allocation point + * @param _poolToken the address of pool token + * @param _allocationPoint the allocation point (weight) for the given pool + * @param _updateAllFlag the flag whether we need to update all pools + */ + function update( + address _poolToken, + uint96 _allocationPoint, + bool _updateAllFlag + ) external onlyAuthorized { + if (_updateAllFlag) { + updateAllPools(); + } else { + updatePool(_poolToken); + } + _updateToken(_poolToken, _allocationPoint); + } + + function _updateToken(address _poolToken, uint96 _allocationPoint) internal { + uint256 poolId = _getPoolId(_poolToken); + + uint256 previousAllocationPoint = poolInfoList[poolId].allocationPoint; + totalAllocationPoint = totalAllocationPoint.sub(previousAllocationPoint).add( + _allocationPoint + ); + poolInfoList[poolId].allocationPoint = _allocationPoint; + + emit PoolTokenUpdated(msg.sender, _poolToken, _allocationPoint, previousAllocationPoint); + } + + /** + * @notice updates the given pools' reward tokens allocation points + * @param _poolTokens array of addresses of pool tokens + * @param _allocationPoints array of allocation points (weight) for the given pools + * @param _updateAllFlag the flag whether we need to update all pools + */ + function updateTokens( + address[] calldata _poolTokens, + uint96[] calldata _allocationPoints, + bool _updateAllFlag + ) external onlyAuthorized { + require(_poolTokens.length == _allocationPoints.length, "Arrays mismatch"); + + if (_updateAllFlag) { + updateAllPools(); + } + uint256 length = _poolTokens.length; + for (uint256 i = 0; i < length; i++) { + if (!_updateAllFlag) { + updatePool(_poolTokens[i]); + } + _updateToken(_poolTokens[i], _allocationPoints[i]); + } + } + + /** + * @notice returns reward multiplier over the given _from to _to block + * @param _from the first block for a calculation + * @param _to the last block for a calculation + */ + function _getPassedBlocksWithBonusMultiplier(uint256 _from, uint256 _to) + internal + view + returns (uint256) + { + if (_from < startBlock) { + _from = startBlock; + } + if (endBlock > 0 && _to > endBlock) { + _to = endBlock; + } + if (_to <= bonusEndBlock) { + return _to.sub(_from).mul(BONUS_BLOCK_MULTIPLIER); + } else if (_from >= bonusEndBlock) { + return _to.sub(_from); + } else { + return + bonusEndBlock.sub(_from).mul(BONUS_BLOCK_MULTIPLIER).add(_to.sub(bonusEndBlock)); + } + } + + function _getUserAccumulatedReward(uint256 _poolId, address _user) + internal + view + returns (uint256) + { + PoolInfo storage pool = poolInfoList[_poolId]; + UserInfo storage user = userInfoMap[_poolId][_user]; + + uint256 accumulatedRewardPerShare = pool.accumulatedRewardPerShare; + uint256 poolTokenBalance = pool.poolToken.balanceOf(address(this)); + if (block.number > pool.lastRewardBlock && poolTokenBalance != 0) { + (, uint256 accumulatedRewardPerShare_) = _getPoolAccumulatedReward(pool); + accumulatedRewardPerShare = accumulatedRewardPerShare.add(accumulatedRewardPerShare_); + } + return user.amount.mul(accumulatedRewardPerShare).div(PRECISION).sub(user.rewardDebt); + } + + /** + * @notice returns accumulated reward + * @param _poolToken the address of pool token + * @param _user the user address + */ + function getUserAccumulatedReward(address _poolToken, address _user) + external + view + returns (uint256) + { + uint256 poolId = _getPoolId(_poolToken); + return _getUserAccumulatedReward(poolId, _user); + } + + /** + * @notice returns estimated reward + * @param _poolToken the address of pool token + * @param _amount the amount of tokens to be deposited + * @param _duration the duration of liquidity providing in seconds + */ + function getEstimatedReward( + address _poolToken, + uint256 _amount, + uint256 _duration + ) external view returns (uint256) { + uint256 poolId = _getPoolId(_poolToken); + PoolInfo storage pool = poolInfoList[poolId]; + uint256 start = block.number; + uint256 end = start.add(_duration.div(SECONDS_PER_BLOCK)); + (, uint256 accumulatedRewardPerShare) = + _getPoolAccumulatedReward(pool, _amount, start, end); + return _amount.mul(accumulatedRewardPerShare).div(PRECISION); + } + + /** + * @notice Updates reward variables for all pools. + * @dev Be careful of gas spending! + */ + function updateAllPools() public { + uint256 length = poolInfoList.length; + for (uint256 i = 0; i < length; i++) { + _updatePool(i); + } + } + + /** + * @notice Updates reward variables of the given pool to be up-to-date + * @param _poolToken the address of pool token + */ + function updatePool(address _poolToken) public { + uint256 poolId = _getPoolId(_poolToken); + _updatePool(poolId); + } + + function _updatePool(uint256 _poolId) internal { + PoolInfo storage pool = poolInfoList[_poolId]; + + //this pool has been updated recently + if (block.number <= pool.lastRewardBlock) { + return; + } + + uint256 poolTokenBalance = pool.poolToken.balanceOf(address(this)); + if (poolTokenBalance == 0) { + pool.lastRewardBlock = block.number; + return; + } + + (uint256 accumulatedReward_, uint256 accumulatedRewardPerShare_) = + _getPoolAccumulatedReward(pool); + pool.accumulatedRewardPerShare = pool.accumulatedRewardPerShare.add( + accumulatedRewardPerShare_ + ); + pool.lastRewardBlock = block.number; + + totalUsersBalance = totalUsersBalance.add(accumulatedReward_); + } + + function _getPoolAccumulatedReward(PoolInfo storage _pool) + internal + view + returns (uint256, uint256) + { + return _getPoolAccumulatedReward(_pool, 0, _pool.lastRewardBlock, block.number); + } + + function _getPoolAccumulatedReward( + PoolInfo storage _pool, + uint256 _additionalAmount, + uint256 _startBlock, + uint256 _endBlock + ) internal view returns (uint256, uint256) { + uint256 passedBlocks = _getPassedBlocksWithBonusMultiplier(_startBlock, _endBlock); + uint256 accumulatedReward = + passedBlocks.mul(rewardTokensPerBlock).mul(_pool.allocationPoint).div( + totalAllocationPoint + ); + + uint256 poolTokenBalance = _pool.poolToken.balanceOf(address(this)); + poolTokenBalance = poolTokenBalance.add(_additionalAmount); + uint256 accumulatedRewardPerShare = accumulatedReward.mul(PRECISION).div(poolTokenBalance); + return (accumulatedReward, accumulatedRewardPerShare); + } + + /** + * @notice deposits pool tokens + * @param _poolToken the address of pool token + * @param _amount the amount of pool tokens + * @param _user the address of user, tokens will be deposited to it or to msg.sender + */ + function deposit( + address _poolToken, + uint256 _amount, + address _user + ) external { + _deposit(_poolToken, _amount, _user, false); + } + + /** + * @notice if the lending pools directly mint/transfer tokens to this address, process it like a user deposit + * @dev only callable by the pool which issues the tokens + * @param _user the user address + * @param _amount the minted amount + */ + function onTokensDeposited(address _user, uint256 _amount) external { + //the msg.sender is the pool token. if the msg.sender is not a valid pool token, _deposit will revert + _deposit(msg.sender, _amount, _user, true); + } + + /** + * @notice internal function for depositing pool tokens + * @param _poolToken the address of pool token + * @param _amount the amount of pool tokens + * @param _user the address of user, tokens will be deposited to it + * @param alreadyTransferred true if the pool tokens have already been transferred + */ + function _deposit( + address _poolToken, + uint256 _amount, + address _user, + bool alreadyTransferred + ) internal { + require(poolIdList[_poolToken] != 0, "Pool token not found"); + address userAddress = _user != address(0) ? _user : msg.sender; + + uint256 poolId = _getPoolId(_poolToken); + PoolInfo storage pool = poolInfoList[poolId]; + UserInfo storage user = userInfoMap[poolId][userAddress]; + + _updatePool(poolId); + //sends reward directly to the user + _updateReward(pool, user); + + if (_amount > 0) { + //receives pool tokens from msg.sender, it can be user or WrapperProxy contract + if (!alreadyTransferred) + pool.poolToken.safeTransferFrom(address(msg.sender), address(this), _amount); + user.amount = user.amount.add(_amount); + } + _updateRewardDebt(pool, user); + emit Deposit(userAddress, _poolToken, _amount); + } + + /** + * @notice transfers reward tokens + * @param _poolToken the address of pool token + * @param _user the address of user to claim reward from (can be passed only by wrapper contract) + */ + function claimReward(address _poolToken, address _user) external { + address userAddress = _getUserAddress(_user); + + uint256 poolId = _getPoolId(_poolToken); + _claimReward(poolId, userAddress, true); + } + + function _claimReward( + uint256 _poolId, + address _userAddress, + bool _isStakingTokens + ) internal { + PoolInfo storage pool = poolInfoList[_poolId]; + UserInfo storage user = userInfoMap[_poolId][_userAddress]; + + _updatePool(_poolId); + _updateReward(pool, user); + _transferReward(address(pool.poolToken), user, _userAddress, _isStakingTokens, true); + _updateRewardDebt(pool, user); + } + + /** + * @notice transfers reward tokens from all pools + * @param _user the address of user to claim reward from (can be passed only by wrapper contract) + */ + function claimRewardFromAllPools(address _user) external { + address userAddress = _getUserAddress(_user); + + uint256 length = poolInfoList.length; + for (uint256 i = 0; i < length; i++) { + uint256 poolId = i; + _claimReward(poolId, userAddress, false); + } + lockedSOV.withdrawAndStakeTokensFrom(userAddress); + } + + /** + * @notice withdraws pool tokens and transfers reward tokens + * @param _poolToken the address of pool token + * @param _amount the amount of pool tokens + * @param _user the user address will be used to process a withdrawal (can be passed only by wrapper contract) + */ + function withdraw( + address _poolToken, + uint256 _amount, + address _user + ) external { + require(poolIdList[_poolToken] != 0, "Pool token not found"); + address userAddress = _getUserAddress(_user); + + uint256 poolId = _getPoolId(_poolToken); + PoolInfo storage pool = poolInfoList[poolId]; + UserInfo storage user = userInfoMap[poolId][userAddress]; + require(user.amount >= _amount, "Not enough balance"); + + _updatePool(poolId); + _updateReward(pool, user); + _transferReward(_poolToken, user, userAddress, false, false); + + user.amount = user.amount.sub(_amount); + + //msg.sender is wrapper -> send to wrapper + if (msg.sender == wrapper) { + pool.poolToken.safeTransfer(address(msg.sender), _amount); + } + //msg.sender is user or pool token (lending pool) -> send to user + else { + pool.poolToken.safeTransfer(userAddress, _amount); + } + + _updateRewardDebt(pool, user); + emit Withdraw(userAddress, _poolToken, _amount); + } + + function _getUserAddress(address _user) internal view returns (address) { + address userAddress = msg.sender; + if (_user != address(0)) { + //only wrapper can pass _user parameter + require( + msg.sender == wrapper || poolIdList[msg.sender] != 0, + "only wrapper or pools may withdraw for a user" + ); + userAddress = _user; + } + return userAddress; + } + + function _updateReward(PoolInfo storage pool, UserInfo storage user) internal { + //update user accumulated reward + if (user.amount > 0) { + //add reward for the previous amount of deposited tokens + uint256 accumulatedReward = + user.amount.mul(pool.accumulatedRewardPerShare).div(PRECISION).sub( + user.rewardDebt + ); + user.accumulatedReward = user.accumulatedReward.add(accumulatedReward); + } + } + + function _updateRewardDebt(PoolInfo storage pool, UserInfo storage user) internal { + //reward accumulated before amount update (should be subtracted during next reward calculation) + user.rewardDebt = user.amount.mul(pool.accumulatedRewardPerShare).div(PRECISION); + } + + /** + * @notice Send reward in SOV to the lockedSOV vault. + * @param _user The user info, to get its reward share. + * @param _userAddress The address of the user, to send SOV in its behalf. + * @param _isStakingTokens The flag whether we need to stake tokens + * @param _isCheckingBalance The flag whether we need to throw error or don't process reward if SOV balance isn't enough + */ + function _transferReward( + address _poolToken, + UserInfo storage _user, + address _userAddress, + bool _isStakingTokens, + bool _isCheckingBalance + ) internal { + uint256 userAccumulatedReward = _user.accumulatedReward; + + /// @dev Transfer if enough SOV balance on this LM contract. + uint256 balance = SOV.balanceOf(address(this)); + if (balance >= userAccumulatedReward) { + totalUsersBalance = totalUsersBalance.sub(userAccumulatedReward); + _user.accumulatedReward = 0; + + /// @dev Instead of transferring the reward to the LP (user), + /// deposit it into lockedSOV vault contract, but first + /// SOV deposit must be approved to move the SOV tokens + /// from this LM contract into the lockedSOV vault. + require(SOV.approve(address(lockedSOV), userAccumulatedReward), "Approve failed"); + lockedSOV.deposit(_userAddress, userAccumulatedReward, unlockedImmediatelyPercent); + + if (_isStakingTokens) { + lockedSOV.withdrawAndStakeTokensFrom(_userAddress); + } + + /// @dev Event log. + emit RewardClaimed(_userAddress, _poolToken, userAccumulatedReward); + } else { + require(!_isCheckingBalance, "Claiming reward failed"); + } + } + + /** + * @notice withdraws pool tokens without transferring reward tokens + * @param _poolToken the address of pool token + * @dev EMERGENCY ONLY + */ + function emergencyWithdraw(address _poolToken) external { + uint256 poolId = _getPoolId(_poolToken); + PoolInfo storage pool = poolInfoList[poolId]; + UserInfo storage user = userInfoMap[poolId][msg.sender]; + + _updatePool(poolId); + _updateReward(pool, user); + + totalUsersBalance = totalUsersBalance.sub(user.accumulatedReward); + uint256 userAmount = user.amount; + uint256 userAccumulatedReward = user.accumulatedReward; + user.amount = 0; + user.rewardDebt = 0; + user.accumulatedReward = 0; + pool.poolToken.safeTransfer(address(msg.sender), userAmount); + + _updateRewardDebt(pool, user); + + emit EmergencyWithdraw(msg.sender, _poolToken, userAmount, userAccumulatedReward); + } + + /** + * @notice returns pool id + * @param _poolToken the address of pool token + */ + function getPoolId(address _poolToken) external view returns (uint256) { + return _getPoolId(_poolToken); + } + + function _getPoolId(address _poolToken) internal view returns (uint256) { + uint256 poolId = poolIdList[_poolToken]; + require(poolId > 0, "Pool token not found"); + return poolId - 1; + } + + /** + * @notice returns count of pool tokens + */ + function getPoolLength() external view returns (uint256) { + return poolInfoList.length; + } + + /** + * @notice returns list of pool token's info + */ + function getPoolInfoList() external view returns (PoolInfo[] memory) { + return poolInfoList; + } + + /** + * @notice returns pool info for the given token + * @param _poolToken the address of pool token + */ + function getPoolInfo(address _poolToken) external view returns (PoolInfo memory) { + uint256 poolId = _getPoolId(_poolToken); + return poolInfoList[poolId]; + } + + /** + * @notice returns list of [amount, accumulatedReward] for the given user for each pool token + * @param _user the address of the user + */ + function getUserBalanceList(address _user) external view returns (uint256[2][] memory) { + uint256 length = poolInfoList.length; + uint256[2][] memory userBalanceList = new uint256[2][](length); + for (uint256 i = 0; i < length; i++) { + userBalanceList[i][0] = userInfoMap[i][_user].amount; + userBalanceList[i][1] = _getUserAccumulatedReward(i, _user); + } + return userBalanceList; + } + + /** + * @notice returns UserInfo for the given pool and user + * @param _poolToken the address of pool token + * @param _user the address of the user + */ + function getUserInfo(address _poolToken, address _user) public view returns (UserInfo memory) { + uint256 poolId = _getPoolId(_poolToken); + return userInfoMap[poolId][_user]; + } + + /** + * @notice returns list of UserInfo for the given user for each pool token + * @param _user the address of the user + */ + function getUserInfoList(address _user) external view returns (UserInfo[] memory) { + uint256 length = poolInfoList.length; + UserInfo[] memory userInfoList = new UserInfo[](length); + for (uint256 i = 0; i < length; i++) { + userInfoList[i] = userInfoMap[i][_user]; + } + return userInfoList; + } + + /** + * @notice returns accumulated reward for the given user for each pool token + * @param _user the address of the user + */ + function getUserAccumulatedRewardList(address _user) external view returns (uint256[] memory) { + uint256 length = poolInfoList.length; + uint256[] memory rewardList = new uint256[](length); + for (uint256 i = 0; i < length; i++) { + rewardList[i] = _getUserAccumulatedReward(i, _user); + } + return rewardList; + } + + /** + * @notice returns the pool token balance a user has on the contract + * @param _poolToken the address of pool token + * @param _user the address of the user + */ + function getUserPoolTokenBalance(address _poolToken, address _user) + external + view + returns (uint256) + { + UserInfo memory ui = getUserInfo(_poolToken, _user); + return ui.amount; + } } diff --git a/contracts/farm/LiquidityMiningConfigToken.sol b/contracts/farm/LiquidityMiningConfigToken.sol index 19803a528..9d6ac523d 100644 --- a/contracts/farm/LiquidityMiningConfigToken.sol +++ b/contracts/farm/LiquidityMiningConfigToken.sol @@ -8,31 +8,31 @@ import "../openzeppelin/IERC20_.sol"; * @dev We need this token for having a flexibility with LiquidityMining configuration */ contract LiquidityMiningConfigToken is IERC20_ { - function totalSupply() external view returns (uint256) { - return 0; - } + function totalSupply() external view returns (uint256) { + return 0; + } - function balanceOf(address account) external view returns (uint256) { - return 0; - } + function balanceOf(address account) external view returns (uint256) { + return 0; + } - function transfer(address recipient, uint256 amount) external returns (bool) { - return false; - } + function transfer(address recipient, uint256 amount) external returns (bool) { + return false; + } - function allowance(address owner, address spender) external view returns (uint256) { - return 0; - } + function allowance(address owner, address spender) external view returns (uint256) { + return 0; + } - function approve(address spender, uint256 amount) external returns (bool) { - return false; - } + function approve(address spender, uint256 amount) external returns (bool) { + return false; + } - function transferFrom( - address sender, - address recipient, - uint256 amount - ) external returns (bool) { - return false; - } + function transferFrom( + address sender, + address recipient, + uint256 amount + ) external returns (bool) { + return false; + } } diff --git a/contracts/farm/LiquidityMiningStorage.sol b/contracts/farm/LiquidityMiningStorage.sol index 79808aba1..d727b23f6 100644 --- a/contracts/farm/LiquidityMiningStorage.sol +++ b/contracts/farm/LiquidityMiningStorage.sol @@ -7,63 +7,63 @@ import "../locked/ILockedSOV.sol"; import "../utils/AdminRole.sol"; contract LiquidityMiningStorage is AdminRole { - // Info of each user. - struct UserInfo { - uint256 amount; // How many pool tokens the user has provided. - uint256 rewardDebt; // Reward debt. See explanation below. - uint256 accumulatedReward; //Reward that's ready to be transferred - // - // We do some fancy math here. Basically, any point in time, the amount of reward tokens - // entitled to a user but is accumulated to be distributed is: - // - // accumulated reward = (user.amount * pool.accumulatedRewardPerShare) - user.rewardDebt - // - // Whenever a user deposits or withdraws LP tokens to a pool. Here's what happens: - // 1. The pool's `accumulatedRewardPerShare` (and `lastRewardBlock`) gets updated. - // 2. User receives the accumulated reward sent to his/her address. - // 3. User's `amount` gets updated. - // 4. User's `rewardDebt` gets updated. - } + // Info of each user. + struct UserInfo { + uint256 amount; // How many pool tokens the user has provided. + uint256 rewardDebt; // Reward debt. See explanation below. + uint256 accumulatedReward; //Reward that's ready to be transferred + // + // We do some fancy math here. Basically, any point in time, the amount of reward tokens + // entitled to a user but is accumulated to be distributed is: + // + // accumulated reward = (user.amount * pool.accumulatedRewardPerShare) - user.rewardDebt + // + // Whenever a user deposits or withdraws LP tokens to a pool. Here's what happens: + // 1. The pool's `accumulatedRewardPerShare` (and `lastRewardBlock`) gets updated. + // 2. User receives the accumulated reward sent to his/her address. + // 3. User's `amount` gets updated. + // 4. User's `rewardDebt` gets updated. + } - // Info of each pool. - struct PoolInfo { - IERC20 poolToken; // Address of LP token contract. - uint96 allocationPoint; // How many allocation points assigned to this pool. Amount of reward tokens to distribute per block. - uint256 lastRewardBlock; // Last block number that reward tokens distribution occurs. - uint256 accumulatedRewardPerShare; // Accumulated amount of reward tokens per share, times 1e12. See below. - } + // Info of each pool. + struct PoolInfo { + IERC20 poolToken; // Address of LP token contract. + uint96 allocationPoint; // How many allocation points assigned to this pool. Amount of reward tokens to distribute per block. + uint256 lastRewardBlock; // Last block number that reward tokens distribution occurs. + uint256 accumulatedRewardPerShare; // Accumulated amount of reward tokens per share, times 1e12. See below. + } - // SVR tokens created per block. - uint256 public rewardTokensPerBlock; - // The block number when reward token mining starts. - uint256 public startBlock; - // Block number when bonus reward token period ends. - uint256 public bonusEndBlock; - // Block number when reward token period ends. - uint256 public endBlock; + // SVR tokens created per block. + uint256 public rewardTokensPerBlock; + // The block number when reward token mining starts. + uint256 public startBlock; + // Block number when bonus reward token period ends. + uint256 public bonusEndBlock; + // Block number when reward token period ends. + uint256 public endBlock; - //Wrapper contract which will be a proxy between user and LM - address public wrapper; + //Wrapper contract which will be a proxy between user and LM + address public wrapper; - // Info of each pool. - PoolInfo[] public poolInfoList; - // Mapping pool token address => pool id - mapping(address => uint256) poolIdList; - // Total allocation points. Must be the sum of all allocation points in all pools. - uint256 public totalAllocationPoint; + // Info of each pool. + PoolInfo[] public poolInfoList; + // Mapping pool token address => pool id + mapping(address => uint256) poolIdList; + // Total allocation points. Must be the sum of all allocation points in all pools. + uint256 public totalAllocationPoint; - // Info of each user that stakes LP tokens. - mapping(uint256 => mapping(address => UserInfo)) public userInfoMap; - // Total balance this contract should have to handle withdrawal for all users - uint256 public totalUsersBalance; + // Info of each user that stakes LP tokens. + mapping(uint256 => mapping(address => UserInfo)) public userInfoMap; + // Total balance this contract should have to handle withdrawal for all users + uint256 public totalUsersBalance; - /// @dev The SOV token - IERC20 public SOV; + /// @dev The SOV token + IERC20 public SOV; - /// @dev The locked vault contract to deposit LP's rewards into. - ILockedSOV public lockedSOV; + /// @dev The locked vault contract to deposit LP's rewards into. + ILockedSOV public lockedSOV; - // The % which determines how much will be unlocked immediately. - /// @dev 10000 is 100% - uint256 public unlockedImmediatelyPercent; + // The % which determines how much will be unlocked immediately. + /// @dev 10000 is 100% + uint256 public unlockedImmediatelyPercent; } diff --git a/contracts/feeds/BProPriceFeed.sol b/contracts/feeds/BProPriceFeed.sol index 4114b6971..bd269b982 100644 --- a/contracts/feeds/BProPriceFeed.sol +++ b/contracts/feeds/BProPriceFeed.sol @@ -12,47 +12,47 @@ import "../openzeppelin/Address.sol"; * contract and queries its method bproUsdPrice to get bPro/USD valuation. * */ contract BProPriceFeed is IPriceFeedsExt, Ownable { - address public mocStateAddress; + address public mocStateAddress; - event SetMoCStateAddress(address indexed mocStateAddress, address changerAddress); + event SetMoCStateAddress(address indexed mocStateAddress, address changerAddress); - /** - * @notice Initializes a new MoC state. - * - * @param _mocStateAddress MoC state address - * */ - constructor(address _mocStateAddress) public { - setMoCStateAddress(_mocStateAddress); - } + /** + * @notice Initializes a new MoC state. + * + * @param _mocStateAddress MoC state address + * */ + constructor(address _mocStateAddress) public { + setMoCStateAddress(_mocStateAddress); + } - /** - * @notice Get BPro USD price. - * - * @return the BPro USD Price [using mocPrecision] - */ - function latestAnswer() external view returns (uint256) { - IMoCState _mocState = IMoCState(mocStateAddress); - return _mocState.bproUsdPrice(); - } + /** + * @notice Get BPro USD price. + * + * @return the BPro USD Price [using mocPrecision] + */ + function latestAnswer() external view returns (uint256) { + IMoCState _mocState = IMoCState(mocStateAddress); + return _mocState.bproUsdPrice(); + } - /** - * @notice Supposed to get the MoC update time, but instead - * get the current timestamp. - * - * @return Always returns current block's timestamp. - * */ - function latestTimestamp() external view returns (uint256) { - return now; /// MoC state doesn't return update timestamp. - } + /** + * @notice Supposed to get the MoC update time, but instead + * get the current timestamp. + * + * @return Always returns current block's timestamp. + * */ + function latestTimestamp() external view returns (uint256) { + return now; /// MoC state doesn't return update timestamp. + } - /** - * @notice Set MoC state address. - * - * @param _mocStateAddress The MoC state address. - * */ - function setMoCStateAddress(address _mocStateAddress) public onlyOwner { - require(Address.isContract(_mocStateAddress), "_mocStateAddress not a contract"); - mocStateAddress = _mocStateAddress; - emit SetMoCStateAddress(mocStateAddress, msg.sender); - } + /** + * @notice Set MoC state address. + * + * @param _mocStateAddress The MoC state address. + * */ + function setMoCStateAddress(address _mocStateAddress) public onlyOwner { + require(Address.isContract(_mocStateAddress), "_mocStateAddress not a contract"); + mocStateAddress = _mocStateAddress; + emit SetMoCStateAddress(mocStateAddress, msg.sender); + } } diff --git a/contracts/feeds/IMoCState.sol b/contracts/feeds/IMoCState.sol index a97588dfc..66ea46917 100644 --- a/contracts/feeds/IMoCState.sol +++ b/contracts/feeds/IMoCState.sol @@ -1,25 +1,25 @@ pragma solidity >=0.5.0 <0.6.0; interface IMoCState { - function getRbtcInBitPro(bytes32 bucket) external view returns (uint256); + function getRbtcInBitPro(bytes32 bucket) external view returns (uint256); - function globalMaxBPro() external view returns (uint256); + function globalMaxBPro() external view returns (uint256); - function maxBPro(bytes32 bucket) external view returns (uint256); + function maxBPro(bytes32 bucket) external view returns (uint256); - function absoluteMaxBPro() external view returns (uint256); + function absoluteMaxBPro() external view returns (uint256); - function maxBProWithDiscount() external view returns (uint256); + function maxBProWithDiscount() external view returns (uint256); - function bproTecPrice() external view returns (uint256); + function bproTecPrice() external view returns (uint256); - function bucketBProTecPrice(bytes32 bucket) external view returns (uint256); + function bucketBProTecPrice(bytes32 bucket) external view returns (uint256); - function bproDiscountPrice() external view returns (uint256); + function bproDiscountPrice() external view returns (uint256); - function bproUsdPrice() external view returns (uint256); + function bproUsdPrice() external view returns (uint256); - function bproSpotDiscountRate() external view returns (uint256); + function bproSpotDiscountRate() external view returns (uint256); - function getBucketNBPro(bytes32 bucket) external view returns (uint256); + function getBucketNBPro(bytes32 bucket) external view returns (uint256); } diff --git a/contracts/feeds/IPriceFeeds.sol b/contracts/feeds/IPriceFeeds.sol index d23b38911..d88969b48 100644 --- a/contracts/feeds/IPriceFeeds.sol +++ b/contracts/feeds/IPriceFeeds.sol @@ -6,55 +6,61 @@ pragma solidity 0.5.17; interface IPriceFeeds { - function queryRate(address sourceToken, address destToken) external view returns (uint256 rate, uint256 precision); - - function queryPrecision(address sourceToken, address destToken) external view returns (uint256 precision); - - function queryReturn( - address sourceToken, - address destToken, - uint256 sourceAmount - ) external view returns (uint256 destAmount); - - function checkPriceDisagreement( - address sourceToken, - address destToken, - uint256 sourceAmount, - uint256 destAmount, - uint256 maxSlippage - ) external view returns (uint256 sourceToDestSwapRate); - - function amountInEth(address Token, uint256 amount) external view returns (uint256 ethAmount); - - function getMaxDrawdown( - address loanToken, - address collateralToken, - uint256 loanAmount, - uint256 collateralAmount, - uint256 maintenanceMargin - ) external view returns (uint256); - - function getCurrentMarginAndCollateralSize( - address loanToken, - address collateralToken, - uint256 loanAmount, - uint256 collateralAmount - ) external view returns (uint256 currentMargin, uint256 collateralInEthAmount); - - function getCurrentMargin( - address loanToken, - address collateralToken, - uint256 loanAmount, - uint256 collateralAmount - ) external view returns (uint256 currentMargin, uint256 collateralToLoanRate); - - function shouldLiquidate( - address loanToken, - address collateralToken, - uint256 loanAmount, - uint256 collateralAmount, - uint256 maintenanceMargin - ) external view returns (bool); - - function getFastGasPrice(address payToken) external view returns (uint256); + function queryRate(address sourceToken, address destToken) + external + view + returns (uint256 rate, uint256 precision); + + function queryPrecision(address sourceToken, address destToken) + external + view + returns (uint256 precision); + + function queryReturn( + address sourceToken, + address destToken, + uint256 sourceAmount + ) external view returns (uint256 destAmount); + + function checkPriceDisagreement( + address sourceToken, + address destToken, + uint256 sourceAmount, + uint256 destAmount, + uint256 maxSlippage + ) external view returns (uint256 sourceToDestSwapRate); + + function amountInEth(address Token, uint256 amount) external view returns (uint256 ethAmount); + + function getMaxDrawdown( + address loanToken, + address collateralToken, + uint256 loanAmount, + uint256 collateralAmount, + uint256 maintenanceMargin + ) external view returns (uint256); + + function getCurrentMarginAndCollateralSize( + address loanToken, + address collateralToken, + uint256 loanAmount, + uint256 collateralAmount + ) external view returns (uint256 currentMargin, uint256 collateralInEthAmount); + + function getCurrentMargin( + address loanToken, + address collateralToken, + uint256 loanAmount, + uint256 collateralAmount + ) external view returns (uint256 currentMargin, uint256 collateralToLoanRate); + + function shouldLiquidate( + address loanToken, + address collateralToken, + uint256 loanAmount, + uint256 collateralAmount, + uint256 maintenanceMargin + ) external view returns (bool); + + function getFastGasPrice(address payToken) external view returns (uint256); } diff --git a/contracts/feeds/IRSKOracle.sol b/contracts/feeds/IRSKOracle.sol index 47ac415bd..f51ab36f0 100644 --- a/contracts/feeds/IRSKOracle.sol +++ b/contracts/feeds/IRSKOracle.sol @@ -1,11 +1,11 @@ pragma solidity >=0.5.0 <0.6.0; interface IRSKOracle { - function updatePrice(uint256 price, uint256 timestamp) external; + function updatePrice(uint256 price, uint256 timestamp) external; - function getPricing() external view returns (uint256, uint256); + function getPricing() external view returns (uint256, uint256); - function setOracleAddress(address addr) external; + function setOracleAddress(address addr) external; - function clearOracleAddress() external; + function clearOracleAddress() external; } diff --git a/contracts/feeds/IV1PoolOracle.sol b/contracts/feeds/IV1PoolOracle.sol index fbace3a80..b9cd555d6 100644 --- a/contracts/feeds/IV1PoolOracle.sol +++ b/contracts/feeds/IV1PoolOracle.sol @@ -1,25 +1,25 @@ pragma solidity >=0.5.0 <0.6.0; interface IV1PoolOracle { - function read(uint256 price, uint256 timestamp) - external - view - returns ( - uint256, - uint256, - uint256, - uint256, - uint256, - uint256 - ); + function read(uint256 price, uint256 timestamp) + external + view + returns ( + uint256, + uint256, + uint256, + uint256, + uint256, + uint256 + ); - function latestAnswer() external view returns (uint256); + function latestAnswer() external view returns (uint256); - function liquidityPool() external view returns (address); + function liquidityPool() external view returns (address); - function latestPrice(address _baseToken) external view returns (uint256 answer); + function latestPrice(address _baseToken) external view returns (uint256 answer); } interface ILiquidityPoolV1Converter { - function reserveTokens(uint256 index) external view returns (address); + function reserveTokens(uint256 index) external view returns (address); } diff --git a/contracts/feeds/PriceFeedRSKOracle.sol b/contracts/feeds/PriceFeedRSKOracle.sol index 33e2629e0..2231ad13c 100644 --- a/contracts/feeds/PriceFeedRSKOracle.sol +++ b/contracts/feeds/PriceFeedRSKOracle.sol @@ -12,51 +12,51 @@ import "../openzeppelin/Address.sol"; * getting the price and the last timestamp from an external oracle contract. * */ contract PriceFeedRSKOracle is IPriceFeedsExt, Ownable { - /* Storage */ - - address public rskOracleAddress; - - /* Events */ - - event SetRSKOracleAddress(address indexed rskOracleAddress, address changerAddress); - - /* Functions */ - - /** - * @notice Initialize a new RSK Oracle. - * - * @param _rskOracleAddress The RSK Oracle address. - * */ - constructor(address _rskOracleAddress) public { - setRSKOracleAddress(_rskOracleAddress); - } - - /** - * @notice Get the oracle price. - * @return The price from Oracle. - * */ - function latestAnswer() external view returns (uint256 _price) { - IRSKOracle _rskOracle = IRSKOracle(rskOracleAddress); - (_price, ) = _rskOracle.getPricing(); - } - - /** - * @notice Get the las time oracle updated the price. - * @return The latest time. - */ - function latestTimestamp() external view returns (uint256 _timestamp) { - IRSKOracle _rskOracle = IRSKOracle(rskOracleAddress); - (, _timestamp) = _rskOracle.getPricing(); - } - - /** - * @notice Set the RSK Oracle address. - * - * @param _rskOracleAddress The RSK Oracle address. - */ - function setRSKOracleAddress(address _rskOracleAddress) public onlyOwner { - require(Address.isContract(_rskOracleAddress), "_rskOracleAddress not a contract"); - rskOracleAddress = _rskOracleAddress; - emit SetRSKOracleAddress(rskOracleAddress, msg.sender); - } + /* Storage */ + + address public rskOracleAddress; + + /* Events */ + + event SetRSKOracleAddress(address indexed rskOracleAddress, address changerAddress); + + /* Functions */ + + /** + * @notice Initialize a new RSK Oracle. + * + * @param _rskOracleAddress The RSK Oracle address. + * */ + constructor(address _rskOracleAddress) public { + setRSKOracleAddress(_rskOracleAddress); + } + + /** + * @notice Get the oracle price. + * @return The price from Oracle. + * */ + function latestAnswer() external view returns (uint256 _price) { + IRSKOracle _rskOracle = IRSKOracle(rskOracleAddress); + (_price, ) = _rskOracle.getPricing(); + } + + /** + * @notice Get the las time oracle updated the price. + * @return The latest time. + */ + function latestTimestamp() external view returns (uint256 _timestamp) { + IRSKOracle _rskOracle = IRSKOracle(rskOracleAddress); + (, _timestamp) = _rskOracle.getPricing(); + } + + /** + * @notice Set the RSK Oracle address. + * + * @param _rskOracleAddress The RSK Oracle address. + */ + function setRSKOracleAddress(address _rskOracleAddress) public onlyOwner { + require(Address.isContract(_rskOracleAddress), "_rskOracleAddress not a contract"); + rskOracleAddress = _rskOracleAddress; + emit SetRSKOracleAddress(rskOracleAddress, msg.sender); + } } diff --git a/contracts/feeds/PriceFeedV1PoolOracle.sol b/contracts/feeds/PriceFeedV1PoolOracle.sol index 4681d255a..56b8ce1bd 100644 --- a/contracts/feeds/PriceFeedV1PoolOracle.sol +++ b/contracts/feeds/PriceFeedV1PoolOracle.sol @@ -14,115 +14,116 @@ import "./IPriceFeeds.sol"; * getting the price from v1 pool oracle. * */ contract PriceFeedV1PoolOracle is IPriceFeedsExt, Ownable { - using SafeMath for uint256; - /* Storage */ - - address public v1PoolOracleAddress; - address public wRBTCAddress; - address public docAddress; - address public baseCurrency; - - /* Events */ - event SetV1PoolOracleAddress(address indexed v1PoolOracleAddress, address changerAddress); - event SetWRBTCAddress(address indexed wRBTCAddress, address changerAddress); - event SetDOCAddress(address indexed docAddress, address changerAddress); - event SetBaseCurrency(address indexed baseCurrency, address changerAddress); - - /* Functions */ - - /** - * @notice Initialize a new V1 Pool Oracle. - * - * @param _v1PoolOracleAddress The V1 Pool Oracle address. - * @param _wRBTCAddress The wrbtc token address. - * @param _docAddress The doc token address. - * */ - constructor( - address _v1PoolOracleAddress, - address _wRBTCAddress, - address _docAddress, - address _baseCurrency - ) public { - setRBTCAddress(_wRBTCAddress); - setDOCAddress(_docAddress); - setV1PoolOracleAddress(_v1PoolOracleAddress); - setBaseCurrency(_baseCurrency); - } - - /** - * @notice Get the oracle price. - * @return The price from Oracle. - * */ - function latestAnswer() external view returns (uint256) { - IV1PoolOracle _v1PoolOracle = IV1PoolOracle(v1PoolOracleAddress); - - uint256 _price = _v1PoolOracle.latestPrice(baseCurrency); - - // Need to convert to USD, since the V1 pool return value is based on BTC - uint256 priceInUSD = _convertAnswerToUsd(_price); - require(priceInUSD != 0, "price error"); - - return priceInUSD; - } - - function _convertAnswerToUsd(uint256 _valueInBTC) private view returns (uint256) { - address _priceFeeds = msg.sender; - - uint256 precision = IPriceFeeds(_priceFeeds).queryPrecision(wRBTCAddress, docAddress); - uint256 valueInUSD = IPriceFeeds(_priceFeeds).queryReturn(wRBTCAddress, docAddress, _valueInBTC); - - /// Need to multiply by query precision (doc's precision) and divide by 1*10^18 (Because the based price in v1 pool is using 18 decimals) - return valueInUSD.mul(precision).div(1e18); - } - - /** - * @notice Set the V1 Pool Oracle address. - * - * @param _v1PoolOracleAddress The V1 Pool Oracle address. - */ - function setV1PoolOracleAddress(address _v1PoolOracleAddress) public onlyOwner { - require(Address.isContract(_v1PoolOracleAddress), "_v1PoolOracleAddress not a contract"); - IV1PoolOracle _v1PoolOracle = IV1PoolOracle(_v1PoolOracleAddress); - address liquidityPool = _v1PoolOracle.liquidityPool(); - require( - ILiquidityPoolV1Converter(liquidityPool).reserveTokens(0) == wRBTCAddress || - ILiquidityPoolV1Converter(liquidityPool).reserveTokens(1) == wRBTCAddress, - "one of the two reserves needs to be wrbtc" - ); - v1PoolOracleAddress = _v1PoolOracleAddress; - emit SetV1PoolOracleAddress(v1PoolOracleAddress, msg.sender); - } - - /** - * @notice Set the rBtc address. V1 pool based price is BTC, so need to convert the value from v1 pool to USD. That's why we need to get the price of the rBtc - * - * @param _wRBTCAddress The rBTC address - */ - function setRBTCAddress(address _wRBTCAddress) public onlyOwner { - require(_wRBTCAddress != address(0), "wRBTC address cannot be zero address"); - wRBTCAddress = _wRBTCAddress; - emit SetWRBTCAddress(wRBTCAddress, msg.sender); - } - - /** - * @notice Set the DoC address. V1 pool based price is BTC, so need to convert the value from v1 pool to USD. That's why we need to get the price of the DoC - * - * @param _docAddress The DoC address - */ - function setDOCAddress(address _docAddress) public onlyOwner { - require(_docAddress != address(0), "DOC address cannot be zero address"); - docAddress = _docAddress; - emit SetDOCAddress(_docAddress, msg.sender); - } - - /** - * @notice Set the base currency address. That's the reserve address which is not WRBTC - * - * @param _baseCurrency The base currency address - */ - function setBaseCurrency(address _baseCurrency) public onlyOwner { - require(_baseCurrency != address(0), "Base currency address cannot be zero address"); - baseCurrency = _baseCurrency; - emit SetBaseCurrency(_baseCurrency, msg.sender); - } + using SafeMath for uint256; + /* Storage */ + + address public v1PoolOracleAddress; + address public wRBTCAddress; + address public docAddress; + address public baseCurrency; + + /* Events */ + event SetV1PoolOracleAddress(address indexed v1PoolOracleAddress, address changerAddress); + event SetWRBTCAddress(address indexed wRBTCAddress, address changerAddress); + event SetDOCAddress(address indexed docAddress, address changerAddress); + event SetBaseCurrency(address indexed baseCurrency, address changerAddress); + + /* Functions */ + + /** + * @notice Initialize a new V1 Pool Oracle. + * + * @param _v1PoolOracleAddress The V1 Pool Oracle address. + * @param _wRBTCAddress The wrbtc token address. + * @param _docAddress The doc token address. + * */ + constructor( + address _v1PoolOracleAddress, + address _wRBTCAddress, + address _docAddress, + address _baseCurrency + ) public { + setRBTCAddress(_wRBTCAddress); + setDOCAddress(_docAddress); + setV1PoolOracleAddress(_v1PoolOracleAddress); + setBaseCurrency(_baseCurrency); + } + + /** + * @notice Get the oracle price. + * @return The price from Oracle. + * */ + function latestAnswer() external view returns (uint256) { + IV1PoolOracle _v1PoolOracle = IV1PoolOracle(v1PoolOracleAddress); + + uint256 _price = _v1PoolOracle.latestPrice(baseCurrency); + + // Need to convert to USD, since the V1 pool return value is based on BTC + uint256 priceInUSD = _convertAnswerToUsd(_price); + require(priceInUSD != 0, "price error"); + + return priceInUSD; + } + + function _convertAnswerToUsd(uint256 _valueInBTC) private view returns (uint256) { + address _priceFeeds = msg.sender; + + uint256 precision = IPriceFeeds(_priceFeeds).queryPrecision(wRBTCAddress, docAddress); + uint256 valueInUSD = + IPriceFeeds(_priceFeeds).queryReturn(wRBTCAddress, docAddress, _valueInBTC); + + /// Need to multiply by query precision (doc's precision) and divide by 1*10^18 (Because the based price in v1 pool is using 18 decimals) + return valueInUSD.mul(precision).div(1e18); + } + + /** + * @notice Set the V1 Pool Oracle address. + * + * @param _v1PoolOracleAddress The V1 Pool Oracle address. + */ + function setV1PoolOracleAddress(address _v1PoolOracleAddress) public onlyOwner { + require(Address.isContract(_v1PoolOracleAddress), "_v1PoolOracleAddress not a contract"); + IV1PoolOracle _v1PoolOracle = IV1PoolOracle(_v1PoolOracleAddress); + address liquidityPool = _v1PoolOracle.liquidityPool(); + require( + ILiquidityPoolV1Converter(liquidityPool).reserveTokens(0) == wRBTCAddress || + ILiquidityPoolV1Converter(liquidityPool).reserveTokens(1) == wRBTCAddress, + "one of the two reserves needs to be wrbtc" + ); + v1PoolOracleAddress = _v1PoolOracleAddress; + emit SetV1PoolOracleAddress(v1PoolOracleAddress, msg.sender); + } + + /** + * @notice Set the rBtc address. V1 pool based price is BTC, so need to convert the value from v1 pool to USD. That's why we need to get the price of the rBtc + * + * @param _wRBTCAddress The rBTC address + */ + function setRBTCAddress(address _wRBTCAddress) public onlyOwner { + require(_wRBTCAddress != address(0), "wRBTC address cannot be zero address"); + wRBTCAddress = _wRBTCAddress; + emit SetWRBTCAddress(wRBTCAddress, msg.sender); + } + + /** + * @notice Set the DoC address. V1 pool based price is BTC, so need to convert the value from v1 pool to USD. That's why we need to get the price of the DoC + * + * @param _docAddress The DoC address + */ + function setDOCAddress(address _docAddress) public onlyOwner { + require(_docAddress != address(0), "DOC address cannot be zero address"); + docAddress = _docAddress; + emit SetDOCAddress(_docAddress, msg.sender); + } + + /** + * @notice Set the base currency address. That's the reserve address which is not WRBTC + * + * @param _baseCurrency The base currency address + */ + function setBaseCurrency(address _baseCurrency) public onlyOwner { + require(_baseCurrency != address(0), "Base currency address cannot be zero address"); + baseCurrency = _baseCurrency; + emit SetBaseCurrency(_baseCurrency, msg.sender); + } } diff --git a/contracts/feeds/PriceFeeds.sol b/contracts/feeds/PriceFeeds.sol index 7642c1eaa..26f8c1165 100644 --- a/contracts/feeds/PriceFeeds.sol +++ b/contracts/feeds/PriceFeeds.sol @@ -11,7 +11,7 @@ import "../interfaces/IERC20.sol"; import "./PriceFeedsConstants.sol"; interface IPriceFeedsExt { - function latestAnswer() external view returns (uint256); + function latestAnswer() external view returns (uint256); } /** @@ -26,415 +26,449 @@ interface IPriceFeedsExt { * drawdown, margin and collateral. * */ contract PriceFeeds is Constants, Ownable { - using SafeMath for uint256; - - /* Events */ - - event GlobalPricingPaused(address indexed sender, bool indexed isPaused); - - /* Storage */ - - /// Mapping of PriceFeedsExt instances. - /// token => pricefeed - mapping(address => IPriceFeedsExt) public pricesFeeds; - - /// Decimals of supported tokens. - mapping(address => uint256) public decimals; - - /// Value on rBTC weis for the protocol token. - uint256 public protocolTokenEthPrice = 0.0002 ether; - - /// Flag to pause pricings. - bool public globalPricingPaused = false; - - /* Functions */ - - /** - * @notice Contract deployment requires 3 parameters. - * - * @param _wrbtcTokenAddress The address of the wrapped wrBTC token. - * @param _protocolTokenAddress The address of the protocol token. - * @param _baseTokenAddress The address of the base token. - * */ - constructor( - address _wrbtcTokenAddress, - address _protocolTokenAddress, - address _baseTokenAddress - ) public { - /// Set decimals for this token. - decimals[address(0)] = 18; - decimals[_wrbtcTokenAddress] = 18; - _setWrbtcToken(_wrbtcTokenAddress); - _setProtocolTokenAddress(_protocolTokenAddress); - _setBaseToken(_baseTokenAddress); - } - - /** - * @notice Calculate the price ratio between two tokens. - * - * @dev Public wrapper for _queryRate internal function. - * - * @param sourceToken The address of the source tokens. - * @param destToken The address of the destiny tokens. - * - * @return rate The price ratio source/dest. - * @return precision The ratio precision. - * */ - function queryRate(address sourceToken, address destToken) public view returns (uint256 rate, uint256 precision) { - return _queryRate(sourceToken, destToken); - } - - /** - * @notice Calculate the relative precision between two tokens. - * - * @dev Public wrapper for _getDecimalPrecision internal function. - * - * @param sourceToken The address of the source tokens. - * @param destToken The address of the destiny tokens. - * - * @return The precision ratio source/dest. - * */ - function queryPrecision(address sourceToken, address destToken) public view returns (uint256) { - return sourceToken != destToken ? _getDecimalPrecision(sourceToken, destToken) : 10**18; - } - - /** - * @notice Price conversor: Calculate the price of an amount of source - * tokens in destiny token units. - * - * @dev NOTE: This function returns 0 during a pause, rather than a revert. - * Ensure calling contracts handle correctly. - * - * @param sourceToken The address of the source tokens. - * @param destToken The address of the destiny tokens. - * @param sourceAmount The amount of the source tokens. - * - * @return destAmount The amount of destiny tokens equivalent in price - * to the amount of source tokens. - * */ - function queryReturn( - address sourceToken, - address destToken, - uint256 sourceAmount - ) public view returns (uint256 destAmount) { - if (globalPricingPaused) { - return 0; - } - - (uint256 rate, uint256 precision) = _queryRate(sourceToken, destToken); - - destAmount = sourceAmount.mul(rate).div(precision); - } - - /** - * @notice Calculate the swap rate between two tokens. - * - * Regarding slippage, there is a hardcoded slippage limit of 5%, enforced - * by this function for all borrowing, lending and margin trading - * originated swaps performed in the Sovryn exchange. - * - * This means all operations in the Sovryn exchange are subject to losing - * up to 5% from the internal swap performed. - * - * @param sourceToken The address of the source tokens. - * @param destToken The address of the destiny tokens. - * @param sourceAmount The amount of source tokens. - * @param destAmount The amount of destiny tokens. - * @param maxSlippage The maximum slippage limit. - * - * @return sourceToDestSwapRate The swap rate between tokens. - * */ - function checkPriceDisagreement( - address sourceToken, - address destToken, - uint256 sourceAmount, - uint256 destAmount, - uint256 maxSlippage - ) public view returns (uint256 sourceToDestSwapRate) { - require(!globalPricingPaused, "pricing is paused"); - (uint256 rate, uint256 precision) = _queryRate(sourceToken, destToken); - - sourceToDestSwapRate = destAmount.mul(precision).div(sourceAmount); - - if (rate > sourceToDestSwapRate) { - uint256 spreadValue = rate - sourceToDestSwapRate; - spreadValue = spreadValue.mul(10**20).div(sourceToDestSwapRate); - require(spreadValue <= maxSlippage, "price disagreement"); - } - } - - /** - * @notice Calculate the rBTC amount equivalent to a given token amount. - * Native coin on RSK is rBTC. This code comes from Ethereum applications, - * so Eth refers to 10**18 weis of native coin, i.e.: 1 rBTC. - * - * @param tokenAddress The address of the token to calculate price. - * @param amount The amount of tokens to calculate price. - * - * @return ethAmount The amount of rBTC equivalent. - * */ - function amountInEth(address tokenAddress, uint256 amount) public view returns (uint256 ethAmount) { - /// Token is wrBTC, amount in rBTC is the same. - if (tokenAddress == address(wrbtcToken)) { - ethAmount = amount; - } else { - (uint256 toEthRate, uint256 toEthPrecision) = queryRate(tokenAddress, address(wrbtcToken)); - ethAmount = amount.mul(toEthRate).div(toEthPrecision); - } - } - - /** - * @notice Calculate the maximum drawdown of a loan. - * - * A drawdown is commonly defined as the decline from a high peak to a - * pullback low of a specific investment or equity in an account. - * - * Drawdown magnitude refers to the amount of value that a user loses - * during the drawdown period. - * - * @param loanToken The address of the loan token. - * @param collateralToken The address of the collateral token. - * @param loanAmount The amount of the loan. - * @param collateralAmount The amount of the collateral. - * @param margin The relation between the position size and the loan. - * margin = (total position size - loan) / loan - * - * @return maxDrawdown The maximum drawdown. - * */ - function getMaxDrawdown( - address loanToken, - address collateralToken, - uint256 loanAmount, - uint256 collateralAmount, - uint256 margin - ) public view returns (uint256 maxDrawdown) { - uint256 loanToCollateralAmount; - if (collateralToken == loanToken) { - loanToCollateralAmount = loanAmount; - } else { - (uint256 rate, uint256 precision) = queryRate(loanToken, collateralToken); - loanToCollateralAmount = loanAmount.mul(rate).div(precision); - } - - uint256 combined = loanToCollateralAmount.add(loanToCollateralAmount.mul(margin).div(10**20)); - - maxDrawdown = collateralAmount > combined ? collateralAmount - combined : 0; - } - - /** - * @notice Calculate the margin and the collateral on rBTC. - * - * @param loanToken The address of the loan token. - * @param collateralToken The address of the collateral token. - * @param loanAmount The amount of the loan. - * @param collateralAmount The amount of the collateral. - * - * @return currentMargin The margin of the loan. - * @return collateralInEthAmount The amount of collateral on rBTC. - * */ - function getCurrentMarginAndCollateralSize( - address loanToken, - address collateralToken, - uint256 loanAmount, - uint256 collateralAmount - ) public view returns (uint256 currentMargin, uint256 collateralInEthAmount) { - (currentMargin, ) = getCurrentMargin(loanToken, collateralToken, loanAmount, collateralAmount); - - collateralInEthAmount = amountInEth(collateralToken, collateralAmount); - } - - /** - * @notice Calculate the margin of a loan. - * - * @dev current margin = (total position size - loan) / loan - * The collateral amount passed as parameter equals the total position size. - * - * @param loanToken The address of the loan token. - * @param collateralToken The address of the collateral token. - * @param loanAmount The amount of the loan. - * @param collateralAmount The amount of the collateral. - * - * @return currentMargin The margin of the loan. - * @return collateralToLoanRate The price ratio between collateral and - * loan tokens. - * */ - function getCurrentMargin( - address loanToken, - address collateralToken, - uint256 loanAmount, - uint256 collateralAmount - ) public view returns (uint256 currentMargin, uint256 collateralToLoanRate) { - uint256 collateralToLoanAmount; - if (collateralToken == loanToken) { - collateralToLoanAmount = collateralAmount; - collateralToLoanRate = 10**18; - } else { - uint256 collateralToLoanPrecision; - (collateralToLoanRate, collateralToLoanPrecision) = queryRate(collateralToken, loanToken); - - collateralToLoanRate = collateralToLoanRate.mul(10**18).div(collateralToLoanPrecision); - - collateralToLoanAmount = collateralAmount.mul(collateralToLoanRate).div(10**18); - } - - if (loanAmount != 0 && collateralToLoanAmount >= loanAmount) { - return (collateralToLoanAmount.sub(loanAmount).mul(10**20).div(loanAmount), collateralToLoanRate); - } else { - return (0, collateralToLoanRate); - } - } - - /** - * @notice Get assessment about liquidating a loan. - * - * @param loanToken The address of the loan token. - * @param collateralToken The address of the collateral token. - * @param loanAmount The amount of the loan. - * @param collateralAmount The amount of the collateral. - * @param maintenanceMargin The minimum margin before liquidation. - * - * @return True/false to liquidate the loan. - * */ - function shouldLiquidate( - address loanToken, - address collateralToken, - uint256 loanAmount, - uint256 collateralAmount, - uint256 maintenanceMargin - ) public view returns (bool) { - (uint256 currentMargin, ) = getCurrentMargin(loanToken, collateralToken, loanAmount, collateralAmount); - - return currentMargin <= maintenanceMargin; - } - - /* - * Owner functions - */ - - /** - * @notice Set new value for protocolTokenEthPrice - * - * @param newPrice The new value for protocolTokenEthPrice - * */ - function setProtocolTokenEthPrice(uint256 newPrice) external onlyOwner { - require(newPrice != 0, "invalid price"); - protocolTokenEthPrice = newPrice; - } - - /** - * @notice Populate pricesFeeds mapping w/ values from feeds[] - * - * @param tokens The array of tokens to loop and get addresses. - * @param feeds The array of contract instances for every token. - * */ - function setPriceFeed(address[] calldata tokens, IPriceFeedsExt[] calldata feeds) external onlyOwner { - require(tokens.length == feeds.length, "count mismatch"); - - for (uint256 i = 0; i < tokens.length; i++) { - pricesFeeds[tokens[i]] = feeds[i]; - } - } - - /** - * @notice Populate decimals mapping w/ values from tokens[].decimals - * - * @param tokens The array of tokens to loop and get values from. - * */ - function setDecimals(IERC20[] calldata tokens) external onlyOwner { - for (uint256 i = 0; i < tokens.length; i++) { - decimals[address(tokens[i])] = tokens[i].decimals(); - } - } - - /** - * @notice Set flag globalPricingPaused - * - * @param isPaused The new status of pause (true/false). - * */ - function setGlobalPricingPaused(bool isPaused) external onlyOwner { - if (globalPricingPaused != isPaused) { - globalPricingPaused = isPaused; - - emit GlobalPricingPaused(msg.sender, isPaused); - } - } - - /* - * Internal functions - */ - - /** - * @notice Calculate the price ratio between two tokens. - * - * @param sourceToken The address of the source tokens. - * @param destToken The address of the destiny tokens. - * - * @return rate The price ratio source/dest. - * @return precision The ratio precision. - * */ - function _queryRate(address sourceToken, address destToken) internal view returns (uint256 rate, uint256 precision) { - require(!globalPricingPaused, "pricing is paused"); - - /// Different tokens, query prices and perform division. - if (sourceToken != destToken) { - uint256 sourceRate; - if (sourceToken != address(baseToken) && sourceToken != protocolTokenAddress) { - IPriceFeedsExt _sourceFeed = pricesFeeds[sourceToken]; - require(address(_sourceFeed) != address(0), "unsupported src feed"); - - /// Query token price on priceFeedsExt instance. - sourceRate = _sourceFeed.latestAnswer(); - require(sourceRate != 0 && (sourceRate >> 128) == 0, "price error"); - } else { - sourceRate = sourceToken == protocolTokenAddress ? protocolTokenEthPrice : 10**18; - } - - uint256 destRate; - if (destToken != address(baseToken) && destToken != protocolTokenAddress) { - IPriceFeedsExt _destFeed = pricesFeeds[destToken]; - require(address(_destFeed) != address(0), "unsupported dst feed"); - - /// Query token price on priceFeedsExt instance. - destRate = _destFeed.latestAnswer(); - require(destRate != 0 && (destRate >> 128) == 0, "price error"); - } else { - destRate = destToken == protocolTokenAddress ? protocolTokenEthPrice : 10**18; - } - - rate = sourceRate.mul(10**18).div(destRate); - - precision = _getDecimalPrecision(sourceToken, destToken); - - /// Same tokens, return 1 with decimals. - } else { - rate = 10**18; - precision = 10**18; - } - } - - /** - * @notice Calculate the relative precision between two tokens. - * - * @param sourceToken The address of the source tokens. - * @param destToken The address of the destiny tokens. - * - * @return The precision ratio source/dest. - * */ - function _getDecimalPrecision(address sourceToken, address destToken) internal view returns (uint256) { - /// Same tokens, return 1 with decimals. - if (sourceToken == destToken) { - return 10**18; - - /// Different tokens, query ERC20 precisions and return 18 +- diff. - } else { - uint256 sourceTokenDecimals = decimals[sourceToken]; - if (sourceTokenDecimals == 0) sourceTokenDecimals = IERC20(sourceToken).decimals(); - - uint256 destTokenDecimals = decimals[destToken]; - if (destTokenDecimals == 0) destTokenDecimals = IERC20(destToken).decimals(); - - if (destTokenDecimals >= sourceTokenDecimals) return 10**(SafeMath.sub(18, destTokenDecimals - sourceTokenDecimals)); - else return 10**(SafeMath.add(18, sourceTokenDecimals - destTokenDecimals)); - } - } + using SafeMath for uint256; + + /* Events */ + + event GlobalPricingPaused(address indexed sender, bool indexed isPaused); + + /* Storage */ + + /// Mapping of PriceFeedsExt instances. + /// token => pricefeed + mapping(address => IPriceFeedsExt) public pricesFeeds; + + /// Decimals of supported tokens. + mapping(address => uint256) public decimals; + + /// Value on rBTC weis for the protocol token. + uint256 public protocolTokenEthPrice = 0.0002 ether; + + /// Flag to pause pricings. + bool public globalPricingPaused = false; + + /* Functions */ + + /** + * @notice Contract deployment requires 3 parameters. + * + * @param _wrbtcTokenAddress The address of the wrapped wrBTC token. + * @param _protocolTokenAddress The address of the protocol token. + * @param _baseTokenAddress The address of the base token. + * */ + constructor( + address _wrbtcTokenAddress, + address _protocolTokenAddress, + address _baseTokenAddress + ) public { + /// Set decimals for this token. + decimals[address(0)] = 18; + decimals[_wrbtcTokenAddress] = 18; + _setWrbtcToken(_wrbtcTokenAddress); + _setProtocolTokenAddress(_protocolTokenAddress); + _setBaseToken(_baseTokenAddress); + } + + /** + * @notice Calculate the price ratio between two tokens. + * + * @dev Public wrapper for _queryRate internal function. + * + * @param sourceToken The address of the source tokens. + * @param destToken The address of the destiny tokens. + * + * @return rate The price ratio source/dest. + * @return precision The ratio precision. + * */ + function queryRate(address sourceToken, address destToken) + public + view + returns (uint256 rate, uint256 precision) + { + return _queryRate(sourceToken, destToken); + } + + /** + * @notice Calculate the relative precision between two tokens. + * + * @dev Public wrapper for _getDecimalPrecision internal function. + * + * @param sourceToken The address of the source tokens. + * @param destToken The address of the destiny tokens. + * + * @return The precision ratio source/dest. + * */ + function queryPrecision(address sourceToken, address destToken) public view returns (uint256) { + return sourceToken != destToken ? _getDecimalPrecision(sourceToken, destToken) : 10**18; + } + + /** + * @notice Price conversor: Calculate the price of an amount of source + * tokens in destiny token units. + * + * @dev NOTE: This function returns 0 during a pause, rather than a revert. + * Ensure calling contracts handle correctly. + * + * @param sourceToken The address of the source tokens. + * @param destToken The address of the destiny tokens. + * @param sourceAmount The amount of the source tokens. + * + * @return destAmount The amount of destiny tokens equivalent in price + * to the amount of source tokens. + * */ + function queryReturn( + address sourceToken, + address destToken, + uint256 sourceAmount + ) public view returns (uint256 destAmount) { + if (globalPricingPaused) { + return 0; + } + + (uint256 rate, uint256 precision) = _queryRate(sourceToken, destToken); + + destAmount = sourceAmount.mul(rate).div(precision); + } + + /** + * @notice Calculate the swap rate between two tokens. + * + * Regarding slippage, there is a hardcoded slippage limit of 5%, enforced + * by this function for all borrowing, lending and margin trading + * originated swaps performed in the Sovryn exchange. + * + * This means all operations in the Sovryn exchange are subject to losing + * up to 5% from the internal swap performed. + * + * @param sourceToken The address of the source tokens. + * @param destToken The address of the destiny tokens. + * @param sourceAmount The amount of source tokens. + * @param destAmount The amount of destiny tokens. + * @param maxSlippage The maximum slippage limit. + * + * @return sourceToDestSwapRate The swap rate between tokens. + * */ + function checkPriceDisagreement( + address sourceToken, + address destToken, + uint256 sourceAmount, + uint256 destAmount, + uint256 maxSlippage + ) public view returns (uint256 sourceToDestSwapRate) { + require(!globalPricingPaused, "pricing is paused"); + (uint256 rate, uint256 precision) = _queryRate(sourceToken, destToken); + + sourceToDestSwapRate = destAmount.mul(precision).div(sourceAmount); + + if (rate > sourceToDestSwapRate) { + uint256 spreadValue = rate - sourceToDestSwapRate; + spreadValue = spreadValue.mul(10**20).div(sourceToDestSwapRate); + require(spreadValue <= maxSlippage, "price disagreement"); + } + } + + /** + * @notice Calculate the rBTC amount equivalent to a given token amount. + * Native coin on RSK is rBTC. This code comes from Ethereum applications, + * so Eth refers to 10**18 weis of native coin, i.e.: 1 rBTC. + * + * @param tokenAddress The address of the token to calculate price. + * @param amount The amount of tokens to calculate price. + * + * @return ethAmount The amount of rBTC equivalent. + * */ + function amountInEth(address tokenAddress, uint256 amount) + public + view + returns (uint256 ethAmount) + { + /// Token is wrBTC, amount in rBTC is the same. + if (tokenAddress == address(wrbtcToken)) { + ethAmount = amount; + } else { + (uint256 toEthRate, uint256 toEthPrecision) = + queryRate(tokenAddress, address(wrbtcToken)); + ethAmount = amount.mul(toEthRate).div(toEthPrecision); + } + } + + /** + * @notice Calculate the maximum drawdown of a loan. + * + * A drawdown is commonly defined as the decline from a high peak to a + * pullback low of a specific investment or equity in an account. + * + * Drawdown magnitude refers to the amount of value that a user loses + * during the drawdown period. + * + * @param loanToken The address of the loan token. + * @param collateralToken The address of the collateral token. + * @param loanAmount The amount of the loan. + * @param collateralAmount The amount of the collateral. + * @param margin The relation between the position size and the loan. + * margin = (total position size - loan) / loan + * + * @return maxDrawdown The maximum drawdown. + * */ + function getMaxDrawdown( + address loanToken, + address collateralToken, + uint256 loanAmount, + uint256 collateralAmount, + uint256 margin + ) public view returns (uint256 maxDrawdown) { + uint256 loanToCollateralAmount; + if (collateralToken == loanToken) { + loanToCollateralAmount = loanAmount; + } else { + (uint256 rate, uint256 precision) = queryRate(loanToken, collateralToken); + loanToCollateralAmount = loanAmount.mul(rate).div(precision); + } + + uint256 combined = + loanToCollateralAmount.add(loanToCollateralAmount.mul(margin).div(10**20)); + + maxDrawdown = collateralAmount > combined ? collateralAmount - combined : 0; + } + + /** + * @notice Calculate the margin and the collateral on rBTC. + * + * @param loanToken The address of the loan token. + * @param collateralToken The address of the collateral token. + * @param loanAmount The amount of the loan. + * @param collateralAmount The amount of the collateral. + * + * @return currentMargin The margin of the loan. + * @return collateralInEthAmount The amount of collateral on rBTC. + * */ + function getCurrentMarginAndCollateralSize( + address loanToken, + address collateralToken, + uint256 loanAmount, + uint256 collateralAmount + ) public view returns (uint256 currentMargin, uint256 collateralInEthAmount) { + (currentMargin, ) = getCurrentMargin( + loanToken, + collateralToken, + loanAmount, + collateralAmount + ); + + collateralInEthAmount = amountInEth(collateralToken, collateralAmount); + } + + /** + * @notice Calculate the margin of a loan. + * + * @dev current margin = (total position size - loan) / loan + * The collateral amount passed as parameter equals the total position size. + * + * @param loanToken The address of the loan token. + * @param collateralToken The address of the collateral token. + * @param loanAmount The amount of the loan. + * @param collateralAmount The amount of the collateral. + * + * @return currentMargin The margin of the loan. + * @return collateralToLoanRate The price ratio between collateral and + * loan tokens. + * */ + function getCurrentMargin( + address loanToken, + address collateralToken, + uint256 loanAmount, + uint256 collateralAmount + ) public view returns (uint256 currentMargin, uint256 collateralToLoanRate) { + uint256 collateralToLoanAmount; + if (collateralToken == loanToken) { + collateralToLoanAmount = collateralAmount; + collateralToLoanRate = 10**18; + } else { + uint256 collateralToLoanPrecision; + (collateralToLoanRate, collateralToLoanPrecision) = queryRate( + collateralToken, + loanToken + ); + + collateralToLoanRate = collateralToLoanRate.mul(10**18).div(collateralToLoanPrecision); + + collateralToLoanAmount = collateralAmount.mul(collateralToLoanRate).div(10**18); + } + + if (loanAmount != 0 && collateralToLoanAmount >= loanAmount) { + return ( + collateralToLoanAmount.sub(loanAmount).mul(10**20).div(loanAmount), + collateralToLoanRate + ); + } else { + return (0, collateralToLoanRate); + } + } + + /** + * @notice Get assessment about liquidating a loan. + * + * @param loanToken The address of the loan token. + * @param collateralToken The address of the collateral token. + * @param loanAmount The amount of the loan. + * @param collateralAmount The amount of the collateral. + * @param maintenanceMargin The minimum margin before liquidation. + * + * @return True/false to liquidate the loan. + * */ + function shouldLiquidate( + address loanToken, + address collateralToken, + uint256 loanAmount, + uint256 collateralAmount, + uint256 maintenanceMargin + ) public view returns (bool) { + (uint256 currentMargin, ) = + getCurrentMargin(loanToken, collateralToken, loanAmount, collateralAmount); + + return currentMargin <= maintenanceMargin; + } + + /* + * Owner functions + */ + + /** + * @notice Set new value for protocolTokenEthPrice + * + * @param newPrice The new value for protocolTokenEthPrice + * */ + function setProtocolTokenEthPrice(uint256 newPrice) external onlyOwner { + require(newPrice != 0, "invalid price"); + protocolTokenEthPrice = newPrice; + } + + /** + * @notice Populate pricesFeeds mapping w/ values from feeds[] + * + * @param tokens The array of tokens to loop and get addresses. + * @param feeds The array of contract instances for every token. + * */ + function setPriceFeed(address[] calldata tokens, IPriceFeedsExt[] calldata feeds) + external + onlyOwner + { + require(tokens.length == feeds.length, "count mismatch"); + + for (uint256 i = 0; i < tokens.length; i++) { + pricesFeeds[tokens[i]] = feeds[i]; + } + } + + /** + * @notice Populate decimals mapping w/ values from tokens[].decimals + * + * @param tokens The array of tokens to loop and get values from. + * */ + function setDecimals(IERC20[] calldata tokens) external onlyOwner { + for (uint256 i = 0; i < tokens.length; i++) { + decimals[address(tokens[i])] = tokens[i].decimals(); + } + } + + /** + * @notice Set flag globalPricingPaused + * + * @param isPaused The new status of pause (true/false). + * */ + function setGlobalPricingPaused(bool isPaused) external onlyOwner { + if (globalPricingPaused != isPaused) { + globalPricingPaused = isPaused; + + emit GlobalPricingPaused(msg.sender, isPaused); + } + } + + /* + * Internal functions + */ + + /** + * @notice Calculate the price ratio between two tokens. + * + * @param sourceToken The address of the source tokens. + * @param destToken The address of the destiny tokens. + * + * @return rate The price ratio source/dest. + * @return precision The ratio precision. + * */ + function _queryRate(address sourceToken, address destToken) + internal + view + returns (uint256 rate, uint256 precision) + { + require(!globalPricingPaused, "pricing is paused"); + + /// Different tokens, query prices and perform division. + if (sourceToken != destToken) { + uint256 sourceRate; + if (sourceToken != address(baseToken) && sourceToken != protocolTokenAddress) { + IPriceFeedsExt _sourceFeed = pricesFeeds[sourceToken]; + require(address(_sourceFeed) != address(0), "unsupported src feed"); + + /// Query token price on priceFeedsExt instance. + sourceRate = _sourceFeed.latestAnswer(); + require(sourceRate != 0 && (sourceRate >> 128) == 0, "price error"); + } else { + sourceRate = sourceToken == protocolTokenAddress ? protocolTokenEthPrice : 10**18; + } + + uint256 destRate; + if (destToken != address(baseToken) && destToken != protocolTokenAddress) { + IPriceFeedsExt _destFeed = pricesFeeds[destToken]; + require(address(_destFeed) != address(0), "unsupported dst feed"); + + /// Query token price on priceFeedsExt instance. + destRate = _destFeed.latestAnswer(); + require(destRate != 0 && (destRate >> 128) == 0, "price error"); + } else { + destRate = destToken == protocolTokenAddress ? protocolTokenEthPrice : 10**18; + } + + rate = sourceRate.mul(10**18).div(destRate); + + precision = _getDecimalPrecision(sourceToken, destToken); + + /// Same tokens, return 1 with decimals. + } else { + rate = 10**18; + precision = 10**18; + } + } + + /** + * @notice Calculate the relative precision between two tokens. + * + * @param sourceToken The address of the source tokens. + * @param destToken The address of the destiny tokens. + * + * @return The precision ratio source/dest. + * */ + function _getDecimalPrecision(address sourceToken, address destToken) + internal + view + returns (uint256) + { + /// Same tokens, return 1 with decimals. + if (sourceToken == destToken) { + return 10**18; + + /// Different tokens, query ERC20 precisions and return 18 +- diff. + } else { + uint256 sourceTokenDecimals = decimals[sourceToken]; + if (sourceTokenDecimals == 0) sourceTokenDecimals = IERC20(sourceToken).decimals(); + + uint256 destTokenDecimals = decimals[destToken]; + if (destTokenDecimals == 0) destTokenDecimals = IERC20(destToken).decimals(); + + if (destTokenDecimals >= sourceTokenDecimals) + return 10**(SafeMath.sub(18, destTokenDecimals - sourceTokenDecimals)); + else return 10**(SafeMath.add(18, sourceTokenDecimals - destTokenDecimals)); + } + } } diff --git a/contracts/feeds/PriceFeedsConstants.sol b/contracts/feeds/PriceFeedsConstants.sol index b2c2a27cc..014f7546c 100644 --- a/contracts/feeds/PriceFeedsConstants.sol +++ b/contracts/feeds/PriceFeedsConstants.sol @@ -18,37 +18,37 @@ import "../openzeppelin/Address.sol"; * and protocol token. * */ contract Constants { - IWrbtcERC20 public wrbtcToken; - IWrbtcERC20 public baseToken; - address internal protocolTokenAddress; + IWrbtcERC20 public wrbtcToken; + IWrbtcERC20 public baseToken; + address internal protocolTokenAddress; - /** - * @notice Set wrBTC token address. - * - * @param _wrbtcTokenAddress The address of the wrapped wrBTC token. - * */ - function _setWrbtcToken(address _wrbtcTokenAddress) internal { - require(Address.isContract(_wrbtcTokenAddress), "_wrbtcTokenAddress not a contract"); - wrbtcToken = IWrbtcERC20(_wrbtcTokenAddress); - } + /** + * @notice Set wrBTC token address. + * + * @param _wrbtcTokenAddress The address of the wrapped wrBTC token. + * */ + function _setWrbtcToken(address _wrbtcTokenAddress) internal { + require(Address.isContract(_wrbtcTokenAddress), "_wrbtcTokenAddress not a contract"); + wrbtcToken = IWrbtcERC20(_wrbtcTokenAddress); + } - /** - * @notice Set protocol token address. - * - * @param _protocolTokenAddress The address of the protocol token. - * */ - function _setProtocolTokenAddress(address _protocolTokenAddress) internal { - require(Address.isContract(_protocolTokenAddress), "_protocolTokenAddress not a contract"); - protocolTokenAddress = _protocolTokenAddress; - } + /** + * @notice Set protocol token address. + * + * @param _protocolTokenAddress The address of the protocol token. + * */ + function _setProtocolTokenAddress(address _protocolTokenAddress) internal { + require(Address.isContract(_protocolTokenAddress), "_protocolTokenAddress not a contract"); + protocolTokenAddress = _protocolTokenAddress; + } - /** - * @notice Set base token address. - * - * @param _baseTokenAddress The address of the base token. - * */ - function _setBaseToken(address _baseTokenAddress) internal { - require(Address.isContract(_baseTokenAddress), "_baseTokenAddress not a contract"); - baseToken = IWrbtcERC20(_baseTokenAddress); - } + /** + * @notice Set base token address. + * + * @param _baseTokenAddress The address of the base token. + * */ + function _setBaseToken(address _baseTokenAddress) internal { + require(Address.isContract(_baseTokenAddress), "_baseTokenAddress not a contract"); + baseToken = IWrbtcERC20(_baseTokenAddress); + } } diff --git a/contracts/feeds/USDTPriceFeed.sol b/contracts/feeds/USDTPriceFeed.sol index 345cec3d3..916fb2ecd 100644 --- a/contracts/feeds/USDTPriceFeed.sol +++ b/contracts/feeds/USDTPriceFeed.sol @@ -10,22 +10,22 @@ import "./PriceFeeds.sol"; * trivial formula, always returning 1 and now. * */ contract USDTPriceFeed is IPriceFeedsExt { - uint256 private constant USDT_RATE = 1 ether; + uint256 private constant USDT_RATE = 1 ether; - /** - * @notice Get the USDT price. - * - * @return Always returns the trivial rate of 1. - * */ - function latestAnswer() external view returns (uint256) { - return USDT_RATE; - } + /** + * @notice Get the USDT price. + * + * @return Always returns the trivial rate of 1. + * */ + function latestAnswer() external view returns (uint256) { + return USDT_RATE; + } - /** - * @notice Get the las time the price was updated. - * @return Always trivial current block's timestamp. - */ - function latestTimestamp() external view returns (uint256) { - return now; - } + /** + * @notice Get the las time the price was updated. + * @return Always trivial current block's timestamp. + */ + function latestTimestamp() external view returns (uint256) { + return now; + } } diff --git a/contracts/feeds/testnet/PriceFeedsLocal.sol b/contracts/feeds/testnet/PriceFeedsLocal.sol index 7e46654fb..f114a68e7 100644 --- a/contracts/feeds/testnet/PriceFeedsLocal.sol +++ b/contracts/feeds/testnet/PriceFeedsLocal.sol @@ -16,76 +16,86 @@ import "../PriceFeeds.sol"; * This contract contains the logic of setting and getting rates between two tokens. * */ contract PriceFeedsLocal is PriceFeeds { - mapping(address => mapping(address => uint256)) public rates; + mapping(address => mapping(address => uint256)) public rates; - /// uint256 public slippageMultiplier = 100 ether; + /// uint256 public slippageMultiplier = 100 ether; - /** - * @notice Deploy local price feed contract. - * - * @param _wrbtcTokenAddress The address of the wrBTC instance. - * @param _protocolTokenAddress The address of the protocol token instance. - * */ - constructor(address _wrbtcTokenAddress, address _protocolTokenAddress) - public - PriceFeeds(_wrbtcTokenAddress, _protocolTokenAddress, _wrbtcTokenAddress) - {} + /** + * @notice Deploy local price feed contract. + * + * @param _wrbtcTokenAddress The address of the wrBTC instance. + * @param _protocolTokenAddress The address of the protocol token instance. + * */ + constructor(address _wrbtcTokenAddress, address _protocolTokenAddress) + public + PriceFeeds(_wrbtcTokenAddress, _protocolTokenAddress, _wrbtcTokenAddress) + {} - /** - * @notice Calculate the price ratio between two tokens. - * - * @param sourceToken The address of the source tokens. - * @param destToken The address of the destiny tokens. - * - * @return rate The price ratio source/dest. - * @return precision The ratio precision. - * */ - function _queryRate(address sourceToken, address destToken) internal view returns (uint256 rate, uint256 precision) { - require(!globalPricingPaused, "pricing is paused"); + /** + * @notice Calculate the price ratio between two tokens. + * + * @param sourceToken The address of the source tokens. + * @param destToken The address of the destiny tokens. + * + * @return rate The price ratio source/dest. + * @return precision The ratio precision. + * */ + function _queryRate(address sourceToken, address destToken) + internal + view + returns (uint256 rate, uint256 precision) + { + require(!globalPricingPaused, "pricing is paused"); - if (sourceToken == destToken) { - rate = 10**18; - precision = 10**18; - } else { - if (sourceToken == protocolTokenAddress) { - /// Hack for testnet; only returns price in rBTC. - rate = protocolTokenEthPrice; - } else if (destToken == protocolTokenAddress) { - /// Hack for testnet; only returns price in rBTC. - rate = SafeMath.div(10**36, protocolTokenEthPrice); - } else { - if (rates[sourceToken][destToken] != 0) { - rate = rates[sourceToken][destToken]; - } else { - uint256 sourceToEther = rates[sourceToken][address(wrbtcToken)] != 0 ? rates[sourceToken][address(wrbtcToken)] : 10**18; - uint256 etherToDest = rates[address(wrbtcToken)][destToken] != 0 ? rates[address(wrbtcToken)][destToken] : 10**18; + if (sourceToken == destToken) { + rate = 10**18; + precision = 10**18; + } else { + if (sourceToken == protocolTokenAddress) { + /// Hack for testnet; only returns price in rBTC. + rate = protocolTokenEthPrice; + } else if (destToken == protocolTokenAddress) { + /// Hack for testnet; only returns price in rBTC. + rate = SafeMath.div(10**36, protocolTokenEthPrice); + } else { + if (rates[sourceToken][destToken] != 0) { + rate = rates[sourceToken][destToken]; + } else { + uint256 sourceToEther = + rates[sourceToken][address(wrbtcToken)] != 0 + ? rates[sourceToken][address(wrbtcToken)] + : 10**18; + uint256 etherToDest = + rates[address(wrbtcToken)][destToken] != 0 + ? rates[address(wrbtcToken)][destToken] + : 10**18; - rate = sourceToEther.mul(etherToDest).div(10**18); - } - } - precision = _getDecimalPrecision(sourceToken, destToken); - } - } + rate = sourceToEther.mul(etherToDest).div(10**18); + } + } + precision = _getDecimalPrecision(sourceToken, destToken); + } + } - /** - * @notice Owner set price ratio between two tokens. - * - * @param sourceToken The address of the source tokens. - * @param destToken The address of the destiny tokens. - * @param rate The price ratio source/dest. - * */ - function setRates( - address sourceToken, - address destToken, - uint256 rate - ) public onlyOwner { - if (sourceToken != destToken) { - rates[sourceToken][destToken] = rate; - rates[destToken][sourceToken] = SafeMath.div(10**36, rate); - } - } + /** + * @notice Owner set price ratio between two tokens. + * + * @param sourceToken The address of the source tokens. + * @param destToken The address of the destiny tokens. + * @param rate The price ratio source/dest. + * */ + function setRates( + address sourceToken, + address destToken, + uint256 rate + ) public onlyOwner { + if (sourceToken != destToken) { + rates[sourceToken][destToken] = rate; + rates[destToken][sourceToken] = SafeMath.div(10**36, rate); + } + } - /*function setSlippageMultiplier( + /*function setSlippageMultiplier( uint256 _slippageMultiplier) public onlyOwner diff --git a/contracts/feeds/testnet/PriceFeedsMoC.sol b/contracts/feeds/testnet/PriceFeedsMoC.sol index dc0b76a57..fcf0a4bce 100644 --- a/contracts/feeds/testnet/PriceFeedsMoC.sol +++ b/contracts/feeds/testnet/PriceFeedsMoC.sol @@ -5,7 +5,7 @@ import "../IRSKOracle.sol"; import "../../openzeppelin/Address.sol"; interface Medianizer { - function peek() external view returns (bytes32, bool); + function peek() external view returns (bytes32, bool); } /** @@ -15,62 +15,62 @@ interface Medianizer { * and query last price update. * */ contract PriceFeedsMoC is IPriceFeedsExt, Ownable { - /* Storage */ + /* Storage */ - address public mocOracleAddress; - address public rskOracleAddress; + address public mocOracleAddress; + address public rskOracleAddress; - /* Events */ + /* Events */ - event SetMoCOracleAddress(address indexed mocOracleAddress, address changerAddress); - event SetRSKOracleAddress(address indexed rskOracleAddress, address changerAddress); + event SetMoCOracleAddress(address indexed mocOracleAddress, address changerAddress); + event SetRSKOracleAddress(address indexed rskOracleAddress, address changerAddress); - /* Functions */ + /* Functions */ - /** - * @notice Initialize a new MoC Oracle. - * - * @param _mocOracleAddress The MoC Oracle address. - * @param _rskOracleAddress The RSK Oracle address. - * */ - constructor(address _mocOracleAddress, address _rskOracleAddress) public { - setMoCOracleAddress(_mocOracleAddress); - setRSKOracleAddress(_rskOracleAddress); - } + /** + * @notice Initialize a new MoC Oracle. + * + * @param _mocOracleAddress The MoC Oracle address. + * @param _rskOracleAddress The RSK Oracle address. + * */ + constructor(address _mocOracleAddress, address _rskOracleAddress) public { + setMoCOracleAddress(_mocOracleAddress); + setRSKOracleAddress(_rskOracleAddress); + } - /** - * @notice Get the las time oracle updated the price. - * @return The latest time. - */ - function latestAnswer() external view returns (uint256) { - (bytes32 value, bool hasValue) = Medianizer(mocOracleAddress).peek(); - if (hasValue) { - return uint256(value); - } else { - (uint256 price, ) = IRSKOracle(rskOracleAddress).getPricing(); - return price; - } - } + /** + * @notice Get the las time oracle updated the price. + * @return The latest time. + */ + function latestAnswer() external view returns (uint256) { + (bytes32 value, bool hasValue) = Medianizer(mocOracleAddress).peek(); + if (hasValue) { + return uint256(value); + } else { + (uint256 price, ) = IRSKOracle(rskOracleAddress).getPricing(); + return price; + } + } - /** - * @notice Set the MoC Oracle address. - * - * @param _mocOracleAddress The MoC Oracle address. - */ - function setMoCOracleAddress(address _mocOracleAddress) public onlyOwner { - require(Address.isContract(_mocOracleAddress), "_mocOracleAddress not a contract"); - mocOracleAddress = _mocOracleAddress; - emit SetMoCOracleAddress(mocOracleAddress, msg.sender); - } + /** + * @notice Set the MoC Oracle address. + * + * @param _mocOracleAddress The MoC Oracle address. + */ + function setMoCOracleAddress(address _mocOracleAddress) public onlyOwner { + require(Address.isContract(_mocOracleAddress), "_mocOracleAddress not a contract"); + mocOracleAddress = _mocOracleAddress; + emit SetMoCOracleAddress(mocOracleAddress, msg.sender); + } - /** - * @notice Set the RSK Oracle address. - * - * @param _rskOracleAddress The RSK Oracle address. - */ - function setRSKOracleAddress(address _rskOracleAddress) public onlyOwner { - require(Address.isContract(_rskOracleAddress), "_rskOracleAddress not a contract"); - rskOracleAddress = _rskOracleAddress; - emit SetRSKOracleAddress(rskOracleAddress, msg.sender); - } + /** + * @notice Set the RSK Oracle address. + * + * @param _rskOracleAddress The RSK Oracle address. + */ + function setRSKOracleAddress(address _rskOracleAddress) public onlyOwner { + require(Address.isContract(_rskOracleAddress), "_rskOracleAddress not a contract"); + rskOracleAddress = _rskOracleAddress; + emit SetRSKOracleAddress(rskOracleAddress, msg.sender); + } } diff --git a/contracts/governance/ApprovalReceiver.sol b/contracts/governance/ApprovalReceiver.sol index a20973ff9..e5c09964c 100644 --- a/contracts/governance/ApprovalReceiver.sol +++ b/contracts/governance/ApprovalReceiver.sol @@ -7,99 +7,102 @@ import "../token/IApproveAndCall.sol"; * @title Base contract for receiving approval from SOV token. */ contract ApprovalReceiver is ErrorDecoder, IApproveAndCall { - modifier onlyThisContract() { - // Accepts calls only from receiveApproval function. - require(msg.sender == address(this), "unauthorized"); - _; - } + modifier onlyThisContract() { + // Accepts calls only from receiveApproval function. + require(msg.sender == address(this), "unauthorized"); + _; + } - /** - * @notice Receives approval from SOV token. - * @param _data The data will be used for low level call. - */ - function receiveApproval( - address _sender, - uint256 _amount, - address _token, - bytes calldata _data - ) external { - // Accepts calls only from SOV token. - require(msg.sender == _getToken(), "unauthorized"); - require(msg.sender == _token, "unauthorized"); + /** + * @notice Receives approval from SOV token. + * @param _data The data will be used for low level call. + */ + function receiveApproval( + address _sender, + uint256 _amount, + address _token, + bytes calldata _data + ) external { + // Accepts calls only from SOV token. + require(msg.sender == _getToken(), "unauthorized"); + require(msg.sender == _token, "unauthorized"); - // Only allowed methods. - bool isAllowed = false; - bytes4[] memory selectors = _getSelectors(); - bytes4 sig = _getSig(_data); - for (uint256 i = 0; i < selectors.length; i++) { - if (sig == selectors[i]) { - isAllowed = true; - break; - } - } - require(isAllowed, "method is not allowed"); + // Only allowed methods. + bool isAllowed = false; + bytes4[] memory selectors = _getSelectors(); + bytes4 sig = _getSig(_data); + for (uint256 i = 0; i < selectors.length; i++) { + if (sig == selectors[i]) { + isAllowed = true; + break; + } + } + require(isAllowed, "method is not allowed"); - // Check sender and amount. - address sender; - uint256 amount; - (, sender, amount) = abi.decode(abi.encodePacked(bytes28(0), _data), (bytes32, address, uint256)); - require(sender == _sender, "sender mismatch"); - require(amount == _amount, "amount mismatch"); + // Check sender and amount. + address sender; + uint256 amount; + (, sender, amount) = abi.decode( + abi.encodePacked(bytes28(0), _data), + (bytes32, address, uint256) + ); + require(sender == _sender, "sender mismatch"); + require(amount == _amount, "amount mismatch"); - _call(_data); - } + _call(_data); + } - /** - * @notice Returns token address, only this address can be a sender for receiveApproval. - * @dev Should be overridden in child contracts, otherwise error will be thrown. - * @return By default, 0x. When overriden, the token address making the call. - */ - function _getToken() internal view returns (address) { - return address(0); - } + /** + * @notice Returns token address, only this address can be a sender for receiveApproval. + * @dev Should be overridden in child contracts, otherwise error will be thrown. + * @return By default, 0x. When overriden, the token address making the call. + */ + function _getToken() internal view returns (address) { + return address(0); + } - /** - * @notice Returns list of function selectors allowed to be invoked. - * @dev Should be overridden in child contracts, otherwise error will be thrown. - * @return By default, empty array. When overriden, allowed selectors. - */ - function _getSelectors() internal view returns (bytes4[] memory) { - return new bytes4[](0); - } + /** + * @notice Returns list of function selectors allowed to be invoked. + * @dev Should be overridden in child contracts, otherwise error will be thrown. + * @return By default, empty array. When overriden, allowed selectors. + */ + function _getSelectors() internal view returns (bytes4[] memory) { + return new bytes4[](0); + } - /** - * @notice Makes call and reverts w/ enhanced error message. - * @param _data Error message as bytes. - */ - function _call(bytes memory _data) internal { - (bool success, bytes memory returnData) = address(this).call(_data); - if (!success) { - if (returnData.length <= ERROR_MESSAGE_SHIFT) { - revert("receiveApproval: Transaction execution reverted."); - } else { - revert(_addErrorMessage("receiveApproval: ", string(returnData))); - } - } - } + /** + * @notice Makes call and reverts w/ enhanced error message. + * @param _data Error message as bytes. + */ + function _call(bytes memory _data) internal { + (bool success, bytes memory returnData) = address(this).call(_data); + if (!success) { + if (returnData.length <= ERROR_MESSAGE_SHIFT) { + revert("receiveApproval: Transaction execution reverted."); + } else { + revert(_addErrorMessage("receiveApproval: ", string(returnData))); + } + } + } - /** - * @notice Extracts the called function selector, a hash of the signature. - * @dev The first four bytes of the call data for a function call specifies - * the function to be called. It is the first (left, high-order in big-endian) - * four bytes of the Keccak-256 (SHA-3) hash of the signature of the function. - * Solidity doesn't yet support a casting of byte[4] to bytes4. - * Example: - * msg.data: - * 0xcdcd77c000000000000000000000000000000000000000000000000000000000000 - * 000450000000000000000000000000000000000000000000000000000000000000001 - * selector (or method ID): 0xcdcd77c0 - * signature: baz(uint32,bool) - * @param _data The msg.data from the low level call. - * @return sig First 4 bytes of msg.data i.e. the selector, hash of the signature. - */ - function _getSig(bytes memory _data) internal pure returns (bytes4 sig) { - assembly { - sig := mload(add(_data, 32)) - } - } + /** + * @notice Extracts the called function selector, a hash of the signature. + * @dev The first four bytes of the call data for a function call specifies + * the function to be called. It is the first (left, high-order in big-endian) + * four bytes of the Keccak-256 (SHA-3) hash of the signature of the function. + * Solidity doesn't yet support a casting of byte[4] to bytes4. + * Example: + * msg.data: + * 0xcdcd77c000000000000000000000000000000000000000000000000000000000000 + * 000450000000000000000000000000000000000000000000000000000000000000001 + * selector (or method ID): 0xcdcd77c0 + * signature: baz(uint32,bool) + * @param _data The msg.data from the low level call. + * @return sig First 4 bytes of msg.data i.e. the selector, hash of the signature. + */ + function _getSig(bytes memory _data) internal pure returns (bytes4 sig) { + assembly { + sig := mload(add(_data, 32)) + } + } } diff --git a/contracts/governance/ErrorDecoder.sol b/contracts/governance/ErrorDecoder.sol index 4602304e4..254677cbd 100644 --- a/contracts/governance/ErrorDecoder.sol +++ b/contracts/governance/ErrorDecoder.sol @@ -28,26 +28,31 @@ pragma solidity ^0.5.17; * 7863656564206d696e696d756d2064656c61792e000000000000000000000000 */ contract ErrorDecoder { - uint256 constant ERROR_MESSAGE_SHIFT = 68; // EVM silent revert error string length + uint256 constant ERROR_MESSAGE_SHIFT = 68; // EVM silent revert error string length - /** - * @notice Concats two error strings taking into account ERROR_MESSAGE_SHIFT. - * @param str1 First string, usually a hardcoded context written by dev. - * @param str2 Second string, usually the error message from the reverted call. - * @return The concatenated error string - */ - function _addErrorMessage(string memory str1, string memory str2) internal pure returns (string memory) { - bytes memory bytesStr1 = bytes(str1); - bytes memory bytesStr2 = bytes(str2); - string memory str12 = new string(bytesStr1.length + bytesStr2.length - ERROR_MESSAGE_SHIFT); - bytes memory bytesStr12 = bytes(str12); - uint256 j = 0; - for (uint256 i = 0; i < bytesStr1.length; i++) { - bytesStr12[j++] = bytesStr1[i]; - } - for (uint256 i = ERROR_MESSAGE_SHIFT; i < bytesStr2.length; i++) { - bytesStr12[j++] = bytesStr2[i]; - } - return string(bytesStr12); - } + /** + * @notice Concats two error strings taking into account ERROR_MESSAGE_SHIFT. + * @param str1 First string, usually a hardcoded context written by dev. + * @param str2 Second string, usually the error message from the reverted call. + * @return The concatenated error string + */ + function _addErrorMessage(string memory str1, string memory str2) + internal + pure + returns (string memory) + { + bytes memory bytesStr1 = bytes(str1); + bytes memory bytesStr2 = bytes(str2); + string memory str12 = + new string(bytesStr1.length + bytesStr2.length - ERROR_MESSAGE_SHIFT); + bytes memory bytesStr12 = bytes(str12); + uint256 j = 0; + for (uint256 i = 0; i < bytesStr1.length; i++) { + bytesStr12[j++] = bytesStr1[i]; + } + for (uint256 i = ERROR_MESSAGE_SHIFT; i < bytesStr2.length; i++) { + bytesStr12[j++] = bytesStr2[i]; + } + return string(bytesStr12); + } } diff --git a/contracts/governance/FeeSharingProxy/FeeSharingLogic.sol b/contracts/governance/FeeSharingProxy/FeeSharingLogic.sol index d45c40040..ff0735488 100644 --- a/contracts/governance/FeeSharingProxy/FeeSharingLogic.sol +++ b/contracts/governance/FeeSharingProxy/FeeSharingLogic.sol @@ -43,429 +43,514 @@ import "../../interfaces/IConverterAMM.sol"; * before withdrawing, but not yet implemented. * */ contract FeeSharingLogic is SafeMath96, IFeeSharingProxy, Ownable, FeeSharingProxyStorage { - using SafeMath for uint256; - using SafeERC20 for IERC20; - - /* Events */ - - /// @notice An event emitted when fee get withdrawn. - event FeeWithdrawn(address indexed sender, address indexed token, uint256 amount); - - /// @notice An event emitted when tokens transferred. - event TokensTransferred(address indexed sender, address indexed token, uint256 amount); - - /// @notice An event emitted when checkpoint added. - event CheckpointAdded(address indexed sender, address indexed token, uint256 amount); - - /// @notice An event emitted when user fee get withdrawn. - event UserFeeWithdrawn(address indexed sender, address indexed receiver, address indexed token, uint256 amount); - - /** - * @notice An event emitted when fee from AMM get withdrawn. - * - * @param sender sender who initiate the withdrawn amm fees. - * @param converter the converter address. - * @param amount total amount of fee (Already converted to WRBTC). - */ - event FeeAMMWithdrawn(address indexed sender, address indexed converter, uint256 amount); - - /// @notice An event emitted when converter address has been registered to be whitelisted. - event WhitelistedConverter(address indexed sender, address converter); - - /// @notice An event emitted when converter address has been removed from whitelist. - event UnwhitelistedConverter(address indexed sender, address converter); - - /* Functions */ - - /** - * @notice Withdraw fees for the given token: - * lendingFee + tradingFee + borrowingFee - * the fees (except SOV) will be converted in wRBTC form, and then will be transferred to wRBTC loan pool. - * For SOV, it will be directly deposited into the feeSharingProxy from the protocol. - * - * @param _tokens array address of the token - * */ - function withdrawFees(address[] memory _tokens) public { - for (uint256 i = 0; i < _tokens.length; i++) { - require(Address.isContract(_tokens[i]), "FeeSharingProxy::withdrawFees: token is not a contract"); - } - - uint256 wrbtcAmountWithdrawn = protocol.withdrawFees(_tokens, address(this)); - uint256 poolTokenAmount; - - address wRBTCAddress = protocol.wrbtcToken(); - require(wRBTCAddress != address(0), "FeeSharingProxy::withdrawFees: wRBTCAddress is not set"); - - address loanPoolToken = protocol.underlyingToLoanPool(wRBTCAddress); - require(loanPoolToken != address(0), "FeeSharingProxy::withdrawFees: loan wRBTC not found"); - - if (wrbtcAmountWithdrawn > 0) { - /// @dev TODO can be also used - function addLiquidity(IERC20Token _reserveToken, uint256 _amount, uint256 _minReturn) - IERC20(wRBTCAddress).approve(loanPoolToken, wrbtcAmountWithdrawn); - poolTokenAmount = ILoanToken(loanPoolToken).mint(address(this), wrbtcAmountWithdrawn); - - /// @notice Update unprocessed amount of tokens - uint96 amount96 = safe96(poolTokenAmount, "FeeSharingProxy::withdrawFees: pool token amount exceeds 96 bits"); - - _addCheckpoint(loanPoolToken, amount96); - } - - emit FeeWithdrawn(msg.sender, loanPoolToken, poolTokenAmount); - } - - /** - * @notice Withdraw amm fees for the given converter addresses: - * protocolFee from the conversion - * the fees will be converted in wRBTC form, and then will be transferred to wRBTC loan pool - * - * @param _converters array addresses of the converters - * */ - function withdrawFeesAMM(address[] memory _converters) public { - address wRBTCAddress = protocol.wrbtcToken(); - require(wRBTCAddress != address(0), "FeeSharingProxy::withdrawFees: wRBTCAddress is not set"); - - address loanPoolToken = protocol.underlyingToLoanPool(wRBTCAddress); - require(loanPoolToken != address(0), "FeeSharingProxy::withdrawFees: loan wRBTC not found"); - - // Validate - _validateWhitelistedConverter(_converters); - - uint96 totalPoolTokenAmount; - for (uint256 i = 0; i < _converters.length; i++) { - uint256 wrbtcAmountWithdrawn = IConverterAMM(_converters[i]).withdrawFees(address(this)); - - if (wrbtcAmountWithdrawn > 0) { - /// @dev TODO can be also used - function addLiquidity(IERC20Token _reserveToken, uint256 _amount, uint256 _minReturn) - IERC20(wRBTCAddress).approve(loanPoolToken, wrbtcAmountWithdrawn); - uint256 poolTokenAmount = ILoanToken(loanPoolToken).mint(address(this), wrbtcAmountWithdrawn); - - /// @notice Update unprocessed amount of tokens - uint96 amount96 = safe96(poolTokenAmount, "FeeSharingProxy::withdrawFees: pool token amount exceeds 96 bits"); - - totalPoolTokenAmount = add96( - totalPoolTokenAmount, - amount96, - "FeeSharingProxy::withdrawFees: total pool token amount exceeds 96 bits" - ); - - emit FeeAMMWithdrawn(msg.sender, _converters[i], poolTokenAmount); - } - } - - if (totalPoolTokenAmount > 0) { - _addCheckpoint(loanPoolToken, totalPoolTokenAmount); - } - } - - /** - * @notice Transfer tokens to this contract. - * @dev We just update amount of tokens here and write checkpoint in a separate methods - * in order to prevent adding checkpoints too often. - * @param _token Address of the token. - * @param _amount Amount to be transferred. - * */ - function transferTokens(address _token, uint96 _amount) public { - require(_token != address(0), "FeeSharingProxy::transferTokens: invalid address"); - require(_amount > 0, "FeeSharingProxy::transferTokens: invalid amount"); - - /// @notice Transfer tokens from msg.sender - bool success = IERC20(_token).transferFrom(address(msg.sender), address(this), _amount); - require(success, "Staking::transferTokens: token transfer failed"); - - _addCheckpoint(_token, _amount); - - emit TokensTransferred(msg.sender, _token, _amount); - } - - /** - * @notice Add checkpoint with accumulated amount by function invocation. - * @param _token Address of the token. - * */ - function _addCheckpoint(address _token, uint96 _amount) internal { - if (block.timestamp - lastFeeWithdrawalTime[_token] >= FEE_WITHDRAWAL_INTERVAL) { - lastFeeWithdrawalTime[_token] = block.timestamp; - uint96 amount = add96(unprocessedAmount[_token], _amount, "FeeSharingProxy::_addCheckpoint: amount exceeds 96 bits"); - - /// @notice Reset unprocessed amount of tokens to zero. - unprocessedAmount[_token] = 0; - - /// @notice Write a regular checkpoint. - _writeTokenCheckpoint(_token, amount); - } else { - unprocessedAmount[_token] = add96( - unprocessedAmount[_token], - _amount, - "FeeSharingProxy::_addCheckpoint: unprocessedAmount exceeds 96 bits" - ); - } - } - - /** - * @notice Withdraw accumulated fee to the message sender. - * - * The Sovryn protocol collects fees on every trade/swap and loan. - * These fees will be distributed to SOV stakers based on their voting - * power as a percentage of total voting power. Therefore, staking more - * SOV and/or staking for longer will increase your share of the fees - * generated, meaning you will earn more from staking. - * - * This function will directly burnToBTC and use the msg.sender (user) as the receiver - * - * @param _loanPoolToken Address of the pool token. - * @param _maxCheckpoints Maximum number of checkpoints to be processed. - * @param _receiver The receiver of tokens or msg.sender - * */ - function withdraw( - address _loanPoolToken, - uint32 _maxCheckpoints, - address _receiver - ) public nonReentrant { - /// @dev Prevents processing / checkpoints because of block gas limit. - require(_maxCheckpoints > 0, "FeeSharingProxy::withdraw: _maxCheckpoints should be positive"); - - address wRBTCAddress = protocol.wrbtcToken(); - require(wRBTCAddress != address(0), "FeeSharingProxy::withdraw: wRBTCAddress is not set"); - - address loanPoolTokenWRBTC = protocol.underlyingToLoanPool(wRBTCAddress); - require(loanPoolTokenWRBTC != address(0), "FeeSharingProxy::withdraw: loan wRBTC not found"); - - address user = msg.sender; - if (_receiver == address(0)) { - _receiver = msg.sender; - } - - uint256 amount; - uint256 end; - (amount, end) = _getAccumulatedFees(user, _loanPoolToken, _maxCheckpoints); - require(amount > 0, "FeeSharingProxy::withdrawFees: no tokens for a withdrawal"); - - processedCheckpoints[user][_loanPoolToken] = end; - - if (loanPoolTokenWRBTC == _loanPoolToken) { - // We will change, so that feeSharingProxy will directly burn then loanToken (IWRBTC) to rbtc and send to the user --- by call burnToBTC function - uint256 loanAmountPaid = ILoanTokenWRBTC(_loanPoolToken).burnToBTC(_receiver, amount, false); - } else { - // Previously it directly send the loanToken to the user - require(IERC20(_loanPoolToken).transfer(user, amount), "FeeSharingProxy::withdraw: withdrawal failed"); - } - - emit UserFeeWithdrawn(msg.sender, _receiver, _loanPoolToken, amount); - } - - /** - * @notice Get the accumulated loan pool fee of the message sender. - * @param _user The address of the user or contract. - * @param _loanPoolToken Address of the pool token. - * @return The accumulated fee for the message sender. - * */ - function getAccumulatedFees(address _user, address _loanPoolToken) public view returns (uint256) { - uint256 amount; - (amount, ) = _getAccumulatedFees(_user, _loanPoolToken, 0); - return amount; - } - - /** - * @notice Whenever fees are withdrawn, the staking contract needs to - * checkpoint the block number, the number of pool tokens and the - * total voting power at that time (read from the staking contract). - * While the total voting power would not necessarily need to be - * checkpointed, it makes sense to save gas cost on withdrawal. - * - * When the user wants to withdraw its share of tokens, we need - * to iterate over all of the checkpoints since the users last - * withdrawal (note: remember last withdrawal block), query the - * user’s balance at the checkpoint blocks from the staking contract, - * compute his share of the checkpointed tokens and add them up. - * The maximum number of checkpoints to process at once should be limited. - * - * @param _user Address of the user's account. - * @param _loanPoolToken Loan pool token address. - * @param _maxCheckpoints Checkpoint index incremental. - * */ - function _getAccumulatedFees( - address _user, - address _loanPoolToken, - uint32 _maxCheckpoints - ) internal view returns (uint256, uint256) { - if (staking.isVestingContract(_user)) { - return (0, 0); - } - - uint256 start = processedCheckpoints[_user][_loanPoolToken]; - uint256 end; - - /// @dev Additional bool param can't be used because of stack too deep error. - if (_maxCheckpoints > 0) { - /// @dev withdraw -> _getAccumulatedFees - require(start < numTokenCheckpoints[_loanPoolToken], "FeeSharingProxy::withdrawFees: no tokens for a withdrawal"); - end = _getEndOfRange(start, _loanPoolToken, _maxCheckpoints); - } else { - /// @dev getAccumulatedFees -> _getAccumulatedFees - /// Don't throw error for getter invocation outside of transaction. - if (start >= numTokenCheckpoints[_loanPoolToken]) { - return (0, numTokenCheckpoints[_loanPoolToken]); - } - end = numTokenCheckpoints[_loanPoolToken]; - } - - uint256 amount = 0; - uint256 cachedLockDate = 0; - uint96 cachedWeightedStake = 0; - for (uint256 i = start; i < end; i++) { - Checkpoint storage checkpoint = tokenCheckpoints[_loanPoolToken][i]; - uint256 lockDate = staking.timestampToLockDate(checkpoint.timestamp); - uint96 weightedStake; - if (lockDate == cachedLockDate) { - weightedStake = cachedWeightedStake; - } else { - /// @dev We need to use "checkpoint.blockNumber - 1" here to calculate weighted stake - /// For the same block like we did for total voting power in _writeTokenCheckpoint - weightedStake = staking.getPriorWeightedStake(_user, checkpoint.blockNumber - 1, checkpoint.timestamp); - cachedWeightedStake = weightedStake; - cachedLockDate = lockDate; - } - uint256 share = uint256(checkpoint.numTokens).mul(weightedStake).div(uint256(checkpoint.totalWeightedStake)); - amount = amount.add(share); - } - return (amount, end); - } - - /** - * @notice Withdrawal should only be possible for blocks which were already - * mined. If the fees are withdrawn in the same block as the user withdrawal - * they are not considered by the withdrawing logic (to avoid inconsistencies). - * - * @param start Start of the range. - * @param _loanPoolToken Loan pool token address. - * @param _maxCheckpoints Checkpoint index incremental. - * */ - function _getEndOfRange( - uint256 start, - address _loanPoolToken, - uint32 _maxCheckpoints - ) internal view returns (uint256) { - uint256 nCheckpoints = numTokenCheckpoints[_loanPoolToken]; - uint256 end; - if (_maxCheckpoints == 0) { - /// @dev All checkpoints will be processed (only for getter outside of a transaction). - end = nCheckpoints; - } else { - if (_maxCheckpoints > MAX_CHECKPOINTS) { - _maxCheckpoints = MAX_CHECKPOINTS; - } - end = safe32(start + _maxCheckpoints, "FeeSharingProxy::withdraw: checkpoint index exceeds 32 bits"); - if (end > nCheckpoints) { - end = nCheckpoints; - } - } - - /// @dev Withdrawal should only be possible for blocks which were already mined. - uint32 lastBlockNumber = tokenCheckpoints[_loanPoolToken][end - 1].blockNumber; - if (block.number == lastBlockNumber) { - end--; - } - return end; - } - - /** - * @notice Write a regular checkpoint w/ the foolowing data: - * block number, block timestamp, total weighted stake and num of tokens. - * @param _token The pool token address. - * @param _numTokens The amount of pool tokens. - * */ - function _writeTokenCheckpoint(address _token, uint96 _numTokens) internal { - uint32 blockNumber = safe32(block.number, "FeeSharingProxy::_writeCheckpoint: block number exceeds 32 bits"); - uint32 blockTimestamp = safe32(block.timestamp, "FeeSharingProxy::_writeCheckpoint: block timestamp exceeds 32 bits"); - uint256 nCheckpoints = numTokenCheckpoints[_token]; - - uint96 totalWeightedStake = _getVoluntaryWeightedStake(blockNumber - 1, block.timestamp); - require(totalWeightedStake > 0, "Invalid totalWeightedStake"); - if (nCheckpoints > 0 && tokenCheckpoints[_token][nCheckpoints - 1].blockNumber == blockNumber) { - tokenCheckpoints[_token][nCheckpoints - 1].totalWeightedStake = totalWeightedStake; - tokenCheckpoints[_token][nCheckpoints - 1].numTokens = _numTokens; - } else { - tokenCheckpoints[_token][nCheckpoints] = Checkpoint(blockNumber, blockTimestamp, totalWeightedStake, _numTokens); - numTokenCheckpoints[_token] = nCheckpoints + 1; - } - emit CheckpointAdded(msg.sender, _token, _numTokens); - } - - /** - * Queries the total weighted stake and the weighted stake of vesting contracts and returns the difference - * @param blockNumber the blocknumber - * @param timestamp the timestamp - */ - function _getVoluntaryWeightedStake(uint32 blockNumber, uint256 timestamp) internal view returns (uint96 totalWeightedStake) { - uint96 vestingWeightedStake = staking.getPriorVestingWeightedStake(blockNumber, timestamp); - totalWeightedStake = staking.getPriorTotalVotingPower(blockNumber, timestamp); - totalWeightedStake = sub96( - totalWeightedStake, - vestingWeightedStake, - "FeeSharingProxy::_getTotalVoluntaryWeightedStake: vested stake exceeds total stake" - ); - } - - /** - * @dev Whitelisting converter address. - * - * @param converterAddress converter address to be whitelisted. - */ - function addWhitelistedConverterAddress(address converterAddress) external onlyOwner { - require(Address.isContract(converterAddress), "Non contract address given"); - whitelistedConverterList.add(converterAddress); - emit WhitelistedConverter(msg.sender, converterAddress); - } - - /** - * @dev Removing converter address from whitelist. - * - * @param converterAddress converter address to be removed from whitelist. - */ - function removeWhitelistedConverterAddress(address converterAddress) external onlyOwner { - whitelistedConverterList.remove(converterAddress); - emit UnwhitelistedConverter(msg.sender, converterAddress); - } - - /** - * @notice Getter to query all of the whitelisted converter. - * @return All of the whitelisted converter list. - */ - function getWhitelistedConverterList() external view returns (address[] memory converterList) { - converterList = whitelistedConverterList.enumerate(); - } - - /** - * @dev validate array of given address whether is whitelisted or not. - * @dev if one of them is not whitelisted, then revert. - * - * @param converterAddresses array of converter addresses. - */ - function _validateWhitelistedConverter(address[] memory converterAddresses) private view { - for (uint256 i = 0; i < converterAddresses.length; i++) { - require(whitelistedConverterList.contains(converterAddresses[i]), "Invalid Converter"); - } - } - - function withdrawWRBTC(address receiver, uint256 wrbtcAmount) external onlyOwner { - address wRBTCAddress = protocol.wrbtcToken(); - require(wRBTCAddress != address(0), "FeeSharingProxy::withdrawFees: wRBTCAddress is not set"); - - uint256 balance = IERC20(wRBTCAddress).balanceOf(address(this)); - require(wrbtcAmount <= balance, "Insufficient balance"); - - IERC20(wRBTCAddress).safeTransfer(receiver, wrbtcAmount); - } + using SafeMath for uint256; + using SafeERC20 for IERC20; + + /* Events */ + + /// @notice An event emitted when fee get withdrawn. + event FeeWithdrawn(address indexed sender, address indexed token, uint256 amount); + + /// @notice An event emitted when tokens transferred. + event TokensTransferred(address indexed sender, address indexed token, uint256 amount); + + /// @notice An event emitted when checkpoint added. + event CheckpointAdded(address indexed sender, address indexed token, uint256 amount); + + /// @notice An event emitted when user fee get withdrawn. + event UserFeeWithdrawn( + address indexed sender, + address indexed receiver, + address indexed token, + uint256 amount + ); + + /** + * @notice An event emitted when fee from AMM get withdrawn. + * + * @param sender sender who initiate the withdrawn amm fees. + * @param converter the converter address. + * @param amount total amount of fee (Already converted to WRBTC). + */ + event FeeAMMWithdrawn(address indexed sender, address indexed converter, uint256 amount); + + /// @notice An event emitted when converter address has been registered to be whitelisted. + event WhitelistedConverter(address indexed sender, address converter); + + /// @notice An event emitted when converter address has been removed from whitelist. + event UnwhitelistedConverter(address indexed sender, address converter); + + /* Functions */ + + /** + * @notice Withdraw fees for the given token: + * lendingFee + tradingFee + borrowingFee + * the fees (except SOV) will be converted in wRBTC form, and then will be transferred to wRBTC loan pool. + * For SOV, it will be directly deposited into the feeSharingProxy from the protocol. + * + * @param _tokens array address of the token + * */ + function withdrawFees(address[] memory _tokens) public { + for (uint256 i = 0; i < _tokens.length; i++) { + require( + Address.isContract(_tokens[i]), + "FeeSharingProxy::withdrawFees: token is not a contract" + ); + } + + uint256 wrbtcAmountWithdrawn = protocol.withdrawFees(_tokens, address(this)); + uint256 poolTokenAmount; + + address wRBTCAddress = protocol.wrbtcToken(); + require( + wRBTCAddress != address(0), + "FeeSharingProxy::withdrawFees: wRBTCAddress is not set" + ); + + address loanPoolToken = protocol.underlyingToLoanPool(wRBTCAddress); + require( + loanPoolToken != address(0), + "FeeSharingProxy::withdrawFees: loan wRBTC not found" + ); + + if (wrbtcAmountWithdrawn > 0) { + /// @dev TODO can be also used - function addLiquidity(IERC20Token _reserveToken, uint256 _amount, uint256 _minReturn) + IERC20(wRBTCAddress).approve(loanPoolToken, wrbtcAmountWithdrawn); + poolTokenAmount = ILoanToken(loanPoolToken).mint(address(this), wrbtcAmountWithdrawn); + + /// @notice Update unprocessed amount of tokens + uint96 amount96 = + safe96( + poolTokenAmount, + "FeeSharingProxy::withdrawFees: pool token amount exceeds 96 bits" + ); + + _addCheckpoint(loanPoolToken, amount96); + } + + emit FeeWithdrawn(msg.sender, loanPoolToken, poolTokenAmount); + } + + /** + * @notice Withdraw amm fees for the given converter addresses: + * protocolFee from the conversion + * the fees will be converted in wRBTC form, and then will be transferred to wRBTC loan pool + * + * @param _converters array addresses of the converters + * */ + function withdrawFeesAMM(address[] memory _converters) public { + address wRBTCAddress = protocol.wrbtcToken(); + require( + wRBTCAddress != address(0), + "FeeSharingProxy::withdrawFees: wRBTCAddress is not set" + ); + + address loanPoolToken = protocol.underlyingToLoanPool(wRBTCAddress); + require( + loanPoolToken != address(0), + "FeeSharingProxy::withdrawFees: loan wRBTC not found" + ); + + // Validate + _validateWhitelistedConverter(_converters); + + uint96 totalPoolTokenAmount; + for (uint256 i = 0; i < _converters.length; i++) { + uint256 wrbtcAmountWithdrawn = + IConverterAMM(_converters[i]).withdrawFees(address(this)); + + if (wrbtcAmountWithdrawn > 0) { + /// @dev TODO can be also used - function addLiquidity(IERC20Token _reserveToken, uint256 _amount, uint256 _minReturn) + IERC20(wRBTCAddress).approve(loanPoolToken, wrbtcAmountWithdrawn); + uint256 poolTokenAmount = + ILoanToken(loanPoolToken).mint(address(this), wrbtcAmountWithdrawn); + + /// @notice Update unprocessed amount of tokens + uint96 amount96 = + safe96( + poolTokenAmount, + "FeeSharingProxy::withdrawFees: pool token amount exceeds 96 bits" + ); + + totalPoolTokenAmount = add96( + totalPoolTokenAmount, + amount96, + "FeeSharingProxy::withdrawFees: total pool token amount exceeds 96 bits" + ); + + emit FeeAMMWithdrawn(msg.sender, _converters[i], poolTokenAmount); + } + } + + if (totalPoolTokenAmount > 0) { + _addCheckpoint(loanPoolToken, totalPoolTokenAmount); + } + } + + /** + * @notice Transfer tokens to this contract. + * @dev We just update amount of tokens here and write checkpoint in a separate methods + * in order to prevent adding checkpoints too often. + * @param _token Address of the token. + * @param _amount Amount to be transferred. + * */ + function transferTokens(address _token, uint96 _amount) public { + require(_token != address(0), "FeeSharingProxy::transferTokens: invalid address"); + require(_amount > 0, "FeeSharingProxy::transferTokens: invalid amount"); + + /// @notice Transfer tokens from msg.sender + bool success = IERC20(_token).transferFrom(address(msg.sender), address(this), _amount); + require(success, "Staking::transferTokens: token transfer failed"); + + _addCheckpoint(_token, _amount); + + emit TokensTransferred(msg.sender, _token, _amount); + } + + /** + * @notice Add checkpoint with accumulated amount by function invocation. + * @param _token Address of the token. + * */ + function _addCheckpoint(address _token, uint96 _amount) internal { + if (block.timestamp - lastFeeWithdrawalTime[_token] >= FEE_WITHDRAWAL_INTERVAL) { + lastFeeWithdrawalTime[_token] = block.timestamp; + uint96 amount = + add96( + unprocessedAmount[_token], + _amount, + "FeeSharingProxy::_addCheckpoint: amount exceeds 96 bits" + ); + + /// @notice Reset unprocessed amount of tokens to zero. + unprocessedAmount[_token] = 0; + + /// @notice Write a regular checkpoint. + _writeTokenCheckpoint(_token, amount); + } else { + unprocessedAmount[_token] = add96( + unprocessedAmount[_token], + _amount, + "FeeSharingProxy::_addCheckpoint: unprocessedAmount exceeds 96 bits" + ); + } + } + + /** + * @notice Withdraw accumulated fee to the message sender. + * + * The Sovryn protocol collects fees on every trade/swap and loan. + * These fees will be distributed to SOV stakers based on their voting + * power as a percentage of total voting power. Therefore, staking more + * SOV and/or staking for longer will increase your share of the fees + * generated, meaning you will earn more from staking. + * + * This function will directly burnToBTC and use the msg.sender (user) as the receiver + * + * @param _loanPoolToken Address of the pool token. + * @param _maxCheckpoints Maximum number of checkpoints to be processed. + * @param _receiver The receiver of tokens or msg.sender + * */ + function withdraw( + address _loanPoolToken, + uint32 _maxCheckpoints, + address _receiver + ) public nonReentrant { + /// @dev Prevents processing / checkpoints because of block gas limit. + require( + _maxCheckpoints > 0, + "FeeSharingProxy::withdraw: _maxCheckpoints should be positive" + ); + + address wRBTCAddress = protocol.wrbtcToken(); + require(wRBTCAddress != address(0), "FeeSharingProxy::withdraw: wRBTCAddress is not set"); + + address loanPoolTokenWRBTC = protocol.underlyingToLoanPool(wRBTCAddress); + require( + loanPoolTokenWRBTC != address(0), + "FeeSharingProxy::withdraw: loan wRBTC not found" + ); + + address user = msg.sender; + if (_receiver == address(0)) { + _receiver = msg.sender; + } + + uint256 amount; + uint256 end; + (amount, end) = _getAccumulatedFees(user, _loanPoolToken, _maxCheckpoints); + require(amount > 0, "FeeSharingProxy::withdrawFees: no tokens for a withdrawal"); + + processedCheckpoints[user][_loanPoolToken] = end; + + if (loanPoolTokenWRBTC == _loanPoolToken) { + // We will change, so that feeSharingProxy will directly burn then loanToken (IWRBTC) to rbtc and send to the user --- by call burnToBTC function + uint256 loanAmountPaid = + ILoanTokenWRBTC(_loanPoolToken).burnToBTC(_receiver, amount, false); + } else { + // Previously it directly send the loanToken to the user + require( + IERC20(_loanPoolToken).transfer(user, amount), + "FeeSharingProxy::withdraw: withdrawal failed" + ); + } + + emit UserFeeWithdrawn(msg.sender, _receiver, _loanPoolToken, amount); + } + + /** + * @notice Get the accumulated loan pool fee of the message sender. + * @param _user The address of the user or contract. + * @param _loanPoolToken Address of the pool token. + * @return The accumulated fee for the message sender. + * */ + function getAccumulatedFees(address _user, address _loanPoolToken) + public + view + returns (uint256) + { + uint256 amount; + (amount, ) = _getAccumulatedFees(_user, _loanPoolToken, 0); + return amount; + } + + /** + * @notice Whenever fees are withdrawn, the staking contract needs to + * checkpoint the block number, the number of pool tokens and the + * total voting power at that time (read from the staking contract). + * While the total voting power would not necessarily need to be + * checkpointed, it makes sense to save gas cost on withdrawal. + * + * When the user wants to withdraw its share of tokens, we need + * to iterate over all of the checkpoints since the users last + * withdrawal (note: remember last withdrawal block), query the + * user’s balance at the checkpoint blocks from the staking contract, + * compute his share of the checkpointed tokens and add them up. + * The maximum number of checkpoints to process at once should be limited. + * + * @param _user Address of the user's account. + * @param _loanPoolToken Loan pool token address. + * @param _maxCheckpoints Checkpoint index incremental. + * */ + function _getAccumulatedFees( + address _user, + address _loanPoolToken, + uint32 _maxCheckpoints + ) internal view returns (uint256, uint256) { + if (staking.isVestingContract(_user)) { + return (0, 0); + } + + uint256 start = processedCheckpoints[_user][_loanPoolToken]; + uint256 end; + + /// @dev Additional bool param can't be used because of stack too deep error. + if (_maxCheckpoints > 0) { + /// @dev withdraw -> _getAccumulatedFees + require( + start < numTokenCheckpoints[_loanPoolToken], + "FeeSharingProxy::withdrawFees: no tokens for a withdrawal" + ); + end = _getEndOfRange(start, _loanPoolToken, _maxCheckpoints); + } else { + /// @dev getAccumulatedFees -> _getAccumulatedFees + /// Don't throw error for getter invocation outside of transaction. + if (start >= numTokenCheckpoints[_loanPoolToken]) { + return (0, numTokenCheckpoints[_loanPoolToken]); + } + end = numTokenCheckpoints[_loanPoolToken]; + } + + uint256 amount = 0; + uint256 cachedLockDate = 0; + uint96 cachedWeightedStake = 0; + for (uint256 i = start; i < end; i++) { + Checkpoint storage checkpoint = tokenCheckpoints[_loanPoolToken][i]; + uint256 lockDate = staking.timestampToLockDate(checkpoint.timestamp); + uint96 weightedStake; + if (lockDate == cachedLockDate) { + weightedStake = cachedWeightedStake; + } else { + /// @dev We need to use "checkpoint.blockNumber - 1" here to calculate weighted stake + /// For the same block like we did for total voting power in _writeTokenCheckpoint + weightedStake = staking.getPriorWeightedStake( + _user, + checkpoint.blockNumber - 1, + checkpoint.timestamp + ); + cachedWeightedStake = weightedStake; + cachedLockDate = lockDate; + } + uint256 share = + uint256(checkpoint.numTokens).mul(weightedStake).div( + uint256(checkpoint.totalWeightedStake) + ); + amount = amount.add(share); + } + return (amount, end); + } + + /** + * @notice Withdrawal should only be possible for blocks which were already + * mined. If the fees are withdrawn in the same block as the user withdrawal + * they are not considered by the withdrawing logic (to avoid inconsistencies). + * + * @param start Start of the range. + * @param _loanPoolToken Loan pool token address. + * @param _maxCheckpoints Checkpoint index incremental. + * */ + function _getEndOfRange( + uint256 start, + address _loanPoolToken, + uint32 _maxCheckpoints + ) internal view returns (uint256) { + uint256 nCheckpoints = numTokenCheckpoints[_loanPoolToken]; + uint256 end; + if (_maxCheckpoints == 0) { + /// @dev All checkpoints will be processed (only for getter outside of a transaction). + end = nCheckpoints; + } else { + if (_maxCheckpoints > MAX_CHECKPOINTS) { + _maxCheckpoints = MAX_CHECKPOINTS; + } + end = safe32( + start + _maxCheckpoints, + "FeeSharingProxy::withdraw: checkpoint index exceeds 32 bits" + ); + if (end > nCheckpoints) { + end = nCheckpoints; + } + } + + /// @dev Withdrawal should only be possible for blocks which were already mined. + uint32 lastBlockNumber = tokenCheckpoints[_loanPoolToken][end - 1].blockNumber; + if (block.number == lastBlockNumber) { + end--; + } + return end; + } + + /** + * @notice Write a regular checkpoint w/ the foolowing data: + * block number, block timestamp, total weighted stake and num of tokens. + * @param _token The pool token address. + * @param _numTokens The amount of pool tokens. + * */ + function _writeTokenCheckpoint(address _token, uint96 _numTokens) internal { + uint32 blockNumber = + safe32( + block.number, + "FeeSharingProxy::_writeCheckpoint: block number exceeds 32 bits" + ); + uint32 blockTimestamp = + safe32( + block.timestamp, + "FeeSharingProxy::_writeCheckpoint: block timestamp exceeds 32 bits" + ); + uint256 nCheckpoints = numTokenCheckpoints[_token]; + + uint96 totalWeightedStake = _getVoluntaryWeightedStake(blockNumber - 1, block.timestamp); + require(totalWeightedStake > 0, "Invalid totalWeightedStake"); + if ( + nCheckpoints > 0 && + tokenCheckpoints[_token][nCheckpoints - 1].blockNumber == blockNumber + ) { + tokenCheckpoints[_token][nCheckpoints - 1].totalWeightedStake = totalWeightedStake; + tokenCheckpoints[_token][nCheckpoints - 1].numTokens = _numTokens; + } else { + tokenCheckpoints[_token][nCheckpoints] = Checkpoint( + blockNumber, + blockTimestamp, + totalWeightedStake, + _numTokens + ); + numTokenCheckpoints[_token] = nCheckpoints + 1; + } + emit CheckpointAdded(msg.sender, _token, _numTokens); + } + + /** + * Queries the total weighted stake and the weighted stake of vesting contracts and returns the difference + * @param blockNumber the blocknumber + * @param timestamp the timestamp + */ + function _getVoluntaryWeightedStake(uint32 blockNumber, uint256 timestamp) + internal + view + returns (uint96 totalWeightedStake) + { + uint96 vestingWeightedStake = staking.getPriorVestingWeightedStake(blockNumber, timestamp); + totalWeightedStake = staking.getPriorTotalVotingPower(blockNumber, timestamp); + totalWeightedStake = sub96( + totalWeightedStake, + vestingWeightedStake, + "FeeSharingProxy::_getTotalVoluntaryWeightedStake: vested stake exceeds total stake" + ); + } + + /** + * @dev Whitelisting converter address. + * + * @param converterAddress converter address to be whitelisted. + */ + function addWhitelistedConverterAddress(address converterAddress) external onlyOwner { + require(Address.isContract(converterAddress), "Non contract address given"); + whitelistedConverterList.add(converterAddress); + emit WhitelistedConverter(msg.sender, converterAddress); + } + + /** + * @dev Removing converter address from whitelist. + * + * @param converterAddress converter address to be removed from whitelist. + */ + function removeWhitelistedConverterAddress(address converterAddress) external onlyOwner { + whitelistedConverterList.remove(converterAddress); + emit UnwhitelistedConverter(msg.sender, converterAddress); + } + + /** + * @notice Getter to query all of the whitelisted converter. + * @return All of the whitelisted converter list. + */ + function getWhitelistedConverterList() external view returns (address[] memory converterList) { + converterList = whitelistedConverterList.enumerate(); + } + + /** + * @dev validate array of given address whether is whitelisted or not. + * @dev if one of them is not whitelisted, then revert. + * + * @param converterAddresses array of converter addresses. + */ + function _validateWhitelistedConverter(address[] memory converterAddresses) private view { + for (uint256 i = 0; i < converterAddresses.length; i++) { + require(whitelistedConverterList.contains(converterAddresses[i]), "Invalid Converter"); + } + } + + function withdrawWRBTC(address receiver, uint256 wrbtcAmount) external onlyOwner { + address wRBTCAddress = protocol.wrbtcToken(); + require( + wRBTCAddress != address(0), + "FeeSharingProxy::withdrawFees: wRBTCAddress is not set" + ); + + uint256 balance = IERC20(wRBTCAddress).balanceOf(address(this)); + require(wrbtcAmount <= balance, "Insufficient balance"); + + IERC20(wRBTCAddress).safeTransfer(receiver, wrbtcAmount); + } } /* Interfaces */ interface ILoanToken { - function mint(address receiver, uint256 depositAmount) external returns (uint256 mintAmount); + function mint(address receiver, uint256 depositAmount) external returns (uint256 mintAmount); } interface ILoanTokenWRBTC { - function burnToBTC( - address receiver, - uint256 burnAmount, - bool useLM - ) external returns (uint256 loanAmountPaid); + function burnToBTC( + address receiver, + uint256 burnAmount, + bool useLM + ) external returns (uint256 loanAmountPaid); } diff --git a/contracts/governance/FeeSharingProxy/FeeSharingProxy.sol b/contracts/governance/FeeSharingProxy/FeeSharingProxy.sol index c79b968cc..85e27d0a1 100644 --- a/contracts/governance/FeeSharingProxy/FeeSharingProxy.sol +++ b/contracts/governance/FeeSharingProxy/FeeSharingProxy.sol @@ -11,13 +11,13 @@ import "../../proxy/UpgradableProxy.sol"; * the possibility of being enhanced and re-deployed. * */ contract FeeSharingProxy is FeeSharingProxyStorage, UpgradableProxy { - /** - * @notice Construct a new feeSharingProxy contract. - * @param _protocol The address of the sovryn protocol. - * @param _staking The address of the staking - */ - constructor(IProtocol _protocol, IStaking _staking) public { - protocol = _protocol; - staking = _staking; - } + /** + * @notice Construct a new feeSharingProxy contract. + * @param _protocol The address of the sovryn protocol. + * @param _staking The address of the staking + */ + constructor(IProtocol _protocol, IStaking _staking) public { + protocol = _protocol; + staking = _staking; + } } diff --git a/contracts/governance/FeeSharingProxy/FeeSharingProxyStorage.sol b/contracts/governance/FeeSharingProxy/FeeSharingProxyStorage.sol index 100eb9567..f54bccece 100644 --- a/contracts/governance/FeeSharingProxy/FeeSharingProxyStorage.sol +++ b/contracts/governance/FeeSharingProxy/FeeSharingProxyStorage.sol @@ -15,95 +15,97 @@ import "../../mixins/EnumerableAddressSet.sol"; * * */ contract FeeSharingProxyStorage is Ownable { - using EnumerableAddressSet for EnumerableAddressSet.AddressSet; - /// @dev TODO FEE_WITHDRAWAL_INTERVAL, MAX_CHECKPOINTS - uint256 constant FEE_WITHDRAWAL_INTERVAL = 86400; - - uint32 constant MAX_CHECKPOINTS = 100; - - IProtocol public protocol; - IStaking public staking; - - /// @notice Checkpoints by index per pool token address - mapping(address => mapping(uint256 => Checkpoint)) public tokenCheckpoints; - - /// @notice The number of checkpoints for each pool token address. - mapping(address => uint256) public numTokenCheckpoints; - - /// @notice - /// user => token => processed checkpoint - mapping(address => mapping(address => uint256)) public processedCheckpoints; - - /// @notice Last time fees were withdrawn per pool token address: - /// token => time - mapping(address => uint256) public lastFeeWithdrawalTime; - - /// @notice Amount of tokens that were transferred, but not saved in checkpoints. - /// token => amount - mapping(address => uint96) public unprocessedAmount; - - struct Checkpoint { - uint32 blockNumber; - uint32 timestamp; - uint96 totalWeightedStake; - uint96 numTokens; - } - - /** - * @dev Add extra modifier (Reentrancy) below. - * Because we cannot add any additional storage slot before this storage contract after initial deployment - */ - - /// @dev Constant for unlocked guard state - non-zero to prevent extra gas costs. - /// See: https://github.com/OpenZeppelin/openzeppelin-solidity/issues/1056 - uint256 internal constant REENTRANCY_GUARD_FREE = 1; - - /// @dev Constant for locked guard state - uint256 internal constant REENTRANCY_GUARD_LOCKED = 2; - - /** - * @dev We use a single lock for the whole contract. - */ - uint256 internal reentrancyLock = REENTRANCY_GUARD_FREE; - - /** - * @dev Additional storage for converter whitelist mechanism. - * @dev Initialization here does not works. We need to create a separate setter & getter. - * @dev Just set the visibility to internal should be fine. - */ - EnumerableAddressSet.AddressSet internal whitelistedConverterList; - - /** - * @dev Prevents a contract from calling itself, directly or indirectly. - * If you mark a function `nonReentrant`, you should also - * mark it `external`. Calling one `nonReentrant` function from - * another is not supported. Instead, you can implement a - * `private` function doing the actual work, and an `external` - * wrapper marked as `nonReentrant`. - */ - modifier nonReentrant() { - require(reentrancyLock == REENTRANCY_GUARD_FREE, "nonReentrant"); - reentrancyLock = REENTRANCY_GUARD_LOCKED; - _; - reentrancyLock = REENTRANCY_GUARD_FREE; - } + using EnumerableAddressSet for EnumerableAddressSet.AddressSet; + /// @dev TODO FEE_WITHDRAWAL_INTERVAL, MAX_CHECKPOINTS + uint256 constant FEE_WITHDRAWAL_INTERVAL = 86400; + + uint32 constant MAX_CHECKPOINTS = 100; + + IProtocol public protocol; + IStaking public staking; + + /// @notice Checkpoints by index per pool token address + mapping(address => mapping(uint256 => Checkpoint)) public tokenCheckpoints; + + /// @notice The number of checkpoints for each pool token address. + mapping(address => uint256) public numTokenCheckpoints; + + /// @notice + /// user => token => processed checkpoint + mapping(address => mapping(address => uint256)) public processedCheckpoints; + + /// @notice Last time fees were withdrawn per pool token address: + /// token => time + mapping(address => uint256) public lastFeeWithdrawalTime; + + /// @notice Amount of tokens that were transferred, but not saved in checkpoints. + /// token => amount + mapping(address => uint96) public unprocessedAmount; + + struct Checkpoint { + uint32 blockNumber; + uint32 timestamp; + uint96 totalWeightedStake; + uint96 numTokens; + } + + /** + * @dev Add extra modifier (Reentrancy) below. + * Because we cannot add any additional storage slot before this storage contract after initial deployment + */ + + /// @dev Constant for unlocked guard state - non-zero to prevent extra gas costs. + /// See: https://github.com/OpenZeppelin/openzeppelin-solidity/issues/1056 + uint256 internal constant REENTRANCY_GUARD_FREE = 1; + + /// @dev Constant for locked guard state + uint256 internal constant REENTRANCY_GUARD_LOCKED = 2; + + /** + * @dev We use a single lock for the whole contract. + */ + uint256 internal reentrancyLock = REENTRANCY_GUARD_FREE; + + /** + * @dev Additional storage for converter whitelist mechanism. + * @dev Initialization here does not works. We need to create a separate setter & getter. + * @dev Just set the visibility to internal should be fine. + */ + EnumerableAddressSet.AddressSet internal whitelistedConverterList; + + /** + * @dev Prevents a contract from calling itself, directly or indirectly. + * If you mark a function `nonReentrant`, you should also + * mark it `external`. Calling one `nonReentrant` function from + * another is not supported. Instead, you can implement a + * `private` function doing the actual work, and an `external` + * wrapper marked as `nonReentrant`. + */ + modifier nonReentrant() { + require(reentrancyLock == REENTRANCY_GUARD_FREE, "nonReentrant"); + reentrancyLock = REENTRANCY_GUARD_LOCKED; + _; + reentrancyLock = REENTRANCY_GUARD_FREE; + } } /* Interfaces */ interface IProtocol { - /** - * - * @param tokens The array address of the token instance. - * @param receiver The address of the withdrawal recipient. - * - * @return The withdrawn total amount in wRBTC - * */ - function withdrawFees(address[] calldata tokens, address receiver) external returns (uint256 totalWRBTCWithdrawn); - - function underlyingToLoanPool(address token) external returns (address); - - function wrbtcToken() external returns (address); - - function getSovTokenAddress() external view returns (address); + /** + * + * @param tokens The array address of the token instance. + * @param receiver The address of the withdrawal recipient. + * + * @return The withdrawn total amount in wRBTC + * */ + function withdrawFees(address[] calldata tokens, address receiver) + external + returns (uint256 totalWRBTCWithdrawn); + + function underlyingToLoanPool(address token) external returns (address); + + function wrbtcToken() external returns (address); + + function getSovTokenAddress() external view returns (address); } diff --git a/contracts/governance/GovernorAlpha.sol b/contracts/governance/GovernorAlpha.sol index 10587d0cd..864409e30 100644 --- a/contracts/governance/GovernorAlpha.sol +++ b/contracts/governance/GovernorAlpha.sol @@ -24,611 +24,719 @@ import "../rsk/RSKAddrValidator.sol"; * vote for or against any SIP in bitocracy.sovryn.app. * */ contract GovernorAlpha is SafeMath96 { - /* Storage */ - - /// @notice The name of this contract. - string public constant NAME = "Sovryn Governor Alpha"; - - /// @notice The maximum number of actions that can be included in a proposal. - function proposalMaxOperations() public pure returns (uint256) { - return 10; - } // 10 actions - - /// @notice The delay before voting on a proposal may take place, once proposed. - function votingDelay() public pure returns (uint256) { - return 1; - } // 1 block - - /// @notice The duration of voting on a proposal, in blocks. - function votingPeriod() public pure returns (uint256) { - return 2880; - } // ~1 day in blocks (assuming 30s blocks) - - /// @notice The address of the Sovryn Protocol Timelock. - ITimelock public timelock; - - /// @notice The address of the Sovryn staking contract. - IStaking public staking; - - /// @notice The address of the Governor Guardian. - address public guardian; - - /// @notice The total number of proposals. - uint256 public proposalCount; - - /// @notice Percentage of current total voting power require to vote. - uint96 public quorumPercentageVotes; - - // @notice Majority percentage. - uint96 public majorityPercentageVotes; - - struct Proposal { - /// @notice Unique id for looking up a proposal. - uint256 id; - /// @notice The block at which voting begins: holders must delegate their votes prior to this block. - uint32 startBlock; - /// @notice The block at which voting ends: votes must be cast prior to this block. - uint32 endBlock; - /// @notice Current number of votes in favor of this proposal. - uint96 forVotes; - /// @notice Current number of votes in opposition to this proposal. - uint96 againstVotes; - ///@notice the quorum required for this proposal. - uint96 quorum; - ///@notice the majority percentage required for this proposal. - uint96 majorityPercentage; - /// @notice The timestamp that the proposal will be available for execution, set once the vote succeeds. - uint64 eta; - /// @notice the start time is required for the staking contract. - uint64 startTime; - /// @notice Flag marking whether the proposal has been canceled. - bool canceled; - /// @notice Flag marking whether the proposal has been executed. - bool executed; - /// @notice Creator of the proposal. - address proposer; - /// @notice the ordered list of target addresses for calls to be made. - address[] targets; - /// @notice The ordered list of values (i.e. msg.value) to be passed to the calls to be made. - uint256[] values; - /// @notice The ordered list of function signatures to be called. - string[] signatures; - /// @notice The ordered list of calldata to be passed to each call. - bytes[] calldatas; - /// @notice Receipts of ballots for the entire set of voters. - mapping(address => Receipt) receipts; - } - - /// @notice Ballot receipt record for a voter - struct Receipt { - /// @notice Whether or not a vote has been cast. - bool hasVoted; - /// @notice Whether or not the voter supports the proposal. - bool support; - /// @notice The number of votes the voter had, which were cast. - uint96 votes; - } - - /// @notice Possible states that a proposal may be in. - enum ProposalState { Pending, Active, Canceled, Defeated, Succeeded, Queued, Expired, Executed } - - /// @notice The official record of all proposals ever proposed. - mapping(uint256 => Proposal) public proposals; - - /// @notice The latest proposal for each proposer. - mapping(address => uint256) public latestProposalIds; - - /// @notice The EIP-712 typehash for the contract's domain. - bytes32 public constant DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"); - - /// @notice The EIP-712 typehash for the ballot struct used by the contract. - bytes32 public constant BALLOT_TYPEHASH = keccak256("Ballot(uint256 proposalId,bool support)"); - - /* Events */ - - /// @notice An event emitted when a new proposal is created. - event ProposalCreated( - uint256 id, - address proposer, - address[] targets, - uint256[] values, - string[] signatures, - bytes[] calldatas, - uint256 startBlock, - uint256 endBlock, - string description - ); - - /// @notice An event emitted when a vote has been cast on a proposal. - event VoteCast(address voter, uint256 proposalId, bool support, uint256 votes); - - /// @notice An event emitted when a proposal has been canceled. - event ProposalCanceled(uint256 id); - - /// @notice An event emitted when a proposal has been queued in the Timelock. - event ProposalQueued(uint256 id, uint256 eta); - - /// @notice An event emitted when a proposal has been executed in the Timelock. - event ProposalExecuted(uint256 id); - - /* Functions */ - - constructor( - address timelock_, - address staking_, - address guardian_, - uint96 _quorumPercentageVotes, - uint96 _majorityPercentageVotes - ) public { - timelock = ITimelock(timelock_); - staking = IStaking(staking_); - guardian = guardian_; - quorumPercentageVotes = _quorumPercentageVotes; - majorityPercentageVotes = _majorityPercentageVotes; - } - - /// @notice The number of votes required in order for a voter to become a proposer. - function proposalThreshold() public view returns (uint96) { - uint96 totalVotingPower = - staking.getPriorTotalVotingPower( - safe32(block.number - 1, "GovernorAlpha::proposalThreshold: block number overflow"), - block.timestamp - ); - // 1% of current total voting power. - return totalVotingPower / 100; - } - - /// @notice The number of votes in support of a proposal required in order for a quorum to be reached and for a vote to succeed. - function quorumVotes() public view returns (uint96) { - uint96 totalVotingPower = - staking.getPriorTotalVotingPower( - safe32(block.number - 1, "GovernorAlpha::quorumVotes: block number overflow"), - block.timestamp - ); - // 4% of current total voting power. - return mul96(quorumPercentageVotes, totalVotingPower, "GovernorAlpha::quorumVotes:multiplication overflow") / 100; - } - - /** - * @notice Create a new proposal. - * @param targets Array of contract addresses to perform proposal execution. - * @param values Array of rBTC amounts to send on proposal execution. - * @param signatures Array of function signatures to call on proposal execution. - * @param calldatas Array of payloads for the calls on proposal execution. - * @param description Text describing the purpose of the proposal. - * */ - function propose( - address[] memory targets, - uint256[] memory values, - string[] memory signatures, - bytes[] memory calldatas, - string memory description - ) public returns (uint256) { - // note: passing this block's timestamp, but the number of the previous block. - // todo: think if it would be better to pass block.timestamp - 30 (average block time) - // (probably not because proposal starts in 1 block from now). - uint96 threshold = proposalThreshold(); - require( - staking.getPriorVotes(msg.sender, sub256(block.number, 1), block.timestamp) > threshold, - "GovernorAlpha::propose: proposer votes below proposal threshold" - ); - require( - targets.length == values.length && targets.length == signatures.length && targets.length == calldatas.length, - "GovernorAlpha::propose: proposal function information arity mismatch" - ); - require(targets.length != 0, "GovernorAlpha::propose: must provide actions"); - require(targets.length <= proposalMaxOperations(), "GovernorAlpha::propose: too many actions"); - - uint256 latestProposalId = latestProposalIds[msg.sender]; - if (latestProposalId != 0) { - ProposalState proposersLatestProposalState = state(latestProposalId); - require( - proposersLatestProposalState != ProposalState.Active, - "GovernorAlpha::propose: one live proposal per proposer, found an already active proposal" - ); - require( - proposersLatestProposalState != ProposalState.Pending, - "GovernorAlpha::propose: one live proposal per proposer, found an already pending proposal" - ); - } - - uint256 startBlock = add256(block.number, votingDelay()); - uint256 endBlock = add256(startBlock, votingPeriod()); - - proposalCount++; - - /// @dev quorum: proposalThreshold is 1% of total votes, we can save gas using this pre calculated value. - /// @dev startTime: Required by the staking contract. not used by the governance contract itself. - Proposal memory newProposal = - Proposal({ - id: proposalCount, - startBlock: safe32(startBlock, "GovernorAlpha::propose: start block number overflow"), - endBlock: safe32(endBlock, "GovernorAlpha::propose: end block number overflow"), - forVotes: 0, - againstVotes: 0, - quorum: mul96(quorumPercentageVotes, threshold, "GovernorAlpha::propose: overflow on quorum computation"), - majorityPercentage: mul96( - majorityPercentageVotes, - threshold, - "GovernorAlpha::propose: overflow on majorityPercentage computation" - ), - eta: 0, - startTime: safe64(block.timestamp, "GovernorAlpha::propose: startTime overflow"), - canceled: false, - executed: false, - proposer: msg.sender, - targets: targets, - values: values, - signatures: signatures, - calldatas: calldatas - }); - - proposals[newProposal.id] = newProposal; - latestProposalIds[newProposal.proposer] = newProposal.id; - - emit ProposalCreated(newProposal.id, msg.sender, targets, values, signatures, calldatas, startBlock, endBlock, description); - return newProposal.id; - } - - /** - * @notice Enqueue a proposal and everyone of its calls. - * @param proposalId Proposal index to access the list proposals[] from storage. - * */ - function queue(uint256 proposalId) public { - require(state(proposalId) == ProposalState.Succeeded, "GovernorAlpha::queue: proposal can only be queued if it is succeeded"); - Proposal storage proposal = proposals[proposalId]; - uint256 eta = add256(block.timestamp, timelock.delay()); - - for (uint256 i = 0; i < proposal.targets.length; i++) { - _queueOrRevert(proposal.targets[i], proposal.values[i], proposal.signatures[i], proposal.calldatas[i], eta); - } - proposal.eta = safe64(eta, "GovernorAlpha::queue: ETA overflow"); - emit ProposalQueued(proposalId, eta); - } - - /** - * @notice Tries to enqueue a proposal, verifying it has not been previously queued. - * @param target Contract addresses to perform proposal execution. - * @param value rBTC amount to send on proposal execution. - * @param signature Function signature to call on proposal execution. - * @param data Payload for the call on proposal execution. - * @param eta Estimated Time of Accomplishment. The timestamp that the - * proposal will be available for execution, set once the vote succeeds. - * */ - function _queueOrRevert( - address target, - uint256 value, - string memory signature, - bytes memory data, - uint256 eta - ) internal { - require( - !timelock.queuedTransactions(keccak256(abi.encode(target, value, signature, data, eta))), - "GovernorAlpha::_queueOrRevert: proposal action already queued at eta" - ); - timelock.queueTransaction(target, value, signature, data, eta); - } - - /** - * @notice Execute a proposal by looping and performing everyone of its calls. - * @param proposalId Proposal index to access the list proposals[] from storage. - * */ - function execute(uint256 proposalId) public payable { - require(state(proposalId) == ProposalState.Queued, "GovernorAlpha::execute: proposal can only be executed if it is queued"); - Proposal storage proposal = proposals[proposalId]; - proposal.executed = true; - - for (uint256 i = 0; i < proposal.targets.length; i++) { - timelock.executeTransaction.value(proposal.values[i])( - proposal.targets[i], - proposal.values[i], - proposal.signatures[i], - proposal.calldatas[i], - proposal.eta - ); - } - emit ProposalExecuted(proposalId); - } - - /** - * @notice Cancel a proposal by looping and cancelling everyone of its calls. - * @param proposalId Proposal index to access the list proposals[] from storage. - * */ - function cancel(uint256 proposalId) public { - ProposalState state = state(proposalId); - require(state != ProposalState.Executed, "GovernorAlpha::cancel: cannot cancel executed proposal"); - - Proposal storage proposal = proposals[proposalId]; - /// @notice Cancel only if sent by the guardian. - require(msg.sender == guardian, "GovernorAlpha::cancel: sender isn't a guardian"); - - proposal.canceled = true; - - for (uint256 i = 0; i < proposal.targets.length; i++) { - timelock.cancelTransaction( - proposal.targets[i], - proposal.values[i], - proposal.signatures[i], - proposal.calldatas[i], - proposal.eta - ); - } - - emit ProposalCanceled(proposalId); - } - - /** - * @notice Get a proposal list of its calls. - * @param proposalId Proposal index to access the list proposals[] from storage. - * @return Arrays of the 4 call parameters: targets, values, signatures, calldatas. - * */ - function getActions(uint256 proposalId) - public - view - returns ( - address[] memory targets, - uint256[] memory values, - string[] memory signatures, - bytes[] memory calldatas - ) - { - Proposal storage p = proposals[proposalId]; - return (p.targets, p.values, p.signatures, p.calldatas); - } - - /** - * @notice Get a proposal receipt. - * @param proposalId Proposal index to access the list proposals[] from storage. - * @param voter A governance stakeholder with voting power. - * @return The voter receipt of the proposal. - * */ - function getReceipt(uint256 proposalId, address voter) public view returns (Receipt memory) { - return proposals[proposalId].receipts[voter]; - } - - /** - * @notice Casts a vote by sender. - * @param proposalId Proposal index to access the list proposals[] from storage. - * @param support Vote value, yes or no. - * */ - function castVote(uint256 proposalId, bool support) public { - return _castVote(msg.sender, proposalId, support); - } - - /** - * @notice Voting with EIP-712 Signatures. - * - * Voting power can be delegated to any address, and then can be used to - * vote on proposals. A key benefit to users of by-signature functionality - * is that they can create a signed vote transaction for free, and have a - * trusted third-party spend rBTC(or ETH) on gas fees and write it to the - * blockchain for them. - * - * The third party in this scenario, submitting the SOV-holder’s signed - * transaction holds a voting power that is for only a single proposal. - * The signatory still holds the power to vote on their own behalf in - * the proposal if the third party has not yet published the signed - * transaction that was given to them. - * - * @dev The signature needs to be broken up into 3 parameters, known as - * v, r and s: - * const r = '0x' + sig.substring(2).substring(0, 64); - * const s = '0x' + sig.substring(2).substring(64, 128); - * const v = '0x' + sig.substring(2).substring(128, 130); - * - * @param proposalId Proposal index to access the list proposals[] from storage. - * @param support Vote value, yes or no. - * @param v The recovery byte of the signature. - * @param r Half of the ECDSA signature pair. - * @param s Half of the ECDSA signature pair. - * */ - function castVoteBySig( - uint256 proposalId, - bool support, - uint8 v, - bytes32 r, - bytes32 s - ) public { - /** - * @dev The DOMAIN_SEPARATOR is a hash that uniquely identifies a - * smart contract. It is built from a string denoting it as an - * EIP712 Domain, the name of the token contract, the version, - * the chainId in case it changes, and the address that the - * contract is deployed at. - * */ - bytes32 domainSeparator = keccak256(abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(NAME)), getChainId(), address(this))); - - /// @dev GovernorAlpha uses BALLOT_TYPEHASH, while Staking uses DELEGATION_TYPEHASH - bytes32 structHash = keccak256(abi.encode(BALLOT_TYPEHASH, proposalId, support)); - - bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); - address signatory = ecrecover(digest, v, r, s); - - /// @dev Verify address is not null and PK is not null either. - require(RSKAddrValidator.checkPKNotZero(signatory), "GovernorAlpha::castVoteBySig: invalid signature"); - return _castVote(signatory, proposalId, support); - } - - /** - * @notice Cast a vote, adding it to the total counting. - * @param voter A governance stakeholder with voting power that is casting the vote. - * @param proposalId Proposal index to access the list proposals[] from storage. - * @param support Vote value, yes or no. - * */ - function _castVote( - address voter, - uint256 proposalId, - bool support - ) internal { - require(state(proposalId) == ProposalState.Active, "GovernorAlpha::_castVote: voting is closed"); - Proposal storage proposal = proposals[proposalId]; - Receipt storage receipt = proposal.receipts[voter]; - require(receipt.hasVoted == false, "GovernorAlpha::_castVote: voter already voted"); - uint96 votes = staking.getPriorVotes(voter, proposal.startBlock, proposal.startTime); - - if (support) { - proposal.forVotes = add96(proposal.forVotes, votes, "GovernorAlpha::_castVote: vote overflow"); - } else { - proposal.againstVotes = add96(proposal.againstVotes, votes, "GovernorAlpha::_castVote: vote overflow"); - } - - receipt.hasVoted = true; - receipt.support = support; - receipt.votes = votes; - - emit VoteCast(voter, proposalId, support, votes); - } - - /// @dev Timelock wrapper w/ sender check. - function __acceptAdmin() public { - require(msg.sender == guardian, "GovernorAlpha::__acceptAdmin: sender must be gov guardian"); - timelock.acceptAdmin(); - } - - /// @notice Sets guardian address to zero. - function __abdicate() public { - require(msg.sender == guardian, "GovernorAlpha::__abdicate: sender must be gov guardian"); - guardian = address(0); - } - - /// @dev Timelock wrapper w/ sender check. - function __queueSetTimelockPendingAdmin(address newPendingAdmin, uint256 eta) public { - require(msg.sender == guardian, "GovernorAlpha::__queueSetTimelockPendingAdmin: sender must be gov guardian"); - timelock.queueTransaction(address(timelock), 0, "setPendingAdmin(address)", abi.encode(newPendingAdmin), eta); - } - - /// @dev Timelock wrapper w/ sender check. - function __executeSetTimelockPendingAdmin(address newPendingAdmin, uint256 eta) public { - require(msg.sender == guardian, "GovernorAlpha::__executeSetTimelockPendingAdmin: sender must be gov guardian"); - timelock.executeTransaction(address(timelock), 0, "setPendingAdmin(address)", abi.encode(newPendingAdmin), eta); - } - - /** - * @notice Get a proposal state. - * @param proposalId Proposal index to access the list proposals[] from storage. - * @return The state of the proposal: Canceled, Pending, Active, Defeated, - * Succeeded, Executed, Expired. - * */ - function state(uint256 proposalId) public view returns (ProposalState) { - require(proposalCount >= proposalId && proposalId > 0, "GovernorAlpha::state: invalid proposal id"); - Proposal storage proposal = proposals[proposalId]; - - if (proposal.canceled) { - return ProposalState.Canceled; - } - - if (block.number <= proposal.startBlock) { - return ProposalState.Pending; - } - - if (block.number <= proposal.endBlock) { - return ProposalState.Active; - } - - uint96 totalVotes = add96(proposal.forVotes, proposal.againstVotes, "GovernorAlpha:: state: forVotes + againstVotes > uint96"); - uint96 totalVotesMajorityPercentage = div96(totalVotes, 100, "GovernorAlpha:: state: division error"); - totalVotesMajorityPercentage = mul96( - totalVotesMajorityPercentage, - majorityPercentageVotes, - "GovernorAlpha:: state: totalVotes * majorityPercentage > uint96" - ); - if (proposal.forVotes <= totalVotesMajorityPercentage || totalVotes < proposal.quorum) { - return ProposalState.Defeated; - } - - if (proposal.eta == 0) { - return ProposalState.Succeeded; - } - - if (proposal.executed) { - return ProposalState.Executed; - } - - if (block.timestamp >= add256(proposal.eta, timelock.GRACE_PERIOD())) { - return ProposalState.Expired; - } - - return ProposalState.Queued; - } - - /// @dev TODO: use OpenZeppelin's SafeMath function instead. - function add256(uint256 a, uint256 b) internal pure returns (uint256) { - uint256 c = a + b; - require(c >= a, "addition overflow"); - return c; - } - - /// @dev TODO: use OpenZeppelin's SafeMath function instead. - function sub256(uint256 a, uint256 b) internal pure returns (uint256) { - require(b <= a, "subtraction underflow"); - return a - b; - } - - /** - * @notice Retrieve CHAIN_ID of the executing chain. - * - * Chain identifier (chainID) introduced in EIP-155 protects transaction - * included into one chain from being included into another chain. - * Basically, chain identifier is an integer number being used in the - * processes of signing transactions and verifying transaction signatures. - * - * @dev As of version 0.5.12, Solidity includes an assembly function - * chainid() that provides access to the new CHAINID opcode. - * - * TODO: chainId is included in block. So you can get chain id like - * block timestamp or block number: block.chainid; - * */ - function getChainId() internal pure returns (uint256) { - uint256 chainId; - assembly { - chainId := chainid() - } - return chainId; - } + /* Storage */ + + /// @notice The name of this contract. + string public constant NAME = "Sovryn Governor Alpha"; + + /// @notice The maximum number of actions that can be included in a proposal. + function proposalMaxOperations() public pure returns (uint256) { + return 10; + } // 10 actions + + /// @notice The delay before voting on a proposal may take place, once proposed. + function votingDelay() public pure returns (uint256) { + return 1; + } // 1 block + + /// @notice The duration of voting on a proposal, in blocks. + function votingPeriod() public pure returns (uint256) { + return 2880; + } // ~1 day in blocks (assuming 30s blocks) + + /// @notice The address of the Sovryn Protocol Timelock. + ITimelock public timelock; + + /// @notice The address of the Sovryn staking contract. + IStaking public staking; + + /// @notice The address of the Governor Guardian. + address public guardian; + + /// @notice The total number of proposals. + uint256 public proposalCount; + + /// @notice Percentage of current total voting power require to vote. + uint96 public quorumPercentageVotes; + + // @notice Majority percentage. + uint96 public majorityPercentageVotes; + + struct Proposal { + /// @notice Unique id for looking up a proposal. + uint256 id; + /// @notice The block at which voting begins: holders must delegate their votes prior to this block. + uint32 startBlock; + /// @notice The block at which voting ends: votes must be cast prior to this block. + uint32 endBlock; + /// @notice Current number of votes in favor of this proposal. + uint96 forVotes; + /// @notice Current number of votes in opposition to this proposal. + uint96 againstVotes; + ///@notice the quorum required for this proposal. + uint96 quorum; + ///@notice the majority percentage required for this proposal. + uint96 majorityPercentage; + /// @notice The timestamp that the proposal will be available for execution, set once the vote succeeds. + uint64 eta; + /// @notice the start time is required for the staking contract. + uint64 startTime; + /// @notice Flag marking whether the proposal has been canceled. + bool canceled; + /// @notice Flag marking whether the proposal has been executed. + bool executed; + /// @notice Creator of the proposal. + address proposer; + /// @notice the ordered list of target addresses for calls to be made. + address[] targets; + /// @notice The ordered list of values (i.e. msg.value) to be passed to the calls to be made. + uint256[] values; + /// @notice The ordered list of function signatures to be called. + string[] signatures; + /// @notice The ordered list of calldata to be passed to each call. + bytes[] calldatas; + /// @notice Receipts of ballots for the entire set of voters. + mapping(address => Receipt) receipts; + } + + /// @notice Ballot receipt record for a voter + struct Receipt { + /// @notice Whether or not a vote has been cast. + bool hasVoted; + /// @notice Whether or not the voter supports the proposal. + bool support; + /// @notice The number of votes the voter had, which were cast. + uint96 votes; + } + + /// @notice Possible states that a proposal may be in. + enum ProposalState { + Pending, + Active, + Canceled, + Defeated, + Succeeded, + Queued, + Expired, + Executed + } + + /// @notice The official record of all proposals ever proposed. + mapping(uint256 => Proposal) public proposals; + + /// @notice The latest proposal for each proposer. + mapping(address => uint256) public latestProposalIds; + + /// @notice The EIP-712 typehash for the contract's domain. + bytes32 public constant DOMAIN_TYPEHASH = + keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"); + + /// @notice The EIP-712 typehash for the ballot struct used by the contract. + bytes32 public constant BALLOT_TYPEHASH = keccak256("Ballot(uint256 proposalId,bool support)"); + + /* Events */ + + /// @notice An event emitted when a new proposal is created. + event ProposalCreated( + uint256 id, + address proposer, + address[] targets, + uint256[] values, + string[] signatures, + bytes[] calldatas, + uint256 startBlock, + uint256 endBlock, + string description + ); + + /// @notice An event emitted when a vote has been cast on a proposal. + event VoteCast(address voter, uint256 proposalId, bool support, uint256 votes); + + /// @notice An event emitted when a proposal has been canceled. + event ProposalCanceled(uint256 id); + + /// @notice An event emitted when a proposal has been queued in the Timelock. + event ProposalQueued(uint256 id, uint256 eta); + + /// @notice An event emitted when a proposal has been executed in the Timelock. + event ProposalExecuted(uint256 id); + + /* Functions */ + + constructor( + address timelock_, + address staking_, + address guardian_, + uint96 _quorumPercentageVotes, + uint96 _majorityPercentageVotes + ) public { + timelock = ITimelock(timelock_); + staking = IStaking(staking_); + guardian = guardian_; + quorumPercentageVotes = _quorumPercentageVotes; + majorityPercentageVotes = _majorityPercentageVotes; + } + + /// @notice The number of votes required in order for a voter to become a proposer. + function proposalThreshold() public view returns (uint96) { + uint96 totalVotingPower = + staking.getPriorTotalVotingPower( + safe32( + block.number - 1, + "GovernorAlpha::proposalThreshold: block number overflow" + ), + block.timestamp + ); + // 1% of current total voting power. + return totalVotingPower / 100; + } + + /// @notice The number of votes in support of a proposal required in order for a quorum to be reached and for a vote to succeed. + function quorumVotes() public view returns (uint96) { + uint96 totalVotingPower = + staking.getPriorTotalVotingPower( + safe32(block.number - 1, "GovernorAlpha::quorumVotes: block number overflow"), + block.timestamp + ); + // 4% of current total voting power. + return + mul96( + quorumPercentageVotes, + totalVotingPower, + "GovernorAlpha::quorumVotes:multiplication overflow" + ) / 100; + } + + /** + * @notice Create a new proposal. + * @param targets Array of contract addresses to perform proposal execution. + * @param values Array of rBTC amounts to send on proposal execution. + * @param signatures Array of function signatures to call on proposal execution. + * @param calldatas Array of payloads for the calls on proposal execution. + * @param description Text describing the purpose of the proposal. + * */ + function propose( + address[] memory targets, + uint256[] memory values, + string[] memory signatures, + bytes[] memory calldatas, + string memory description + ) public returns (uint256) { + // note: passing this block's timestamp, but the number of the previous block. + // todo: think if it would be better to pass block.timestamp - 30 (average block time) + // (probably not because proposal starts in 1 block from now). + uint96 threshold = proposalThreshold(); + require( + staking.getPriorVotes(msg.sender, sub256(block.number, 1), block.timestamp) > + threshold, + "GovernorAlpha::propose: proposer votes below proposal threshold" + ); + require( + targets.length == values.length && + targets.length == signatures.length && + targets.length == calldatas.length, + "GovernorAlpha::propose: proposal function information arity mismatch" + ); + require(targets.length != 0, "GovernorAlpha::propose: must provide actions"); + require( + targets.length <= proposalMaxOperations(), + "GovernorAlpha::propose: too many actions" + ); + + uint256 latestProposalId = latestProposalIds[msg.sender]; + if (latestProposalId != 0) { + ProposalState proposersLatestProposalState = state(latestProposalId); + require( + proposersLatestProposalState != ProposalState.Active, + "GovernorAlpha::propose: one live proposal per proposer, found an already active proposal" + ); + require( + proposersLatestProposalState != ProposalState.Pending, + "GovernorAlpha::propose: one live proposal per proposer, found an already pending proposal" + ); + } + + uint256 startBlock = add256(block.number, votingDelay()); + uint256 endBlock = add256(startBlock, votingPeriod()); + + proposalCount++; + + /// @dev quorum: proposalThreshold is 1% of total votes, we can save gas using this pre calculated value. + /// @dev startTime: Required by the staking contract. not used by the governance contract itself. + Proposal memory newProposal = + Proposal({ + id: proposalCount, + startBlock: safe32( + startBlock, + "GovernorAlpha::propose: start block number overflow" + ), + endBlock: safe32(endBlock, "GovernorAlpha::propose: end block number overflow"), + forVotes: 0, + againstVotes: 0, + quorum: mul96( + quorumPercentageVotes, + threshold, + "GovernorAlpha::propose: overflow on quorum computation" + ), + majorityPercentage: mul96( + majorityPercentageVotes, + threshold, + "GovernorAlpha::propose: overflow on majorityPercentage computation" + ), + eta: 0, + startTime: safe64(block.timestamp, "GovernorAlpha::propose: startTime overflow"), + canceled: false, + executed: false, + proposer: msg.sender, + targets: targets, + values: values, + signatures: signatures, + calldatas: calldatas + }); + + proposals[newProposal.id] = newProposal; + latestProposalIds[newProposal.proposer] = newProposal.id; + + emit ProposalCreated( + newProposal.id, + msg.sender, + targets, + values, + signatures, + calldatas, + startBlock, + endBlock, + description + ); + return newProposal.id; + } + + /** + * @notice Enqueue a proposal and everyone of its calls. + * @param proposalId Proposal index to access the list proposals[] from storage. + * */ + function queue(uint256 proposalId) public { + require( + state(proposalId) == ProposalState.Succeeded, + "GovernorAlpha::queue: proposal can only be queued if it is succeeded" + ); + Proposal storage proposal = proposals[proposalId]; + uint256 eta = add256(block.timestamp, timelock.delay()); + + for (uint256 i = 0; i < proposal.targets.length; i++) { + _queueOrRevert( + proposal.targets[i], + proposal.values[i], + proposal.signatures[i], + proposal.calldatas[i], + eta + ); + } + proposal.eta = safe64(eta, "GovernorAlpha::queue: ETA overflow"); + emit ProposalQueued(proposalId, eta); + } + + /** + * @notice Tries to enqueue a proposal, verifying it has not been previously queued. + * @param target Contract addresses to perform proposal execution. + * @param value rBTC amount to send on proposal execution. + * @param signature Function signature to call on proposal execution. + * @param data Payload for the call on proposal execution. + * @param eta Estimated Time of Accomplishment. The timestamp that the + * proposal will be available for execution, set once the vote succeeds. + * */ + function _queueOrRevert( + address target, + uint256 value, + string memory signature, + bytes memory data, + uint256 eta + ) internal { + require( + !timelock.queuedTransactions( + keccak256(abi.encode(target, value, signature, data, eta)) + ), + "GovernorAlpha::_queueOrRevert: proposal action already queued at eta" + ); + timelock.queueTransaction(target, value, signature, data, eta); + } + + /** + * @notice Execute a proposal by looping and performing everyone of its calls. + * @param proposalId Proposal index to access the list proposals[] from storage. + * */ + function execute(uint256 proposalId) public payable { + require( + state(proposalId) == ProposalState.Queued, + "GovernorAlpha::execute: proposal can only be executed if it is queued" + ); + Proposal storage proposal = proposals[proposalId]; + proposal.executed = true; + + for (uint256 i = 0; i < proposal.targets.length; i++) { + timelock.executeTransaction.value(proposal.values[i])( + proposal.targets[i], + proposal.values[i], + proposal.signatures[i], + proposal.calldatas[i], + proposal.eta + ); + } + emit ProposalExecuted(proposalId); + } + + /** + * @notice Cancel a proposal by looping and cancelling everyone of its calls. + * @param proposalId Proposal index to access the list proposals[] from storage. + * */ + function cancel(uint256 proposalId) public { + ProposalState state = state(proposalId); + require( + state != ProposalState.Executed, + "GovernorAlpha::cancel: cannot cancel executed proposal" + ); + + Proposal storage proposal = proposals[proposalId]; + /// @notice Cancel only if sent by the guardian. + require(msg.sender == guardian, "GovernorAlpha::cancel: sender isn't a guardian"); + + proposal.canceled = true; + + for (uint256 i = 0; i < proposal.targets.length; i++) { + timelock.cancelTransaction( + proposal.targets[i], + proposal.values[i], + proposal.signatures[i], + proposal.calldatas[i], + proposal.eta + ); + } + + emit ProposalCanceled(proposalId); + } + + /** + * @notice Get a proposal list of its calls. + * @param proposalId Proposal index to access the list proposals[] from storage. + * @return Arrays of the 4 call parameters: targets, values, signatures, calldatas. + * */ + function getActions(uint256 proposalId) + public + view + returns ( + address[] memory targets, + uint256[] memory values, + string[] memory signatures, + bytes[] memory calldatas + ) + { + Proposal storage p = proposals[proposalId]; + return (p.targets, p.values, p.signatures, p.calldatas); + } + + /** + * @notice Get a proposal receipt. + * @param proposalId Proposal index to access the list proposals[] from storage. + * @param voter A governance stakeholder with voting power. + * @return The voter receipt of the proposal. + * */ + function getReceipt(uint256 proposalId, address voter) public view returns (Receipt memory) { + return proposals[proposalId].receipts[voter]; + } + + /** + * @notice Casts a vote by sender. + * @param proposalId Proposal index to access the list proposals[] from storage. + * @param support Vote value, yes or no. + * */ + function castVote(uint256 proposalId, bool support) public { + return _castVote(msg.sender, proposalId, support); + } + + /** + * @notice Voting with EIP-712 Signatures. + * + * Voting power can be delegated to any address, and then can be used to + * vote on proposals. A key benefit to users of by-signature functionality + * is that they can create a signed vote transaction for free, and have a + * trusted third-party spend rBTC(or ETH) on gas fees and write it to the + * blockchain for them. + * + * The third party in this scenario, submitting the SOV-holder’s signed + * transaction holds a voting power that is for only a single proposal. + * The signatory still holds the power to vote on their own behalf in + * the proposal if the third party has not yet published the signed + * transaction that was given to them. + * + * @dev The signature needs to be broken up into 3 parameters, known as + * v, r and s: + * const r = '0x' + sig.substring(2).substring(0, 64); + * const s = '0x' + sig.substring(2).substring(64, 128); + * const v = '0x' + sig.substring(2).substring(128, 130); + * + * @param proposalId Proposal index to access the list proposals[] from storage. + * @param support Vote value, yes or no. + * @param v The recovery byte of the signature. + * @param r Half of the ECDSA signature pair. + * @param s Half of the ECDSA signature pair. + * */ + function castVoteBySig( + uint256 proposalId, + bool support, + uint8 v, + bytes32 r, + bytes32 s + ) public { + /** + * @dev The DOMAIN_SEPARATOR is a hash that uniquely identifies a + * smart contract. It is built from a string denoting it as an + * EIP712 Domain, the name of the token contract, the version, + * the chainId in case it changes, and the address that the + * contract is deployed at. + * */ + bytes32 domainSeparator = + keccak256( + abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(NAME)), getChainId(), address(this)) + ); + + /// @dev GovernorAlpha uses BALLOT_TYPEHASH, while Staking uses DELEGATION_TYPEHASH + bytes32 structHash = keccak256(abi.encode(BALLOT_TYPEHASH, proposalId, support)); + + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + address signatory = ecrecover(digest, v, r, s); + + /// @dev Verify address is not null and PK is not null either. + require( + RSKAddrValidator.checkPKNotZero(signatory), + "GovernorAlpha::castVoteBySig: invalid signature" + ); + return _castVote(signatory, proposalId, support); + } + + /** + * @notice Cast a vote, adding it to the total counting. + * @param voter A governance stakeholder with voting power that is casting the vote. + * @param proposalId Proposal index to access the list proposals[] from storage. + * @param support Vote value, yes or no. + * */ + function _castVote( + address voter, + uint256 proposalId, + bool support + ) internal { + require( + state(proposalId) == ProposalState.Active, + "GovernorAlpha::_castVote: voting is closed" + ); + Proposal storage proposal = proposals[proposalId]; + Receipt storage receipt = proposal.receipts[voter]; + require(receipt.hasVoted == false, "GovernorAlpha::_castVote: voter already voted"); + uint96 votes = staking.getPriorVotes(voter, proposal.startBlock, proposal.startTime); + + if (support) { + proposal.forVotes = add96( + proposal.forVotes, + votes, + "GovernorAlpha::_castVote: vote overflow" + ); + } else { + proposal.againstVotes = add96( + proposal.againstVotes, + votes, + "GovernorAlpha::_castVote: vote overflow" + ); + } + + receipt.hasVoted = true; + receipt.support = support; + receipt.votes = votes; + + emit VoteCast(voter, proposalId, support, votes); + } + + /// @dev Timelock wrapper w/ sender check. + function __acceptAdmin() public { + require( + msg.sender == guardian, + "GovernorAlpha::__acceptAdmin: sender must be gov guardian" + ); + timelock.acceptAdmin(); + } + + /// @notice Sets guardian address to zero. + function __abdicate() public { + require(msg.sender == guardian, "GovernorAlpha::__abdicate: sender must be gov guardian"); + guardian = address(0); + } + + /// @dev Timelock wrapper w/ sender check. + function __queueSetTimelockPendingAdmin(address newPendingAdmin, uint256 eta) public { + require( + msg.sender == guardian, + "GovernorAlpha::__queueSetTimelockPendingAdmin: sender must be gov guardian" + ); + timelock.queueTransaction( + address(timelock), + 0, + "setPendingAdmin(address)", + abi.encode(newPendingAdmin), + eta + ); + } + + /// @dev Timelock wrapper w/ sender check. + function __executeSetTimelockPendingAdmin(address newPendingAdmin, uint256 eta) public { + require( + msg.sender == guardian, + "GovernorAlpha::__executeSetTimelockPendingAdmin: sender must be gov guardian" + ); + timelock.executeTransaction( + address(timelock), + 0, + "setPendingAdmin(address)", + abi.encode(newPendingAdmin), + eta + ); + } + + /** + * @notice Get a proposal state. + * @param proposalId Proposal index to access the list proposals[] from storage. + * @return The state of the proposal: Canceled, Pending, Active, Defeated, + * Succeeded, Executed, Expired. + * */ + function state(uint256 proposalId) public view returns (ProposalState) { + require( + proposalCount >= proposalId && proposalId > 0, + "GovernorAlpha::state: invalid proposal id" + ); + Proposal storage proposal = proposals[proposalId]; + + if (proposal.canceled) { + return ProposalState.Canceled; + } + + if (block.number <= proposal.startBlock) { + return ProposalState.Pending; + } + + if (block.number <= proposal.endBlock) { + return ProposalState.Active; + } + + uint96 totalVotes = + add96( + proposal.forVotes, + proposal.againstVotes, + "GovernorAlpha:: state: forVotes + againstVotes > uint96" + ); + uint96 totalVotesMajorityPercentage = + div96(totalVotes, 100, "GovernorAlpha:: state: division error"); + totalVotesMajorityPercentage = mul96( + totalVotesMajorityPercentage, + majorityPercentageVotes, + "GovernorAlpha:: state: totalVotes * majorityPercentage > uint96" + ); + if (proposal.forVotes <= totalVotesMajorityPercentage || totalVotes < proposal.quorum) { + return ProposalState.Defeated; + } + + if (proposal.eta == 0) { + return ProposalState.Succeeded; + } + + if (proposal.executed) { + return ProposalState.Executed; + } + + if (block.timestamp >= add256(proposal.eta, timelock.GRACE_PERIOD())) { + return ProposalState.Expired; + } + + return ProposalState.Queued; + } + + /// @dev TODO: use OpenZeppelin's SafeMath function instead. + function add256(uint256 a, uint256 b) internal pure returns (uint256) { + uint256 c = a + b; + require(c >= a, "addition overflow"); + return c; + } + + /// @dev TODO: use OpenZeppelin's SafeMath function instead. + function sub256(uint256 a, uint256 b) internal pure returns (uint256) { + require(b <= a, "subtraction underflow"); + return a - b; + } + + /** + * @notice Retrieve CHAIN_ID of the executing chain. + * + * Chain identifier (chainID) introduced in EIP-155 protects transaction + * included into one chain from being included into another chain. + * Basically, chain identifier is an integer number being used in the + * processes of signing transactions and verifying transaction signatures. + * + * @dev As of version 0.5.12, Solidity includes an assembly function + * chainid() that provides access to the new CHAINID opcode. + * + * TODO: chainId is included in block. So you can get chain id like + * block timestamp or block number: block.chainid; + * */ + function getChainId() internal pure returns (uint256) { + uint256 chainId; + assembly { + chainId := chainid() + } + return chainId; + } } /* Interfaces */ interface TimelockInterface { - function delay() external view returns (uint256); - - function GRACE_PERIOD() external view returns (uint256); - - function acceptAdmin() external; - - function queuedTransactions(bytes32 hash) external view returns (bool); - - function queueTransaction( - address target, - uint256 value, - string calldata signature, - bytes calldata data, - uint256 eta - ) external returns (bytes32); - - function cancelTransaction( - address target, - uint256 value, - string calldata signature, - bytes calldata data, - uint256 eta - ) external; - - function executeTransaction( - address target, - uint256 value, - string calldata signature, - bytes calldata data, - uint256 eta - ) external payable returns (bytes memory); + function delay() external view returns (uint256); + + function GRACE_PERIOD() external view returns (uint256); + + function acceptAdmin() external; + + function queuedTransactions(bytes32 hash) external view returns (bool); + + function queueTransaction( + address target, + uint256 value, + string calldata signature, + bytes calldata data, + uint256 eta + ) external returns (bytes32); + + function cancelTransaction( + address target, + uint256 value, + string calldata signature, + bytes calldata data, + uint256 eta + ) external; + + function executeTransaction( + address target, + uint256 value, + string calldata signature, + bytes calldata data, + uint256 eta + ) external payable returns (bytes memory); } interface StakingInterface { - function getPriorVotes( - address account, - uint256 blockNumber, - uint256 date - ) external view returns (uint96); - - function getPriorTotalVotingPower(uint32 blockNumber, uint256 time) external view returns (uint96); + function getPriorVotes( + address account, + uint256 blockNumber, + uint256 date + ) external view returns (uint96); + + function getPriorTotalVotingPower(uint32 blockNumber, uint256 time) + external + view + returns (uint96); } diff --git a/contracts/governance/GovernorVault.sol b/contracts/governance/GovernorVault.sol index 7af849860..99d87d216 100644 --- a/contracts/governance/GovernorVault.sol +++ b/contracts/governance/GovernorVault.sol @@ -9,50 +9,50 @@ import "../interfaces/IERC20.sol"; * i.e. Sovryn governance. * */ contract GovernorVault is Ownable { - /* Events */ - - event Deposited(address indexed sender, uint256 amount); - event TokensTransferred(address indexed receiver, address indexed token, uint256 amount); - event RbtcTransferred(address indexed receiver, uint256 amount); - - /* Functions */ - - /** - * @notice Transfer tokens. - * @param _receiver The receiver of tokens. - * @param _token The address of token contract. - * @param _amount The amount to be transferred. - * */ - function transferTokens( - address _receiver, - address _token, - uint256 _amount - ) public onlyOwner { - require(_receiver != address(0), "Invalid receiver address"); - require(_token != address(0), "Invalid token address"); - - require(IERC20(_token).transfer(_receiver, _amount), "Transfer failed"); - emit TokensTransferred(_receiver, _token, _amount); - } - - /** - * @notice Transfer RBTC. - * @param _receiver The receiver of RBTC. - * @param _amount The amount to be transferred. - * */ - function transferRbtc(address payable _receiver, uint256 _amount) public onlyOwner { - require(_receiver != address(0), "Invalid receiver address"); - - address(_receiver).transfer(_amount); - emit RbtcTransferred(_receiver, _amount); - } - - /** - * @notice Fallback function is to react to receiving value (rBTC). - * */ - function() external payable { - if (msg.value > 0) { - emit Deposited(msg.sender, msg.value); - } - } + /* Events */ + + event Deposited(address indexed sender, uint256 amount); + event TokensTransferred(address indexed receiver, address indexed token, uint256 amount); + event RbtcTransferred(address indexed receiver, uint256 amount); + + /* Functions */ + + /** + * @notice Transfer tokens. + * @param _receiver The receiver of tokens. + * @param _token The address of token contract. + * @param _amount The amount to be transferred. + * */ + function transferTokens( + address _receiver, + address _token, + uint256 _amount + ) public onlyOwner { + require(_receiver != address(0), "Invalid receiver address"); + require(_token != address(0), "Invalid token address"); + + require(IERC20(_token).transfer(_receiver, _amount), "Transfer failed"); + emit TokensTransferred(_receiver, _token, _amount); + } + + /** + * @notice Transfer RBTC. + * @param _receiver The receiver of RBTC. + * @param _amount The amount to be transferred. + * */ + function transferRbtc(address payable _receiver, uint256 _amount) public onlyOwner { + require(_receiver != address(0), "Invalid receiver address"); + + address(_receiver).transfer(_amount); + emit RbtcTransferred(_receiver, _amount); + } + + /** + * @notice Fallback function is to react to receiving value (rBTC). + * */ + function() external payable { + if (msg.value > 0) { + emit Deposited(msg.sender, msg.value); + } + } } diff --git a/contracts/governance/IFeeSharingProxy.sol b/contracts/governance/IFeeSharingProxy.sol index aa07a8913..3950bacbb 100644 --- a/contracts/governance/IFeeSharingProxy.sol +++ b/contracts/governance/IFeeSharingProxy.sol @@ -5,13 +5,13 @@ pragma solidity ^0.5.17; * @dev Interfaces are used to cast a contract address into a callable instance. * */ interface IFeeSharingProxy { - function withdrawFees(address[] calldata _token) external; + function withdrawFees(address[] calldata _token) external; - function transferTokens(address _token, uint96 _amount) external; + function transferTokens(address _token, uint96 _amount) external; - function withdraw( - address _loanPoolToken, - uint32 _maxCheckpoints, - address _receiver - ) external; + function withdraw( + address _loanPoolToken, + uint32 _maxCheckpoints, + address _receiver + ) external; } diff --git a/contracts/governance/Staking/Checkpoints.sol b/contracts/governance/Staking/Checkpoints.sol index 6cacab537..e0735b9fc 100644 --- a/contracts/governance/Staking/Checkpoints.sol +++ b/contracts/governance/Staking/Checkpoints.sol @@ -10,249 +10,309 @@ import "./SafeMath96.sol"; * total daily stake. * */ contract Checkpoints is StakingStorage, SafeMath96 { - /// @notice An event emitted when an account changes its delegate. - event DelegateChanged(address indexed delegator, uint256 lockedUntil, address indexed fromDelegate, address indexed toDelegate); + /// @notice An event emitted when an account changes its delegate. + event DelegateChanged( + address indexed delegator, + uint256 lockedUntil, + address indexed fromDelegate, + address indexed toDelegate + ); - /// @notice An event emitted when a delegate account's stake balance changes. - event DelegateStakeChanged(address indexed delegate, uint256 lockedUntil, uint256 previousBalance, uint256 newBalance); + /// @notice An event emitted when a delegate account's stake balance changes. + event DelegateStakeChanged( + address indexed delegate, + uint256 lockedUntil, + uint256 previousBalance, + uint256 newBalance + ); - /// @notice An event emitted when tokens get staked. - event TokensStaked(address indexed staker, uint256 amount, uint256 lockedUntil, uint256 totalStaked); + /// @notice An event emitted when tokens get staked. + event TokensStaked( + address indexed staker, + uint256 amount, + uint256 lockedUntil, + uint256 totalStaked + ); - /// @notice An event emitted when staked tokens get withdrawn. - event StakingWithdrawn(address indexed staker, uint256 amount, uint256 until, address indexed receiver, bool isGovernance); + /// @notice An event emitted when staked tokens get withdrawn. + event StakingWithdrawn( + address indexed staker, + uint256 amount, + uint256 until, + address indexed receiver, + bool isGovernance + ); - /// @notice An event emitted when vesting tokens get withdrawn. - event VestingTokensWithdrawn(address vesting, address receiver); + /// @notice An event emitted when vesting tokens get withdrawn. + event VestingTokensWithdrawn(address vesting, address receiver); - /// @notice An event emitted when the owner unlocks all tokens. - event TokensUnlocked(uint256 amount); + /// @notice An event emitted when the owner unlocks all tokens. + event TokensUnlocked(uint256 amount); - /// @notice An event emitted when a staking period gets extended. - event ExtendedStakingDuration(address indexed staker, uint256 previousDate, uint256 newDate, uint256 amountStaked); + /// @notice An event emitted when a staking period gets extended. + event ExtendedStakingDuration( + address indexed staker, + uint256 previousDate, + uint256 newDate, + uint256 amountStaked + ); - event AdminAdded(address admin); + event AdminAdded(address admin); - event AdminRemoved(address admin); + event AdminRemoved(address admin); - event ContractCodeHashAdded(bytes32 hash); + event ContractCodeHashAdded(bytes32 hash); - event ContractCodeHashRemoved(bytes32 hash); + event ContractCodeHashRemoved(bytes32 hash); - event VestingStakeSet(uint256 lockedTS, uint96 value); + event VestingStakeSet(uint256 lockedTS, uint96 value); - /** - * @notice Increases the user's vesting stake for a giving lock date and writes a checkpoint. - * @param lockedTS The lock date. - * @param value The value to add to the staked balance. - * */ - function _increaseVestingStake(uint256 lockedTS, uint96 value) internal { - uint32 nCheckpoints = numVestingCheckpoints[lockedTS]; - uint96 vested = vestingCheckpoints[lockedTS][nCheckpoints - 1].stake; - uint96 newVest = add96(vested, value, "Staking::_increaseVestingStake: vested amount overflow"); - _writeVestingCheckpoint(lockedTS, nCheckpoints, newVest); - } + /** + * @notice Increases the user's vesting stake for a giving lock date and writes a checkpoint. + * @param lockedTS The lock date. + * @param value The value to add to the staked balance. + * */ + function _increaseVestingStake(uint256 lockedTS, uint96 value) internal { + uint32 nCheckpoints = numVestingCheckpoints[lockedTS]; + uint96 vested = vestingCheckpoints[lockedTS][nCheckpoints - 1].stake; + uint96 newVest = + add96(vested, value, "Staking::_increaseVestingStake: vested amount overflow"); + _writeVestingCheckpoint(lockedTS, nCheckpoints, newVest); + } - /** - * @notice Decreases the user's vesting stake for a giving lock date and writes a checkpoint. - * @param lockedTS The lock date. - * @param value The value to substract to the staked balance. - * */ - function _decreaseVestingStake(uint256 lockedTS, uint96 value) internal { - uint32 nCheckpoints = numVestingCheckpoints[lockedTS]; - uint96 vested = vestingCheckpoints[lockedTS][nCheckpoints - 1].stake; - uint96 newVest = sub96(vested, value, "Staking::_decreaseVestingStake: vested amount underflow"); - _writeVestingCheckpoint(lockedTS, nCheckpoints, newVest); - } + /** + * @notice Decreases the user's vesting stake for a giving lock date and writes a checkpoint. + * @param lockedTS The lock date. + * @param value The value to substract to the staked balance. + * */ + function _decreaseVestingStake(uint256 lockedTS, uint96 value) internal { + uint32 nCheckpoints = numVestingCheckpoints[lockedTS]; + uint96 vested = vestingCheckpoints[lockedTS][nCheckpoints - 1].stake; + uint96 newVest = + sub96(vested, value, "Staking::_decreaseVestingStake: vested amount underflow"); + _writeVestingCheckpoint(lockedTS, nCheckpoints, newVest); + } - /** - * @notice Writes on storage the user vested amount. - * @param lockedTS The lock date. - * @param nCheckpoints The number of checkpoints, to find out the last one index. - * @param newVest The new vest balance. - * */ - function _writeVestingCheckpoint( - uint256 lockedTS, - uint32 nCheckpoints, - uint96 newVest - ) internal { - uint32 blockNumber = safe32(block.number, "Staking::_writeVestingCheckpoint: block number exceeds 32 bits"); + /** + * @notice Writes on storage the user vested amount. + * @param lockedTS The lock date. + * @param nCheckpoints The number of checkpoints, to find out the last one index. + * @param newVest The new vest balance. + * */ + function _writeVestingCheckpoint( + uint256 lockedTS, + uint32 nCheckpoints, + uint96 newVest + ) internal { + uint32 blockNumber = + safe32(block.number, "Staking::_writeVestingCheckpoint: block number exceeds 32 bits"); - if (nCheckpoints > 0 && vestingCheckpoints[lockedTS][nCheckpoints - 1].fromBlock == blockNumber) { - vestingCheckpoints[lockedTS][nCheckpoints - 1].stake = newVest; - } else { - vestingCheckpoints[lockedTS][nCheckpoints] = Checkpoint(blockNumber, newVest); - numVestingCheckpoints[lockedTS] = nCheckpoints + 1; - } - } + if ( + nCheckpoints > 0 && + vestingCheckpoints[lockedTS][nCheckpoints - 1].fromBlock == blockNumber + ) { + vestingCheckpoints[lockedTS][nCheckpoints - 1].stake = newVest; + } else { + vestingCheckpoints[lockedTS][nCheckpoints] = Checkpoint(blockNumber, newVest); + numVestingCheckpoints[lockedTS] = nCheckpoints + 1; + } + } - /** - * @notice Increases the user's stake for a giving lock date and writes a checkpoint. - * @param account The user address. - * @param lockedTS The lock date. - * @param value The value to add to the staked balance. - * */ - function _increaseUserStake( - address account, - uint256 lockedTS, - uint96 value - ) internal { - uint32 nCheckpoints = numUserStakingCheckpoints[account][lockedTS]; - uint96 staked = userStakingCheckpoints[account][lockedTS][nCheckpoints - 1].stake; - uint96 newStake = add96(staked, value, "Staking::_increaseUserStake: staked amount overflow"); - _writeUserCheckpoint(account, lockedTS, nCheckpoints, newStake); - } + /** + * @notice Increases the user's stake for a giving lock date and writes a checkpoint. + * @param account The user address. + * @param lockedTS The lock date. + * @param value The value to add to the staked balance. + * */ + function _increaseUserStake( + address account, + uint256 lockedTS, + uint96 value + ) internal { + uint32 nCheckpoints = numUserStakingCheckpoints[account][lockedTS]; + uint96 staked = userStakingCheckpoints[account][lockedTS][nCheckpoints - 1].stake; + uint96 newStake = + add96(staked, value, "Staking::_increaseUserStake: staked amount overflow"); + _writeUserCheckpoint(account, lockedTS, nCheckpoints, newStake); + } - /** - * @notice Decreases the user's stake for a giving lock date and writes a checkpoint. - * @param account The user address. - * @param lockedTS The lock date. - * @param value The value to substract to the staked balance. - * */ - function _decreaseUserStake( - address account, - uint256 lockedTS, - uint96 value - ) internal { - uint32 nCheckpoints = numUserStakingCheckpoints[account][lockedTS]; - uint96 staked = userStakingCheckpoints[account][lockedTS][nCheckpoints - 1].stake; - uint96 newStake = sub96(staked, value, "Staking::_decreaseUserStake: staked amount underflow"); - _writeUserCheckpoint(account, lockedTS, nCheckpoints, newStake); - } + /** + * @notice Decreases the user's stake for a giving lock date and writes a checkpoint. + * @param account The user address. + * @param lockedTS The lock date. + * @param value The value to substract to the staked balance. + * */ + function _decreaseUserStake( + address account, + uint256 lockedTS, + uint96 value + ) internal { + uint32 nCheckpoints = numUserStakingCheckpoints[account][lockedTS]; + uint96 staked = userStakingCheckpoints[account][lockedTS][nCheckpoints - 1].stake; + uint96 newStake = + sub96(staked, value, "Staking::_decreaseUserStake: staked amount underflow"); + _writeUserCheckpoint(account, lockedTS, nCheckpoints, newStake); + } - /** - * @notice Writes on storage the user stake. - * @param account The user address. - * @param lockedTS The lock date. - * @param nCheckpoints The number of checkpoints, to find out the last one index. - * @param newStake The new staked balance. - * */ - function _writeUserCheckpoint( - address account, - uint256 lockedTS, - uint32 nCheckpoints, - uint96 newStake - ) internal { - uint32 blockNumber = safe32(block.number, "Staking::_writeStakingCheckpoint: block number exceeds 32 bits"); + /** + * @notice Writes on storage the user stake. + * @param account The user address. + * @param lockedTS The lock date. + * @param nCheckpoints The number of checkpoints, to find out the last one index. + * @param newStake The new staked balance. + * */ + function _writeUserCheckpoint( + address account, + uint256 lockedTS, + uint32 nCheckpoints, + uint96 newStake + ) internal { + uint32 blockNumber = + safe32(block.number, "Staking::_writeStakingCheckpoint: block number exceeds 32 bits"); - if (nCheckpoints > 0 && userStakingCheckpoints[account][lockedTS][nCheckpoints - 1].fromBlock == blockNumber) { - userStakingCheckpoints[account][lockedTS][nCheckpoints - 1].stake = newStake; - } else { - userStakingCheckpoints[account][lockedTS][nCheckpoints] = Checkpoint(blockNumber, newStake); - numUserStakingCheckpoints[account][lockedTS] = nCheckpoints + 1; - } - } + if ( + nCheckpoints > 0 && + userStakingCheckpoints[account][lockedTS][nCheckpoints - 1].fromBlock == blockNumber + ) { + userStakingCheckpoints[account][lockedTS][nCheckpoints - 1].stake = newStake; + } else { + userStakingCheckpoints[account][lockedTS][nCheckpoints] = Checkpoint( + blockNumber, + newStake + ); + numUserStakingCheckpoints[account][lockedTS] = nCheckpoints + 1; + } + } - /** - * @notice Increases the delegatee's stake for a giving lock date and writes a checkpoint. - * @param delegatee The delegatee address. - * @param lockedTS The lock date. - * @param value The value to add to the staked balance. - * */ - function _increaseDelegateStake( - address delegatee, - uint256 lockedTS, - uint96 value - ) internal { - uint32 nCheckpoints = numDelegateStakingCheckpoints[delegatee][lockedTS]; - uint96 staked = delegateStakingCheckpoints[delegatee][lockedTS][nCheckpoints - 1].stake; - uint96 newStake = add96(staked, value, "Staking::_increaseDelegateStake: staked amount overflow"); - _writeDelegateCheckpoint(delegatee, lockedTS, nCheckpoints, newStake); - } + /** + * @notice Increases the delegatee's stake for a giving lock date and writes a checkpoint. + * @param delegatee The delegatee address. + * @param lockedTS The lock date. + * @param value The value to add to the staked balance. + * */ + function _increaseDelegateStake( + address delegatee, + uint256 lockedTS, + uint96 value + ) internal { + uint32 nCheckpoints = numDelegateStakingCheckpoints[delegatee][lockedTS]; + uint96 staked = delegateStakingCheckpoints[delegatee][lockedTS][nCheckpoints - 1].stake; + uint96 newStake = + add96(staked, value, "Staking::_increaseDelegateStake: staked amount overflow"); + _writeDelegateCheckpoint(delegatee, lockedTS, nCheckpoints, newStake); + } - /** - * @notice Decreases the delegatee's stake for a giving lock date and writes a checkpoint. - * @param delegatee The delegatee address. - * @param lockedTS The lock date. - * @param value The value to substract to the staked balance. - * */ - function _decreaseDelegateStake( - address delegatee, - uint256 lockedTS, - uint96 value - ) internal { - uint32 nCheckpoints = numDelegateStakingCheckpoints[delegatee][lockedTS]; - uint96 staked = delegateStakingCheckpoints[delegatee][lockedTS][nCheckpoints - 1].stake; - uint96 newStake = 0; - // @dev We need to check delegate checkpoint value here, - // because we had an issue in `stake` function: - // delegate checkpoint wasn't updating for the second and next stakes for the same date - // if first stake was withdrawn completely and stake was delegated to the staker - // (no delegation to another address). - // @dev It can be greater than 0, but inconsistent after 3 transactions - if (staked > value) { - newStake = sub96(staked, value, "Staking::_decreaseDelegateStake: staked amount underflow"); - } - _writeDelegateCheckpoint(delegatee, lockedTS, nCheckpoints, newStake); - } + /** + * @notice Decreases the delegatee's stake for a giving lock date and writes a checkpoint. + * @param delegatee The delegatee address. + * @param lockedTS The lock date. + * @param value The value to substract to the staked balance. + * */ + function _decreaseDelegateStake( + address delegatee, + uint256 lockedTS, + uint96 value + ) internal { + uint32 nCheckpoints = numDelegateStakingCheckpoints[delegatee][lockedTS]; + uint96 staked = delegateStakingCheckpoints[delegatee][lockedTS][nCheckpoints - 1].stake; + uint96 newStake = 0; + // @dev We need to check delegate checkpoint value here, + // because we had an issue in `stake` function: + // delegate checkpoint wasn't updating for the second and next stakes for the same date + // if first stake was withdrawn completely and stake was delegated to the staker + // (no delegation to another address). + // @dev It can be greater than 0, but inconsistent after 3 transactions + if (staked > value) { + newStake = sub96( + staked, + value, + "Staking::_decreaseDelegateStake: staked amount underflow" + ); + } + _writeDelegateCheckpoint(delegatee, lockedTS, nCheckpoints, newStake); + } - /** - * @notice Writes on storage the delegate stake. - * @param delegatee The delegate address. - * @param lockedTS The lock date. - * @param nCheckpoints The number of checkpoints, to find out the last one index. - * @param newStake The new staked balance. - * */ - function _writeDelegateCheckpoint( - address delegatee, - uint256 lockedTS, - uint32 nCheckpoints, - uint96 newStake - ) internal { - uint32 blockNumber = safe32(block.number, "Staking::_writeStakingCheckpoint: block number exceeds 32 bits"); - uint96 oldStake = delegateStakingCheckpoints[delegatee][lockedTS][nCheckpoints - 1].stake; + /** + * @notice Writes on storage the delegate stake. + * @param delegatee The delegate address. + * @param lockedTS The lock date. + * @param nCheckpoints The number of checkpoints, to find out the last one index. + * @param newStake The new staked balance. + * */ + function _writeDelegateCheckpoint( + address delegatee, + uint256 lockedTS, + uint32 nCheckpoints, + uint96 newStake + ) internal { + uint32 blockNumber = + safe32(block.number, "Staking::_writeStakingCheckpoint: block number exceeds 32 bits"); + uint96 oldStake = delegateStakingCheckpoints[delegatee][lockedTS][nCheckpoints - 1].stake; - if (nCheckpoints > 0 && delegateStakingCheckpoints[delegatee][lockedTS][nCheckpoints - 1].fromBlock == blockNumber) { - delegateStakingCheckpoints[delegatee][lockedTS][nCheckpoints - 1].stake = newStake; - } else { - delegateStakingCheckpoints[delegatee][lockedTS][nCheckpoints] = Checkpoint(blockNumber, newStake); - numDelegateStakingCheckpoints[delegatee][lockedTS] = nCheckpoints + 1; - } - emit DelegateStakeChanged(delegatee, lockedTS, oldStake, newStake); - } + if ( + nCheckpoints > 0 && + delegateStakingCheckpoints[delegatee][lockedTS][nCheckpoints - 1].fromBlock == + blockNumber + ) { + delegateStakingCheckpoints[delegatee][lockedTS][nCheckpoints - 1].stake = newStake; + } else { + delegateStakingCheckpoints[delegatee][lockedTS][nCheckpoints] = Checkpoint( + blockNumber, + newStake + ); + numDelegateStakingCheckpoints[delegatee][lockedTS] = nCheckpoints + 1; + } + emit DelegateStakeChanged(delegatee, lockedTS, oldStake, newStake); + } - /** - * @notice Increases the total stake for a giving lock date and writes a checkpoint. - * @param lockedTS The lock date. - * @param value The value to add to the staked balance. - * */ - function _increaseDailyStake(uint256 lockedTS, uint96 value) internal { - uint32 nCheckpoints = numTotalStakingCheckpoints[lockedTS]; - uint96 staked = totalStakingCheckpoints[lockedTS][nCheckpoints - 1].stake; - uint96 newStake = add96(staked, value, "Staking::_increaseDailyStake: staked amount overflow"); - _writeStakingCheckpoint(lockedTS, nCheckpoints, newStake); - } + /** + * @notice Increases the total stake for a giving lock date and writes a checkpoint. + * @param lockedTS The lock date. + * @param value The value to add to the staked balance. + * */ + function _increaseDailyStake(uint256 lockedTS, uint96 value) internal { + uint32 nCheckpoints = numTotalStakingCheckpoints[lockedTS]; + uint96 staked = totalStakingCheckpoints[lockedTS][nCheckpoints - 1].stake; + uint96 newStake = + add96(staked, value, "Staking::_increaseDailyStake: staked amount overflow"); + _writeStakingCheckpoint(lockedTS, nCheckpoints, newStake); + } - /** - * @notice Decreases the total stake for a giving lock date and writes a checkpoint. - * @param lockedTS The lock date. - * @param value The value to substract to the staked balance. - * */ - function _decreaseDailyStake(uint256 lockedTS, uint96 value) internal { - uint32 nCheckpoints = numTotalStakingCheckpoints[lockedTS]; - uint96 staked = totalStakingCheckpoints[lockedTS][nCheckpoints - 1].stake; - uint96 newStake = sub96(staked, value, "Staking::_decreaseDailyStake: staked amount underflow"); - _writeStakingCheckpoint(lockedTS, nCheckpoints, newStake); - } + /** + * @notice Decreases the total stake for a giving lock date and writes a checkpoint. + * @param lockedTS The lock date. + * @param value The value to substract to the staked balance. + * */ + function _decreaseDailyStake(uint256 lockedTS, uint96 value) internal { + uint32 nCheckpoints = numTotalStakingCheckpoints[lockedTS]; + uint96 staked = totalStakingCheckpoints[lockedTS][nCheckpoints - 1].stake; + uint96 newStake = + sub96(staked, value, "Staking::_decreaseDailyStake: staked amount underflow"); + _writeStakingCheckpoint(lockedTS, nCheckpoints, newStake); + } - /** - * @notice Writes on storage the total stake. - * @param lockedTS The lock date. - * @param nCheckpoints The number of checkpoints, to find out the last one index. - * @param newStake The new staked balance. - * */ - function _writeStakingCheckpoint( - uint256 lockedTS, - uint32 nCheckpoints, - uint96 newStake - ) internal { - uint32 blockNumber = safe32(block.number, "Staking::_writeStakingCheckpoint: block number exceeds 32 bits"); + /** + * @notice Writes on storage the total stake. + * @param lockedTS The lock date. + * @param nCheckpoints The number of checkpoints, to find out the last one index. + * @param newStake The new staked balance. + * */ + function _writeStakingCheckpoint( + uint256 lockedTS, + uint32 nCheckpoints, + uint96 newStake + ) internal { + uint32 blockNumber = + safe32(block.number, "Staking::_writeStakingCheckpoint: block number exceeds 32 bits"); - if (nCheckpoints > 0 && totalStakingCheckpoints[lockedTS][nCheckpoints - 1].fromBlock == blockNumber) { - totalStakingCheckpoints[lockedTS][nCheckpoints - 1].stake = newStake; - } else { - totalStakingCheckpoints[lockedTS][nCheckpoints] = Checkpoint(blockNumber, newStake); - numTotalStakingCheckpoints[lockedTS] = nCheckpoints + 1; - } - } + if ( + nCheckpoints > 0 && + totalStakingCheckpoints[lockedTS][nCheckpoints - 1].fromBlock == blockNumber + ) { + totalStakingCheckpoints[lockedTS][nCheckpoints - 1].stake = newStake; + } else { + totalStakingCheckpoints[lockedTS][nCheckpoints] = Checkpoint(blockNumber, newStake); + numTotalStakingCheckpoints[lockedTS] = nCheckpoints + 1; + } + } } diff --git a/contracts/governance/Staking/IStaking.sol b/contracts/governance/Staking/IStaking.sol index 1a2a56dd9..54adf5130 100644 --- a/contracts/governance/Staking/IStaking.sol +++ b/contracts/governance/Staking/IStaking.sol @@ -5,39 +5,45 @@ pragma solidity ^0.5.17; * @dev Interfaces are used to cast a contract address into a callable instance. */ interface IStaking { - function stakesBySchedule( - uint256 amount, - uint256 cliff, - uint256 duration, - uint256 intervalLength, - address stakeFor, - address delegatee - ) external; - - function stake( - uint96 amount, - uint256 until, - address stakeFor, - address delegatee - ) external; - - function getPriorVotes( - address account, - uint256 blockNumber, - uint256 date - ) external view returns (uint96); - - function getPriorTotalVotingPower(uint32 blockNumber, uint256 time) external view returns (uint96); - - function getPriorWeightedStake( - address account, - uint256 blockNumber, - uint256 date - ) external view returns (uint96); - - function getPriorVestingWeightedStake(uint256 blockNumber, uint256 date) external view returns (uint96); - - function timestampToLockDate(uint256 timestamp) external view returns (uint256 lockDate); - - function isVestingContract(address stakerAddress) external view returns (bool); + function stakesBySchedule( + uint256 amount, + uint256 cliff, + uint256 duration, + uint256 intervalLength, + address stakeFor, + address delegatee + ) external; + + function stake( + uint96 amount, + uint256 until, + address stakeFor, + address delegatee + ) external; + + function getPriorVotes( + address account, + uint256 blockNumber, + uint256 date + ) external view returns (uint96); + + function getPriorTotalVotingPower(uint32 blockNumber, uint256 time) + external + view + returns (uint96); + + function getPriorWeightedStake( + address account, + uint256 blockNumber, + uint256 date + ) external view returns (uint96); + + function getPriorVestingWeightedStake(uint256 blockNumber, uint256 date) + external + view + returns (uint96); + + function timestampToLockDate(uint256 timestamp) external view returns (uint256 lockDate); + + function isVestingContract(address stakerAddress) external view returns (bool); } diff --git a/contracts/governance/Staking/SafeMath96.sol b/contracts/governance/Staking/SafeMath96.sol index f6b382705..e1514e6ba 100644 --- a/contracts/governance/Staking/SafeMath96.sol +++ b/contracts/governance/Staking/SafeMath96.sol @@ -17,97 +17,97 @@ pragma experimental ABIEncoderV2; * class of bugs, so it's recommended to use it always. * */ contract SafeMath96 { - function safe32(uint256 n, string memory errorMessage) internal pure returns (uint32) { - require(n < 2**32, errorMessage); - return uint32(n); - } + function safe32(uint256 n, string memory errorMessage) internal pure returns (uint32) { + require(n < 2**32, errorMessage); + return uint32(n); + } - function safe64(uint256 n, string memory errorMessage) internal pure returns (uint64) { - require(n < 2**64, errorMessage); - return uint64(n); - } + function safe64(uint256 n, string memory errorMessage) internal pure returns (uint64) { + require(n < 2**64, errorMessage); + return uint64(n); + } - function safe96(uint256 n, string memory errorMessage) internal pure returns (uint96) { - require(n < 2**96, errorMessage); - return uint96(n); - } + function safe96(uint256 n, string memory errorMessage) internal pure returns (uint96) { + require(n < 2**96, errorMessage); + return uint96(n); + } - /** - * @notice Adds two unsigned integers, reverting on overflow. - * @dev Counterpart to Solidity's `+` operator. - * @param a First integer. - * @param b Second integer. - * @param errorMessage The revert message on overflow. - * @return The safe addition a+b. - * */ - function add96( - uint96 a, - uint96 b, - string memory errorMessage - ) internal pure returns (uint96) { - uint96 c = a + b; - require(c >= a, errorMessage); - return c; - } + /** + * @notice Adds two unsigned integers, reverting on overflow. + * @dev Counterpart to Solidity's `+` operator. + * @param a First integer. + * @param b Second integer. + * @param errorMessage The revert message on overflow. + * @return The safe addition a+b. + * */ + function add96( + uint96 a, + uint96 b, + string memory errorMessage + ) internal pure returns (uint96) { + uint96 c = a + b; + require(c >= a, errorMessage); + return c; + } - /** - * @notice Substracts two unsigned integers, reverting on underflow. - * @dev Counterpart to Solidity's `-` operator. - * @param a First integer. - * @param b Second integer. - * @param errorMessage The revert message on underflow. - * @return The safe substraction a-b. - * */ - function sub96( - uint96 a, - uint96 b, - string memory errorMessage - ) internal pure returns (uint96) { - require(b <= a, errorMessage); - return a - b; - } + /** + * @notice Substracts two unsigned integers, reverting on underflow. + * @dev Counterpart to Solidity's `-` operator. + * @param a First integer. + * @param b Second integer. + * @param errorMessage The revert message on underflow. + * @return The safe substraction a-b. + * */ + function sub96( + uint96 a, + uint96 b, + string memory errorMessage + ) internal pure returns (uint96) { + require(b <= a, errorMessage); + return a - b; + } - /** - * @notice Multiplies two unsigned integers, reverting on overflow. - * @dev Counterpart to Solidity's `*` operator. - * @param a First integer. - * @param b Second integer. - * @param errorMessage The revert message on overflow. - * @return The safe product a*b. - * */ - function mul96( - uint96 a, - uint96 b, - string memory errorMessage - ) internal pure returns (uint96) { - if (a == 0) { - return 0; - } + /** + * @notice Multiplies two unsigned integers, reverting on overflow. + * @dev Counterpart to Solidity's `*` operator. + * @param a First integer. + * @param b Second integer. + * @param errorMessage The revert message on overflow. + * @return The safe product a*b. + * */ + function mul96( + uint96 a, + uint96 b, + string memory errorMessage + ) internal pure returns (uint96) { + if (a == 0) { + return 0; + } - uint96 c = a * b; - require(c / a == b, errorMessage); + uint96 c = a * b; + require(c / a == b, errorMessage); - return c; - } + return c; + } - /** - * @notice Divides two unsigned integers, reverting on overflow. - * @dev Counterpart to Solidity's `/` operator. - * @param a First integer. - * @param b Second integer. - * @param errorMessage The revert message on overflow. - * @return The safe division a/b. - * */ - function div96( - uint96 a, - uint96 b, - string memory errorMessage - ) internal pure returns (uint96) { - // Solidity only automatically asserts when dividing by 0 - require(b > 0, errorMessage); - uint96 c = a / b; - // assert(a == b * c + a % b); // There is no case in which this doesn't hold + /** + * @notice Divides two unsigned integers, reverting on overflow. + * @dev Counterpart to Solidity's `/` operator. + * @param a First integer. + * @param b Second integer. + * @param errorMessage The revert message on overflow. + * @return The safe division a/b. + * */ + function div96( + uint96 a, + uint96 b, + string memory errorMessage + ) internal pure returns (uint96) { + // Solidity only automatically asserts when dividing by 0 + require(b > 0, errorMessage); + uint96 c = a / b; + // assert(a == b * c + a % b); // There is no case in which this doesn't hold - return c; - } + return c; + } } diff --git a/contracts/governance/Staking/Staking.sol b/contracts/governance/Staking/Staking.sol index 461efe342..d1824f2be 100644 --- a/contracts/governance/Staking/Staking.sol +++ b/contracts/governance/Staking/Staking.sol @@ -21,707 +21,739 @@ import "../../openzeppelin/SafeMath.sol"; * early unstaking. * */ contract Staking is IStaking, WeightedStaking, ApprovalReceiver { - using SafeMath for uint256; - - /// @notice Constant used for computing the vesting dates. - uint256 constant FOUR_WEEKS = 4 weeks; - - /** - * @notice Stake the given amount for the given duration of time. - * @param amount The number of tokens to stake. - * @param until Timestamp indicating the date until which to stake. - * @param stakeFor The address to stake the tokens for or 0x0 if staking for oneself. - * @param delegatee The address of the delegatee or 0x0 if there is none. - * */ - function stake( - uint96 amount, - uint256 until, - address stakeFor, - address delegatee - ) external { - _stake(msg.sender, amount, until, stakeFor, delegatee, false); - } - - /** - * @notice Stake the given amount for the given duration of time. - * @dev This function will be invoked from receiveApproval - * @dev SOV.approveAndCall -> this.receiveApproval -> this.stakeWithApproval - * @param sender The sender of SOV.approveAndCall - * @param amount The number of tokens to stake. - * @param until Timestamp indicating the date until which to stake. - * @param stakeFor The address to stake the tokens for or 0x0 if staking for oneself. - * @param delegatee The address of the delegatee or 0x0 if there is none. - * */ - function stakeWithApproval( - address sender, - uint96 amount, - uint256 until, - address stakeFor, - address delegatee - ) public onlyThisContract { - _stake(sender, amount, until, stakeFor, delegatee, false); - } - - /** - * @notice Send sender's tokens to this contract and update its staked balance. - * @param sender The sender of the tokens. - * @param amount The number of tokens to send. - * @param until The date until which the tokens will be staked. - * @param stakeFor The beneficiary whose stake will be increased. - * @param delegatee The address of the delegatee or stakeFor if default 0x0. - * @param timeAdjusted Whether fixing date to stacking periods or not. - * */ - function _stake( - address sender, - uint96 amount, - uint256 until, - address stakeFor, - address delegatee, - bool timeAdjusted - ) internal { - require(amount > 0, "amount needs to be bigger than 0"); - - if (!timeAdjusted) { - until = timestampToLockDate(until); - } - require(until > block.timestamp, "Staking::timestampToLockDate: staking period too short"); - - /// @dev Stake for the sender if not specified otherwise. - if (stakeFor == address(0)) { - stakeFor = sender; - } - - /// @dev Delegate for stakeFor if not specified otherwise. - if (delegatee == address(0)) { - delegatee = stakeFor; - } - - /// @dev Do not stake longer than the max duration. - if (!timeAdjusted) { - uint256 latest = timestampToLockDate(block.timestamp + MAX_DURATION); - if (until > latest) until = latest; - } - - uint96 previousBalance = currentBalance(stakeFor, until); - - /// @dev Increase stake. - _increaseStake(sender, amount, stakeFor, until); - - // @dev Previous version wasn't working properly for the following case: - // delegate checkpoint wasn't updating for the second and next stakes for the same date - // if first stake was withdrawn completely and stake was delegated to the staker - // (no delegation to another address). - address previousDelegatee = delegates[stakeFor][until]; - if (previousDelegatee != delegatee) { - /// @dev Update delegatee. - delegates[stakeFor][until] = delegatee; - - /// @dev Decrease stake on previous balance for previous delegatee. - _decreaseDelegateStake(previousDelegatee, until, previousBalance); - - /// @dev Add previousBalance to amount. - amount = add96(previousBalance, amount, "balance overflow"); - } - - /// @dev Increase stake. - _increaseDelegateStake(delegatee, until, amount); - emit DelegateChanged(stakeFor, until, previousDelegatee, delegatee); - } - - /** - * @notice Extend the staking duration until the specified date. - * @param previousLock The old unlocking timestamp. - * @param until The new unlocking timestamp in seconds. - * */ - function extendStakingDuration(uint256 previousLock, uint256 until) public { - until = timestampToLockDate(until); - require(previousLock <= until, "cannot reduce the staking duration"); - - /// @dev Do not exceed the max duration, no overflow possible. - uint256 latest = timestampToLockDate(block.timestamp + MAX_DURATION); - if (until > latest) until = latest; - - /// @dev Update checkpoints. - /// @dev TODO James: Can reading stake at block.number -1 cause trouble with multiple tx in a block? - uint96 amount = _getPriorUserStakeByDate(msg.sender, previousLock, block.number - 1); - require(amount > 0, "nothing staked until the previous lock date"); - _decreaseUserStake(msg.sender, previousLock, amount); - _increaseUserStake(msg.sender, until, amount); - - if (isVestingContract(msg.sender)) { - _decreaseVestingStake(previousLock, amount); - _increaseVestingStake(until, amount); - } - - _decreaseDailyStake(previousLock, amount); - _increaseDailyStake(until, amount); - - /// @dev Delegate might change: if there is already a delegate set for the until date, it will remain the delegate for this position - address delegateFrom = delegates[msg.sender][previousLock]; - address delegateTo = delegates[msg.sender][until]; - if (delegateTo == address(0)) { - delegateTo = delegateFrom; - delegates[msg.sender][until] = delegateFrom; - } - delegates[msg.sender][previousLock] = address(0); - _decreaseDelegateStake(delegateFrom, previousLock, amount); - _increaseDelegateStake(delegateTo, until, amount); - - emit ExtendedStakingDuration(msg.sender, previousLock, until, amount); - } - - /** - * @notice Send sender's tokens to this contract and update its staked balance. - * @param sender The sender of the tokens. - * @param amount The number of tokens to send. - * @param stakeFor The beneficiary whose stake will be increased. - * @param until The date until which the tokens will be staked. - * */ - function _increaseStake( - address sender, - uint96 amount, - address stakeFor, - uint256 until - ) internal { - /// @dev Retrieve the SOV tokens. - bool success = SOVToken.transferFrom(sender, address(this), amount); - require(success); - - /// @dev Increase staked balance. - uint96 balance = currentBalance(stakeFor, until); - balance = add96(balance, amount, "overflow"); - - /// @dev Update checkpoints. - _increaseDailyStake(until, amount); - _increaseUserStake(stakeFor, until, amount); - - if (isVestingContract(stakeFor)) _increaseVestingStake(until, amount); - - emit TokensStaked(stakeFor, amount, until, balance); - } - - /** - * @notice Stake tokens according to the vesting schedule. - * @param amount The amount of tokens to stake. - * @param cliff The time interval to the first withdraw. - * @param duration The staking duration. - * @param intervalLength The length of each staking interval when cliff passed. - * @param stakeFor The address to stake the tokens for or 0x0 if staking for oneself. - * @param delegatee The address of the delegatee or 0x0 if there is none. - * */ - function stakesBySchedule( - uint256 amount, - uint256 cliff, - uint256 duration, - uint256 intervalLength, - address stakeFor, - address delegatee - ) public { - /** - * @dev Stake them until lock dates according to the vesting schedule. - * Note: because staking is only possible in periods of 2 weeks, - * the total duration might end up a bit shorter than specified - * depending on the date of staking. - * */ - uint256 start = timestampToLockDate(block.timestamp + cliff); - if (duration > MAX_DURATION) { - duration = MAX_DURATION; - } - uint256 end = timestampToLockDate(block.timestamp + duration); - uint256 numIntervals = (end - start) / intervalLength + 1; - uint256 stakedPerInterval = amount / numIntervals; - /// @dev stakedPerInterval might lose some dust on rounding. Add it to the first staking date. - if (numIntervals >= 1) { - _stake(msg.sender, uint96(amount - stakedPerInterval * (numIntervals - 1)), start, stakeFor, delegatee, true); - } - /// @dev Stake the rest in 4 week intervals. - for (uint256 i = start + intervalLength; i <= end; i += intervalLength) { - /// @dev Stakes for itself, delegates to the owner. - _stake(msg.sender, uint96(stakedPerInterval), i, stakeFor, delegatee, true); - } - } - - /** - * @notice Withdraw the given amount of tokens if they are unlocked. - * @param amount The number of tokens to withdraw. - * @param until The date until which the tokens were staked. - * @param receiver The receiver of the tokens. If not specified, send to the msg.sender - * */ - function withdraw( - uint96 amount, - uint256 until, - address receiver - ) public { - _withdraw(amount, until, receiver, false); - // @dev withdraws tokens for lock date 2 weeks later than given lock date if sender is a contract - // we need to check block.timestamp here - _withdrawNext(amount, until, receiver, false); - } - - /** - * @notice Withdraw the given amount of tokens. - * @param amount The number of tokens to withdraw. - * @param until The date until which the tokens were staked. - * @param receiver The receiver of the tokens. If not specified, send to the msg.sender - * @dev Can be invoked only by whitelisted contract passed to governanceWithdrawVesting - * */ - function governanceWithdraw( - uint96 amount, - uint256 until, - address receiver - ) public { - require(vestingWhitelist[msg.sender], "unauthorized"); - - _withdraw(amount, until, receiver, true); - // @dev withdraws tokens for lock date 2 weeks later than given lock date if sender is a contract - // we don't need to check block.timestamp here - _withdrawNext(amount, until, receiver, true); - } - - /** - * @notice Withdraw tokens for vesting contract. - * @param vesting The address of Vesting contract. - * @param receiver The receiver of the tokens. If not specified, send to the msg.sender - * @dev Can be invoked only by whitelisted contract passed to governanceWithdrawVesting. - * */ - function governanceWithdrawVesting(address vesting, address receiver) public onlyAuthorized { - vestingWhitelist[vesting] = true; - ITeamVesting(vesting).governanceWithdrawTokens(receiver); - vestingWhitelist[vesting] = false; - - emit VestingTokensWithdrawn(vesting, receiver); - } - - /** - * @notice Send user' staked tokens to a receiver taking into account punishments. - * Sovryn encourages long-term commitment and thinking. When/if you unstake before - * the end of the staking period, a percentage of the original staking amount will - * be slashed. This amount is also added to the reward pool and is distributed - * between all other stakers. - * - * @param amount The number of tokens to withdraw. - * @param until The date until which the tokens were staked. - * @param receiver The receiver of the tokens. If not specified, send to the msg.sender - * @param isGovernance Whether all tokens (true) - * or just unlocked tokens (false). - * */ - function _withdraw( - uint96 amount, - uint256 until, - address receiver, - bool isGovernance - ) internal { - // @dev it's very unlikely some one will have 1/10**18 SOV staked in Vesting contract - // this check is a part of workaround for Vesting.withdrawTokens issue - if (amount == 1 && isVestingContract(msg.sender)) { - return; - } - until = _adjustDateForOrigin(until); - _validateWithdrawParams(amount, until); - - /// @dev Determine the receiver. - if (receiver == address(0)) receiver = msg.sender; - - /// @dev Update the checkpoints. - _decreaseDailyStake(until, amount); - _decreaseUserStake(msg.sender, until, amount); - if (isVestingContract(msg.sender)) _decreaseVestingStake(until, amount); - _decreaseDelegateStake(delegates[msg.sender][until], until, amount); - - /// @dev Early unstaking should be punished. - if (block.timestamp < until && !allUnlocked && !isGovernance) { - uint96 punishedAmount = _getPunishedAmount(amount, until); - amount -= punishedAmount; - - /// @dev punishedAmount can be 0 if block.timestamp are very close to 'until' - if (punishedAmount > 0) { - require(address(feeSharing) != address(0), "Staking::withdraw: FeeSharing address wasn't set"); - /// @dev Move punished amount to fee sharing. - /// @dev Approve transfer here and let feeSharing do transfer and write checkpoint. - SOVToken.approve(address(feeSharing), punishedAmount); - feeSharing.transferTokens(address(SOVToken), punishedAmount); - } - } - - /// @dev transferFrom - bool success = SOVToken.transfer(receiver, amount); - require(success, "Staking::withdraw: Token transfer failed"); - - emit StakingWithdrawn(msg.sender, amount, until, receiver, isGovernance); - } - - // @dev withdraws tokens for lock date 2 weeks later than given lock date - function _withdrawNext( - uint96 amount, - uint256 until, - address receiver, - bool isGovernance - ) internal { - if (isVestingContract(msg.sender)) { - uint256 nextLock = until.add(TWO_WEEKS); - if (isGovernance || block.timestamp >= nextLock) { - uint96 stakes = _getPriorUserStakeByDate(msg.sender, nextLock, block.number - 1); - if (stakes > 0) { - _withdraw(stakes, nextLock, receiver, isGovernance); - } - } - } - } - - /** - * @notice Get available and punished amount for withdrawing. - * @param amount The number of tokens to withdraw. - * @param until The date until which the tokens were staked. - * */ - function getWithdrawAmounts(uint96 amount, uint256 until) public view returns (uint96, uint96) { - _validateWithdrawParams(amount, until); - uint96 punishedAmount = _getPunishedAmount(amount, until); - return (amount - punishedAmount, punishedAmount); - } - - /** - * @notice Get punished amount for withdrawing. - * @param amount The number of tokens to withdraw. - * @param until The date until which the tokens were staked. - * */ - function _getPunishedAmount(uint96 amount, uint256 until) internal view returns (uint96) { - uint256 date = timestampToLockDate(block.timestamp); - uint96 weight = computeWeightByDate(until, date); /// @dev (10 - 1) * WEIGHT_FACTOR - weight = weight * weightScaling; - return (amount * weight) / WEIGHT_FACTOR / 100; - } - - /** - * @notice Validate withdraw parameters. - * @param amount The number of tokens to withdraw. - * @param until The date until which the tokens were staked. - * */ - function _validateWithdrawParams(uint96 amount, uint256 until) internal view { - require(amount > 0, "Staking::withdraw: amount of tokens to be withdrawn needs to be bigger than 0"); - uint96 balance = _getPriorUserStakeByDate(msg.sender, until, block.number - 1); - require(amount <= balance, "Staking::withdraw: not enough balance"); - } - - /** - * @notice Get the current balance of an account locked until a certain date. - * @param account The user address. - * @param lockDate The lock date. - * @return The stake amount. - * */ - function currentBalance(address account, uint256 lockDate) internal view returns (uint96) { - return userStakingCheckpoints[account][lockDate][numUserStakingCheckpoints[account][lockDate] - 1].stake; - } - - /** - * @notice Get the number of staked tokens held by the user account. - * @dev Iterate checkpoints adding up stakes. - * @param account The address of the account to get the balance of. - * @return The number of tokens held. - * */ - function balanceOf(address account) public view returns (uint96 balance) { - for (uint256 i = kickoffTS; i <= block.timestamp + MAX_DURATION; i += TWO_WEEKS) { - balance = add96(balance, currentBalance(account, i), "Staking::balanceOf: overflow"); - } - } - - /** - * @notice Delegate votes from `msg.sender` which are locked until lockDate to `delegatee`. - * @param delegatee The address to delegate votes to. - * @param lockDate the date if the position to delegate. - * */ - function delegate(address delegatee, uint256 lockDate) public { - _delegate(msg.sender, delegatee, lockDate); - // @dev delegates tokens for lock date 2 weeks later than given lock date - // if message sender is a contract - _delegateNext(msg.sender, delegatee, lockDate); - } - - /** - * @notice Delegates votes from signatory to a delegatee account. - * Voting with EIP-712 Signatures. - * - * Voting power can be delegated to any address, and then can be used to - * vote on proposals. A key benefit to users of by-signature functionality - * is that they can create a signed vote transaction for free, and have a - * trusted third-party spend rBTC(or ETH) on gas fees and write it to the - * blockchain for them. - * - * The third party in this scenario, submitting the SOV-holder’s signed - * transaction holds a voting power that is for only a single proposal. - * The signatory still holds the power to vote on their own behalf in - * the proposal if the third party has not yet published the signed - * transaction that was given to them. - * - * @dev The signature needs to be broken up into 3 parameters, known as - * v, r and s: - * const r = '0x' + sig.substring(2).substring(0, 64); - * const s = '0x' + sig.substring(2).substring(64, 128); - * const v = '0x' + sig.substring(2).substring(128, 130); - * - * @param delegatee The address to delegate votes to. - * @param lockDate The date until which the position is locked. - * @param nonce The contract state required to match the signature. - * @param expiry The time at which to expire the signature. - * @param v The recovery byte of the signature. - * @param r Half of the ECDSA signature pair. - * @param s Half of the ECDSA signature pair. - * */ - function delegateBySig( - address delegatee, - uint256 lockDate, - uint256 nonce, - uint256 expiry, - uint8 v, - bytes32 r, - bytes32 s - ) public { - /** - * @dev The DOMAIN_SEPARATOR is a hash that uniquely identifies a - * smart contract. It is built from a string denoting it as an - * EIP712 Domain, the name of the token contract, the version, - * the chainId in case it changes, and the address that the - * contract is deployed at. - * */ - bytes32 domainSeparator = keccak256(abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(name)), getChainId(), address(this))); - - /// @dev GovernorAlpha uses BALLOT_TYPEHASH, while Staking uses DELEGATION_TYPEHASH - bytes32 structHash = keccak256(abi.encode(DELEGATION_TYPEHASH, delegatee, lockDate, nonce, expiry)); - - bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); - address signatory = ecrecover(digest, v, r, s); - - /// @dev Verify address is not null and PK is not null either. - require(RSKAddrValidator.checkPKNotZero(signatory), "Staking::delegateBySig: invalid signature"); - require(nonce == nonces[signatory]++, "Staking::delegateBySig: invalid nonce"); - require(now <= expiry, "Staking::delegateBySig: signature expired"); - _delegate(signatory, delegatee, lockDate); - // @dev delegates tokens for lock date 2 weeks later than given lock date - // if message sender is a contract - _delegateNext(signatory, delegatee, lockDate); - } - - /** - * @notice Get the current votes balance for a user account. - * @param account The address to get votes balance. - * @dev This is a wrapper to simplify arguments. The actual computation is - * performed on WeightedStaking parent contract. - * @return The number of current votes for a user account. - * */ - function getCurrentVotes(address account) external view returns (uint96) { - return getPriorVotes(account, block.number - 1, block.timestamp); - } - - /** - * @notice Get the current number of tokens staked for a day. - * @param lockedTS The timestamp to get the staked tokens for. - * */ - function getCurrentStakedUntil(uint256 lockedTS) external view returns (uint96) { - uint32 nCheckpoints = numTotalStakingCheckpoints[lockedTS]; - return nCheckpoints > 0 ? totalStakingCheckpoints[lockedTS][nCheckpoints - 1].stake : 0; - } - - /** - * @notice Set new delegatee. Move from user's current delegate to a new - * delegatee the stake balance. - * @param delegator The user address to move stake balance from its current delegatee. - * @param delegatee The new delegatee. The address to move stake balance to. - * @param lockedTS The lock date. - * */ - function _delegate( - address delegator, - address delegatee, - uint256 lockedTS - ) internal { - address currentDelegate = delegates[delegator][lockedTS]; - uint96 delegatorBalance = currentBalance(delegator, lockedTS); - delegates[delegator][lockedTS] = delegatee; - - emit DelegateChanged(delegator, lockedTS, currentDelegate, delegatee); - - _moveDelegates(currentDelegate, delegatee, delegatorBalance, lockedTS); - } - - // @dev delegates tokens for lock date 2 weeks later than given lock date - // if message sender is a contract - function _delegateNext( - address delegator, - address delegatee, - uint256 lockedTS - ) internal { - if (isVestingContract(msg.sender)) { - uint256 nextLock = lockedTS.add(TWO_WEEKS); - address currentDelegate = delegates[delegator][nextLock]; - if (currentDelegate != delegatee) { - _delegate(delegator, delegatee, nextLock); - } - - // @dev workaround for the issue with a delegation of the latest stake - uint256 endDate = IVesting(msg.sender).endDate(); - nextLock = lockedTS.add(FOUR_WEEKS); - if (nextLock == endDate) { - currentDelegate = delegates[delegator][nextLock]; - if (currentDelegate != delegatee) { - _delegate(delegator, delegatee, nextLock); - } - } - } - } - - /** - * @notice Move an amount of delegate stake from a source address to a - * destination address. - * @param srcRep The address to get the staked amount from. - * @param dstRep The address to send the staked amount to. - * @param amount The staked amount to move. - * @param lockedTS The lock date. - * */ - function _moveDelegates( - address srcRep, - address dstRep, - uint96 amount, - uint256 lockedTS - ) internal { - if (srcRep != dstRep && amount > 0) { - if (srcRep != address(0)) _decreaseDelegateStake(srcRep, lockedTS, amount); - - if (dstRep != address(0)) _increaseDelegateStake(dstRep, lockedTS, amount); - } - } - - /** - * @notice Retrieve CHAIN_ID of the executing chain. - * - * Chain identifier (chainID) introduced in EIP-155 protects transaction - * included into one chain from being included into another chain. - * Basically, chain identifier is an integer number being used in the - * processes of signing transactions and verifying transaction signatures. - * - * @dev As of version 0.5.12, Solidity includes an assembly function - * chainid() that provides access to the new CHAINID opcode. - * - * TODO: chainId is included in block. So you can get chain id like - * block timestamp or block number: block.chainid; - * */ - function getChainId() internal pure returns (uint256) { - uint256 chainId; - assembly { - chainId := chainid() - } - return chainId; - } - - /** - * @notice Allow the owner to set a new staking contract. - * As a consequence it allows the stakers to migrate their positions - * to the new contract. - * @dev Doesn't have any influence as long as migrateToNewStakingContract - * is not implemented. - * @param _newStakingContract The address of the new staking contract. - * */ - function setNewStakingContract(address _newStakingContract) public onlyOwner { - require(_newStakingContract != address(0), "can't reset the new staking contract to 0"); - newStakingContract = _newStakingContract; - } - - /** - * @notice Allow the owner to set a fee sharing proxy contract. - * We need it for unstaking with slashing. - * @param _feeSharing The address of FeeSharingProxy contract. - * */ - function setFeeSharing(address _feeSharing) public onlyOwner { - require(_feeSharing != address(0), "FeeSharing address shouldn't be 0"); - feeSharing = IFeeSharingProxy(_feeSharing); - } - - /** - * @notice Allow the owner to set weight scaling. - * We need it for unstaking with slashing. - * @param _weightScaling The weight scaling. - * */ - function setWeightScaling(uint96 _weightScaling) public onlyOwner { - require( - MIN_WEIGHT_SCALING <= _weightScaling && _weightScaling <= MAX_WEIGHT_SCALING, - "weight scaling doesn't belong to range [1, 9]" - ); - weightScaling = _weightScaling; - } - - /** - * @notice Allow a staker to migrate his positions to the new staking contract. - * @dev Staking contract needs to be set before by the owner. - * Currently not implemented, just needed for the interface. - * In case it's needed at some point in the future, - * the implementation needs to be changed first. - * */ - function migrateToNewStakingContract() public { - require(newStakingContract != address(0), "there is no new staking contract set"); - /// @dev implementation: - /// @dev Iterate over all possible lock dates from now until now + MAX_DURATION. - /// @dev Read the stake & delegate of the msg.sender - /// @dev If stake > 0, stake it at the new contract until the lock date with the current delegate. - } - - /** - * @notice Allow the owner to unlock all tokens in case the staking contract - * is going to be replaced - * Note: Not reversible on purpose. once unlocked, everything is unlocked. - * The owner should not be able to just quickly unlock to withdraw his own - * tokens and lock again. - * @dev Last resort. - * */ - function unlockAllTokens() public onlyOwner { - allUnlocked = true; - emit TokensUnlocked(SOVToken.balanceOf(address(this))); - } - - /** - * @notice Get list of stakes for a user account. - * @param account The address to get stakes. - * @return The arrays of dates and stakes. - * */ - function getStakes(address account) public view returns (uint256[] memory dates, uint96[] memory stakes) { - uint256 latest = timestampToLockDate(block.timestamp + MAX_DURATION); - - /// @dev Calculate stakes. - uint256 count = 0; - /// @dev We need to iterate from first possible stake date after deployment to the latest from current time. - for (uint256 i = kickoffTS + TWO_WEEKS; i <= latest; i += TWO_WEEKS) { - if (currentBalance(account, i) > 0) { - count++; - } - } - dates = new uint256[](count); - stakes = new uint96[](count); - - /// @dev We need to iterate from first possible stake date after deployment to the latest from current time. - uint256 j = 0; - for (uint256 i = kickoffTS + TWO_WEEKS; i <= latest; i += TWO_WEEKS) { - uint96 balance = currentBalance(account, i); - if (balance > 0) { - dates[j] = i; - stakes[j] = balance; - j++; - } - } - } - - /** - * @notice Overrides default ApprovalReceiver._getToken function to - * register SOV token on this contract. - * @return The address of SOV token. - * */ - function _getToken() internal view returns (address) { - return address(SOVToken); - } - - /** - * @notice Overrides default ApprovalReceiver._getSelectors function to - * register stakeWithApproval selector on this contract. - * @return The array of registered selectors on this contract. - * */ - function _getSelectors() internal view returns (bytes4[] memory) { - bytes4[] memory selectors = new bytes4[](1); - selectors[0] = this.stakeWithApproval.selector; - return selectors; - } + using SafeMath for uint256; + + /// @notice Constant used for computing the vesting dates. + uint256 constant FOUR_WEEKS = 4 weeks; + + /** + * @notice Stake the given amount for the given duration of time. + * @param amount The number of tokens to stake. + * @param until Timestamp indicating the date until which to stake. + * @param stakeFor The address to stake the tokens for or 0x0 if staking for oneself. + * @param delegatee The address of the delegatee or 0x0 if there is none. + * */ + function stake( + uint96 amount, + uint256 until, + address stakeFor, + address delegatee + ) external { + _stake(msg.sender, amount, until, stakeFor, delegatee, false); + } + + /** + * @notice Stake the given amount for the given duration of time. + * @dev This function will be invoked from receiveApproval + * @dev SOV.approveAndCall -> this.receiveApproval -> this.stakeWithApproval + * @param sender The sender of SOV.approveAndCall + * @param amount The number of tokens to stake. + * @param until Timestamp indicating the date until which to stake. + * @param stakeFor The address to stake the tokens for or 0x0 if staking for oneself. + * @param delegatee The address of the delegatee or 0x0 if there is none. + * */ + function stakeWithApproval( + address sender, + uint96 amount, + uint256 until, + address stakeFor, + address delegatee + ) public onlyThisContract { + _stake(sender, amount, until, stakeFor, delegatee, false); + } + + /** + * @notice Send sender's tokens to this contract and update its staked balance. + * @param sender The sender of the tokens. + * @param amount The number of tokens to send. + * @param until The date until which the tokens will be staked. + * @param stakeFor The beneficiary whose stake will be increased. + * @param delegatee The address of the delegatee or stakeFor if default 0x0. + * @param timeAdjusted Whether fixing date to stacking periods or not. + * */ + function _stake( + address sender, + uint96 amount, + uint256 until, + address stakeFor, + address delegatee, + bool timeAdjusted + ) internal { + require(amount > 0, "amount needs to be bigger than 0"); + + if (!timeAdjusted) { + until = timestampToLockDate(until); + } + require(until > block.timestamp, "Staking::timestampToLockDate: staking period too short"); + + /// @dev Stake for the sender if not specified otherwise. + if (stakeFor == address(0)) { + stakeFor = sender; + } + + /// @dev Delegate for stakeFor if not specified otherwise. + if (delegatee == address(0)) { + delegatee = stakeFor; + } + + /// @dev Do not stake longer than the max duration. + if (!timeAdjusted) { + uint256 latest = timestampToLockDate(block.timestamp + MAX_DURATION); + if (until > latest) until = latest; + } + + uint96 previousBalance = currentBalance(stakeFor, until); + + /// @dev Increase stake. + _increaseStake(sender, amount, stakeFor, until); + + // @dev Previous version wasn't working properly for the following case: + // delegate checkpoint wasn't updating for the second and next stakes for the same date + // if first stake was withdrawn completely and stake was delegated to the staker + // (no delegation to another address). + address previousDelegatee = delegates[stakeFor][until]; + if (previousDelegatee != delegatee) { + /// @dev Update delegatee. + delegates[stakeFor][until] = delegatee; + + /// @dev Decrease stake on previous balance for previous delegatee. + _decreaseDelegateStake(previousDelegatee, until, previousBalance); + + /// @dev Add previousBalance to amount. + amount = add96(previousBalance, amount, "balance overflow"); + } + + /// @dev Increase stake. + _increaseDelegateStake(delegatee, until, amount); + emit DelegateChanged(stakeFor, until, previousDelegatee, delegatee); + } + + /** + * @notice Extend the staking duration until the specified date. + * @param previousLock The old unlocking timestamp. + * @param until The new unlocking timestamp in seconds. + * */ + function extendStakingDuration(uint256 previousLock, uint256 until) public { + until = timestampToLockDate(until); + require(previousLock <= until, "cannot reduce the staking duration"); + + /// @dev Do not exceed the max duration, no overflow possible. + uint256 latest = timestampToLockDate(block.timestamp + MAX_DURATION); + if (until > latest) until = latest; + + /// @dev Update checkpoints. + /// @dev TODO James: Can reading stake at block.number -1 cause trouble with multiple tx in a block? + uint96 amount = _getPriorUserStakeByDate(msg.sender, previousLock, block.number - 1); + require(amount > 0, "nothing staked until the previous lock date"); + _decreaseUserStake(msg.sender, previousLock, amount); + _increaseUserStake(msg.sender, until, amount); + + if (isVestingContract(msg.sender)) { + _decreaseVestingStake(previousLock, amount); + _increaseVestingStake(until, amount); + } + + _decreaseDailyStake(previousLock, amount); + _increaseDailyStake(until, amount); + + /// @dev Delegate might change: if there is already a delegate set for the until date, it will remain the delegate for this position + address delegateFrom = delegates[msg.sender][previousLock]; + address delegateTo = delegates[msg.sender][until]; + if (delegateTo == address(0)) { + delegateTo = delegateFrom; + delegates[msg.sender][until] = delegateFrom; + } + delegates[msg.sender][previousLock] = address(0); + _decreaseDelegateStake(delegateFrom, previousLock, amount); + _increaseDelegateStake(delegateTo, until, amount); + + emit ExtendedStakingDuration(msg.sender, previousLock, until, amount); + } + + /** + * @notice Send sender's tokens to this contract and update its staked balance. + * @param sender The sender of the tokens. + * @param amount The number of tokens to send. + * @param stakeFor The beneficiary whose stake will be increased. + * @param until The date until which the tokens will be staked. + * */ + function _increaseStake( + address sender, + uint96 amount, + address stakeFor, + uint256 until + ) internal { + /// @dev Retrieve the SOV tokens. + bool success = SOVToken.transferFrom(sender, address(this), amount); + require(success); + + /// @dev Increase staked balance. + uint96 balance = currentBalance(stakeFor, until); + balance = add96(balance, amount, "overflow"); + + /// @dev Update checkpoints. + _increaseDailyStake(until, amount); + _increaseUserStake(stakeFor, until, amount); + + if (isVestingContract(stakeFor)) _increaseVestingStake(until, amount); + + emit TokensStaked(stakeFor, amount, until, balance); + } + + /** + * @notice Stake tokens according to the vesting schedule. + * @param amount The amount of tokens to stake. + * @param cliff The time interval to the first withdraw. + * @param duration The staking duration. + * @param intervalLength The length of each staking interval when cliff passed. + * @param stakeFor The address to stake the tokens for or 0x0 if staking for oneself. + * @param delegatee The address of the delegatee or 0x0 if there is none. + * */ + function stakesBySchedule( + uint256 amount, + uint256 cliff, + uint256 duration, + uint256 intervalLength, + address stakeFor, + address delegatee + ) public { + /** + * @dev Stake them until lock dates according to the vesting schedule. + * Note: because staking is only possible in periods of 2 weeks, + * the total duration might end up a bit shorter than specified + * depending on the date of staking. + * */ + uint256 start = timestampToLockDate(block.timestamp + cliff); + if (duration > MAX_DURATION) { + duration = MAX_DURATION; + } + uint256 end = timestampToLockDate(block.timestamp + duration); + uint256 numIntervals = (end - start) / intervalLength + 1; + uint256 stakedPerInterval = amount / numIntervals; + /// @dev stakedPerInterval might lose some dust on rounding. Add it to the first staking date. + if (numIntervals >= 1) { + _stake( + msg.sender, + uint96(amount - stakedPerInterval * (numIntervals - 1)), + start, + stakeFor, + delegatee, + true + ); + } + /// @dev Stake the rest in 4 week intervals. + for (uint256 i = start + intervalLength; i <= end; i += intervalLength) { + /// @dev Stakes for itself, delegates to the owner. + _stake(msg.sender, uint96(stakedPerInterval), i, stakeFor, delegatee, true); + } + } + + /** + * @notice Withdraw the given amount of tokens if they are unlocked. + * @param amount The number of tokens to withdraw. + * @param until The date until which the tokens were staked. + * @param receiver The receiver of the tokens. If not specified, send to the msg.sender + * */ + function withdraw( + uint96 amount, + uint256 until, + address receiver + ) public { + _withdraw(amount, until, receiver, false); + // @dev withdraws tokens for lock date 2 weeks later than given lock date if sender is a contract + // we need to check block.timestamp here + _withdrawNext(amount, until, receiver, false); + } + + /** + * @notice Withdraw the given amount of tokens. + * @param amount The number of tokens to withdraw. + * @param until The date until which the tokens were staked. + * @param receiver The receiver of the tokens. If not specified, send to the msg.sender + * @dev Can be invoked only by whitelisted contract passed to governanceWithdrawVesting + * */ + function governanceWithdraw( + uint96 amount, + uint256 until, + address receiver + ) public { + require(vestingWhitelist[msg.sender], "unauthorized"); + + _withdraw(amount, until, receiver, true); + // @dev withdraws tokens for lock date 2 weeks later than given lock date if sender is a contract + // we don't need to check block.timestamp here + _withdrawNext(amount, until, receiver, true); + } + + /** + * @notice Withdraw tokens for vesting contract. + * @param vesting The address of Vesting contract. + * @param receiver The receiver of the tokens. If not specified, send to the msg.sender + * @dev Can be invoked only by whitelisted contract passed to governanceWithdrawVesting. + * */ + function governanceWithdrawVesting(address vesting, address receiver) public onlyAuthorized { + vestingWhitelist[vesting] = true; + ITeamVesting(vesting).governanceWithdrawTokens(receiver); + vestingWhitelist[vesting] = false; + + emit VestingTokensWithdrawn(vesting, receiver); + } + + /** + * @notice Send user' staked tokens to a receiver taking into account punishments. + * Sovryn encourages long-term commitment and thinking. When/if you unstake before + * the end of the staking period, a percentage of the original staking amount will + * be slashed. This amount is also added to the reward pool and is distributed + * between all other stakers. + * + * @param amount The number of tokens to withdraw. + * @param until The date until which the tokens were staked. + * @param receiver The receiver of the tokens. If not specified, send to the msg.sender + * @param isGovernance Whether all tokens (true) + * or just unlocked tokens (false). + * */ + function _withdraw( + uint96 amount, + uint256 until, + address receiver, + bool isGovernance + ) internal { + // @dev it's very unlikely some one will have 1/10**18 SOV staked in Vesting contract + // this check is a part of workaround for Vesting.withdrawTokens issue + if (amount == 1 && isVestingContract(msg.sender)) { + return; + } + until = _adjustDateForOrigin(until); + _validateWithdrawParams(amount, until); + + /// @dev Determine the receiver. + if (receiver == address(0)) receiver = msg.sender; + + /// @dev Update the checkpoints. + _decreaseDailyStake(until, amount); + _decreaseUserStake(msg.sender, until, amount); + if (isVestingContract(msg.sender)) _decreaseVestingStake(until, amount); + _decreaseDelegateStake(delegates[msg.sender][until], until, amount); + + /// @dev Early unstaking should be punished. + if (block.timestamp < until && !allUnlocked && !isGovernance) { + uint96 punishedAmount = _getPunishedAmount(amount, until); + amount -= punishedAmount; + + /// @dev punishedAmount can be 0 if block.timestamp are very close to 'until' + if (punishedAmount > 0) { + require( + address(feeSharing) != address(0), + "Staking::withdraw: FeeSharing address wasn't set" + ); + /// @dev Move punished amount to fee sharing. + /// @dev Approve transfer here and let feeSharing do transfer and write checkpoint. + SOVToken.approve(address(feeSharing), punishedAmount); + feeSharing.transferTokens(address(SOVToken), punishedAmount); + } + } + + /// @dev transferFrom + bool success = SOVToken.transfer(receiver, amount); + require(success, "Staking::withdraw: Token transfer failed"); + + emit StakingWithdrawn(msg.sender, amount, until, receiver, isGovernance); + } + + // @dev withdraws tokens for lock date 2 weeks later than given lock date + function _withdrawNext( + uint96 amount, + uint256 until, + address receiver, + bool isGovernance + ) internal { + if (isVestingContract(msg.sender)) { + uint256 nextLock = until.add(TWO_WEEKS); + if (isGovernance || block.timestamp >= nextLock) { + uint96 stakes = _getPriorUserStakeByDate(msg.sender, nextLock, block.number - 1); + if (stakes > 0) { + _withdraw(stakes, nextLock, receiver, isGovernance); + } + } + } + } + + /** + * @notice Get available and punished amount for withdrawing. + * @param amount The number of tokens to withdraw. + * @param until The date until which the tokens were staked. + * */ + function getWithdrawAmounts(uint96 amount, uint256 until) + public + view + returns (uint96, uint96) + { + _validateWithdrawParams(amount, until); + uint96 punishedAmount = _getPunishedAmount(amount, until); + return (amount - punishedAmount, punishedAmount); + } + + /** + * @notice Get punished amount for withdrawing. + * @param amount The number of tokens to withdraw. + * @param until The date until which the tokens were staked. + * */ + function _getPunishedAmount(uint96 amount, uint256 until) internal view returns (uint96) { + uint256 date = timestampToLockDate(block.timestamp); + uint96 weight = computeWeightByDate(until, date); /// @dev (10 - 1) * WEIGHT_FACTOR + weight = weight * weightScaling; + return (amount * weight) / WEIGHT_FACTOR / 100; + } + + /** + * @notice Validate withdraw parameters. + * @param amount The number of tokens to withdraw. + * @param until The date until which the tokens were staked. + * */ + function _validateWithdrawParams(uint96 amount, uint256 until) internal view { + require( + amount > 0, + "Staking::withdraw: amount of tokens to be withdrawn needs to be bigger than 0" + ); + uint96 balance = _getPriorUserStakeByDate(msg.sender, until, block.number - 1); + require(amount <= balance, "Staking::withdraw: not enough balance"); + } + + /** + * @notice Get the current balance of an account locked until a certain date. + * @param account The user address. + * @param lockDate The lock date. + * @return The stake amount. + * */ + function currentBalance(address account, uint256 lockDate) internal view returns (uint96) { + return + userStakingCheckpoints[account][lockDate][ + numUserStakingCheckpoints[account][lockDate] - 1 + ] + .stake; + } + + /** + * @notice Get the number of staked tokens held by the user account. + * @dev Iterate checkpoints adding up stakes. + * @param account The address of the account to get the balance of. + * @return The number of tokens held. + * */ + function balanceOf(address account) public view returns (uint96 balance) { + for (uint256 i = kickoffTS; i <= block.timestamp + MAX_DURATION; i += TWO_WEEKS) { + balance = add96(balance, currentBalance(account, i), "Staking::balanceOf: overflow"); + } + } + + /** + * @notice Delegate votes from `msg.sender` which are locked until lockDate to `delegatee`. + * @param delegatee The address to delegate votes to. + * @param lockDate the date if the position to delegate. + * */ + function delegate(address delegatee, uint256 lockDate) public { + _delegate(msg.sender, delegatee, lockDate); + // @dev delegates tokens for lock date 2 weeks later than given lock date + // if message sender is a contract + _delegateNext(msg.sender, delegatee, lockDate); + } + + /** + * @notice Delegates votes from signatory to a delegatee account. + * Voting with EIP-712 Signatures. + * + * Voting power can be delegated to any address, and then can be used to + * vote on proposals. A key benefit to users of by-signature functionality + * is that they can create a signed vote transaction for free, and have a + * trusted third-party spend rBTC(or ETH) on gas fees and write it to the + * blockchain for them. + * + * The third party in this scenario, submitting the SOV-holder’s signed + * transaction holds a voting power that is for only a single proposal. + * The signatory still holds the power to vote on their own behalf in + * the proposal if the third party has not yet published the signed + * transaction that was given to them. + * + * @dev The signature needs to be broken up into 3 parameters, known as + * v, r and s: + * const r = '0x' + sig.substring(2).substring(0, 64); + * const s = '0x' + sig.substring(2).substring(64, 128); + * const v = '0x' + sig.substring(2).substring(128, 130); + * + * @param delegatee The address to delegate votes to. + * @param lockDate The date until which the position is locked. + * @param nonce The contract state required to match the signature. + * @param expiry The time at which to expire the signature. + * @param v The recovery byte of the signature. + * @param r Half of the ECDSA signature pair. + * @param s Half of the ECDSA signature pair. + * */ + function delegateBySig( + address delegatee, + uint256 lockDate, + uint256 nonce, + uint256 expiry, + uint8 v, + bytes32 r, + bytes32 s + ) public { + /** + * @dev The DOMAIN_SEPARATOR is a hash that uniquely identifies a + * smart contract. It is built from a string denoting it as an + * EIP712 Domain, the name of the token contract, the version, + * the chainId in case it changes, and the address that the + * contract is deployed at. + * */ + bytes32 domainSeparator = + keccak256( + abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(name)), getChainId(), address(this)) + ); + + /// @dev GovernorAlpha uses BALLOT_TYPEHASH, while Staking uses DELEGATION_TYPEHASH + bytes32 structHash = + keccak256(abi.encode(DELEGATION_TYPEHASH, delegatee, lockDate, nonce, expiry)); + + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + address signatory = ecrecover(digest, v, r, s); + + /// @dev Verify address is not null and PK is not null either. + require( + RSKAddrValidator.checkPKNotZero(signatory), + "Staking::delegateBySig: invalid signature" + ); + require(nonce == nonces[signatory]++, "Staking::delegateBySig: invalid nonce"); + require(now <= expiry, "Staking::delegateBySig: signature expired"); + _delegate(signatory, delegatee, lockDate); + // @dev delegates tokens for lock date 2 weeks later than given lock date + // if message sender is a contract + _delegateNext(signatory, delegatee, lockDate); + } + + /** + * @notice Get the current votes balance for a user account. + * @param account The address to get votes balance. + * @dev This is a wrapper to simplify arguments. The actual computation is + * performed on WeightedStaking parent contract. + * @return The number of current votes for a user account. + * */ + function getCurrentVotes(address account) external view returns (uint96) { + return getPriorVotes(account, block.number - 1, block.timestamp); + } + + /** + * @notice Get the current number of tokens staked for a day. + * @param lockedTS The timestamp to get the staked tokens for. + * */ + function getCurrentStakedUntil(uint256 lockedTS) external view returns (uint96) { + uint32 nCheckpoints = numTotalStakingCheckpoints[lockedTS]; + return nCheckpoints > 0 ? totalStakingCheckpoints[lockedTS][nCheckpoints - 1].stake : 0; + } + + /** + * @notice Set new delegatee. Move from user's current delegate to a new + * delegatee the stake balance. + * @param delegator The user address to move stake balance from its current delegatee. + * @param delegatee The new delegatee. The address to move stake balance to. + * @param lockedTS The lock date. + * */ + function _delegate( + address delegator, + address delegatee, + uint256 lockedTS + ) internal { + address currentDelegate = delegates[delegator][lockedTS]; + uint96 delegatorBalance = currentBalance(delegator, lockedTS); + delegates[delegator][lockedTS] = delegatee; + + emit DelegateChanged(delegator, lockedTS, currentDelegate, delegatee); + + _moveDelegates(currentDelegate, delegatee, delegatorBalance, lockedTS); + } + + // @dev delegates tokens for lock date 2 weeks later than given lock date + // if message sender is a contract + function _delegateNext( + address delegator, + address delegatee, + uint256 lockedTS + ) internal { + if (isVestingContract(msg.sender)) { + uint256 nextLock = lockedTS.add(TWO_WEEKS); + address currentDelegate = delegates[delegator][nextLock]; + if (currentDelegate != delegatee) { + _delegate(delegator, delegatee, nextLock); + } + + // @dev workaround for the issue with a delegation of the latest stake + uint256 endDate = IVesting(msg.sender).endDate(); + nextLock = lockedTS.add(FOUR_WEEKS); + if (nextLock == endDate) { + currentDelegate = delegates[delegator][nextLock]; + if (currentDelegate != delegatee) { + _delegate(delegator, delegatee, nextLock); + } + } + } + } + + /** + * @notice Move an amount of delegate stake from a source address to a + * destination address. + * @param srcRep The address to get the staked amount from. + * @param dstRep The address to send the staked amount to. + * @param amount The staked amount to move. + * @param lockedTS The lock date. + * */ + function _moveDelegates( + address srcRep, + address dstRep, + uint96 amount, + uint256 lockedTS + ) internal { + if (srcRep != dstRep && amount > 0) { + if (srcRep != address(0)) _decreaseDelegateStake(srcRep, lockedTS, amount); + + if (dstRep != address(0)) _increaseDelegateStake(dstRep, lockedTS, amount); + } + } + + /** + * @notice Retrieve CHAIN_ID of the executing chain. + * + * Chain identifier (chainID) introduced in EIP-155 protects transaction + * included into one chain from being included into another chain. + * Basically, chain identifier is an integer number being used in the + * processes of signing transactions and verifying transaction signatures. + * + * @dev As of version 0.5.12, Solidity includes an assembly function + * chainid() that provides access to the new CHAINID opcode. + * + * TODO: chainId is included in block. So you can get chain id like + * block timestamp or block number: block.chainid; + * */ + function getChainId() internal pure returns (uint256) { + uint256 chainId; + assembly { + chainId := chainid() + } + return chainId; + } + + /** + * @notice Allow the owner to set a new staking contract. + * As a consequence it allows the stakers to migrate their positions + * to the new contract. + * @dev Doesn't have any influence as long as migrateToNewStakingContract + * is not implemented. + * @param _newStakingContract The address of the new staking contract. + * */ + function setNewStakingContract(address _newStakingContract) public onlyOwner { + require(_newStakingContract != address(0), "can't reset the new staking contract to 0"); + newStakingContract = _newStakingContract; + } + + /** + * @notice Allow the owner to set a fee sharing proxy contract. + * We need it for unstaking with slashing. + * @param _feeSharing The address of FeeSharingProxy contract. + * */ + function setFeeSharing(address _feeSharing) public onlyOwner { + require(_feeSharing != address(0), "FeeSharing address shouldn't be 0"); + feeSharing = IFeeSharingProxy(_feeSharing); + } + + /** + * @notice Allow the owner to set weight scaling. + * We need it for unstaking with slashing. + * @param _weightScaling The weight scaling. + * */ + function setWeightScaling(uint96 _weightScaling) public onlyOwner { + require( + MIN_WEIGHT_SCALING <= _weightScaling && _weightScaling <= MAX_WEIGHT_SCALING, + "weight scaling doesn't belong to range [1, 9]" + ); + weightScaling = _weightScaling; + } + + /** + * @notice Allow a staker to migrate his positions to the new staking contract. + * @dev Staking contract needs to be set before by the owner. + * Currently not implemented, just needed for the interface. + * In case it's needed at some point in the future, + * the implementation needs to be changed first. + * */ + function migrateToNewStakingContract() public { + require(newStakingContract != address(0), "there is no new staking contract set"); + /// @dev implementation: + /// @dev Iterate over all possible lock dates from now until now + MAX_DURATION. + /// @dev Read the stake & delegate of the msg.sender + /// @dev If stake > 0, stake it at the new contract until the lock date with the current delegate. + } + + /** + * @notice Allow the owner to unlock all tokens in case the staking contract + * is going to be replaced + * Note: Not reversible on purpose. once unlocked, everything is unlocked. + * The owner should not be able to just quickly unlock to withdraw his own + * tokens and lock again. + * @dev Last resort. + * */ + function unlockAllTokens() public onlyOwner { + allUnlocked = true; + emit TokensUnlocked(SOVToken.balanceOf(address(this))); + } + + /** + * @notice Get list of stakes for a user account. + * @param account The address to get stakes. + * @return The arrays of dates and stakes. + * */ + function getStakes(address account) + public + view + returns (uint256[] memory dates, uint96[] memory stakes) + { + uint256 latest = timestampToLockDate(block.timestamp + MAX_DURATION); + + /// @dev Calculate stakes. + uint256 count = 0; + /// @dev We need to iterate from first possible stake date after deployment to the latest from current time. + for (uint256 i = kickoffTS + TWO_WEEKS; i <= latest; i += TWO_WEEKS) { + if (currentBalance(account, i) > 0) { + count++; + } + } + dates = new uint256[](count); + stakes = new uint96[](count); + + /// @dev We need to iterate from first possible stake date after deployment to the latest from current time. + uint256 j = 0; + for (uint256 i = kickoffTS + TWO_WEEKS; i <= latest; i += TWO_WEEKS) { + uint96 balance = currentBalance(account, i); + if (balance > 0) { + dates[j] = i; + stakes[j] = balance; + j++; + } + } + } + + /** + * @notice Overrides default ApprovalReceiver._getToken function to + * register SOV token on this contract. + * @return The address of SOV token. + * */ + function _getToken() internal view returns (address) { + return address(SOVToken); + } + + /** + * @notice Overrides default ApprovalReceiver._getSelectors function to + * register stakeWithApproval selector on this contract. + * @return The array of registered selectors on this contract. + * */ + function _getSelectors() internal view returns (bytes4[] memory) { + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = this.stakeWithApproval.selector; + return selectors; + } } diff --git a/contracts/governance/Staking/StakingProxy.sol b/contracts/governance/Staking/StakingProxy.sol index f9296a453..23d881105 100644 --- a/contracts/governance/Staking/StakingProxy.sol +++ b/contracts/governance/Staking/StakingProxy.sol @@ -11,12 +11,12 @@ import "../../proxy/UpgradableProxy.sol"; * the possibility of being enhanced and re-deployed. * */ contract StakingProxy is StakingStorage, UpgradableProxy { - /** - * @notice Construct a new staking contract. - * @param SOV The address of the SOV token address. - */ - constructor(address SOV) public { - SOVToken = IERC20(SOV); - kickoffTS = block.timestamp; - } + /** + * @notice Construct a new staking contract. + * @param SOV The address of the SOV token address. + */ + constructor(address SOV) public { + SOVToken = IERC20(SOV); + kickoffTS = block.timestamp; + } } diff --git a/contracts/governance/Staking/StakingStorage.sol b/contracts/governance/Staking/StakingStorage.sol index 60d69a2a3..35b391489 100644 --- a/contracts/governance/Staking/StakingStorage.sol +++ b/contracts/governance/Staking/StakingStorage.sol @@ -22,118 +22,122 @@ import "../Vesting/VestingRegistryLogic.sol"; * fee and slashing rewards. * */ contract StakingStorage is Ownable { - /// @notice 2 weeks in seconds. - uint256 constant TWO_WEEKS = 1209600; + /// @notice 2 weeks in seconds. + uint256 constant TWO_WEEKS = 1209600; - /// @notice The maximum possible voting weight before adding +1 (actually 10, but need 9 for computation). - uint96 public constant MAX_VOTING_WEIGHT = 9; + /// @notice The maximum possible voting weight before adding +1 (actually 10, but need 9 for computation). + uint96 public constant MAX_VOTING_WEIGHT = 9; - /// @notice weight is multiplied with this factor (for allowing decimals, like 1.2x). - /// @dev MAX_VOTING_WEIGHT * WEIGHT_FACTOR needs to be < 792, because there are 100,000,000 SOV with 18 decimals - uint96 public constant WEIGHT_FACTOR = 10; + /// @notice weight is multiplied with this factor (for allowing decimals, like 1.2x). + /// @dev MAX_VOTING_WEIGHT * WEIGHT_FACTOR needs to be < 792, because there are 100,000,000 SOV with 18 decimals + uint96 public constant WEIGHT_FACTOR = 10; - /// @notice The maximum duration to stake tokens for. - uint256 public constant MAX_DURATION = 1092 days; + /// @notice The maximum duration to stake tokens for. + uint256 public constant MAX_DURATION = 1092 days; - /// @notice The maximum duration ^2 - uint96 constant MAX_DURATION_POW_2 = 1092 * 1092; + /// @notice The maximum duration ^2 + uint96 constant MAX_DURATION_POW_2 = 1092 * 1092; - /// @notice Default weight scaling. - uint96 constant DEFAULT_WEIGHT_SCALING = 3; + /// @notice Default weight scaling. + uint96 constant DEFAULT_WEIGHT_SCALING = 3; - /// @notice Range for weight scaling. - uint96 constant MIN_WEIGHT_SCALING = 1; - uint96 constant MAX_WEIGHT_SCALING = 9; + /// @notice Range for weight scaling. + uint96 constant MIN_WEIGHT_SCALING = 1; + uint96 constant MAX_WEIGHT_SCALING = 9; - /// @notice The timestamp of contract creation. Base for the staking period calculation. - uint256 public kickoffTS; + /// @notice The timestamp of contract creation. Base for the staking period calculation. + uint256 public kickoffTS; - string name = "SOVStaking"; + string name = "SOVStaking"; - /// @notice The token to be staked. - IERC20 public SOVToken; + /// @notice The token to be staked. + IERC20 public SOVToken; - /// @notice A record of each accounts delegate. - mapping(address => mapping(uint256 => address)) public delegates; + /// @notice A record of each accounts delegate. + mapping(address => mapping(uint256 => address)) public delegates; - /// @notice If this flag is set to true, all tokens are unlocked immediately. - bool public allUnlocked = false; + /// @notice If this flag is set to true, all tokens are unlocked immediately. + bool public allUnlocked = false; - /// @notice The EIP-712 typehash for the contract's domain. - bytes32 public constant DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"); + /// @notice The EIP-712 typehash for the contract's domain. + bytes32 public constant DOMAIN_TYPEHASH = + keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"); - /// @notice The EIP-712 typehash for the delegation struct used by the contract. - bytes32 public constant DELEGATION_TYPEHASH = keccak256("Delegation(address delegatee,uint256 lockDate,uint256 nonce,uint256 expiry)"); + /// @notice The EIP-712 typehash for the delegation struct used by the contract. + bytes32 public constant DELEGATION_TYPEHASH = + keccak256("Delegation(address delegatee,uint256 lockDate,uint256 nonce,uint256 expiry)"); - /// @notice Used for stake migrations to a new staking contract with a different storage structure. - address public newStakingContract; + /// @notice Used for stake migrations to a new staking contract with a different storage structure. + address public newStakingContract; - /*************************** Checkpoints *******************************/ + /*************************** Checkpoints *******************************/ - /// @notice A checkpoint for marking the stakes from a given block - struct Checkpoint { - uint32 fromBlock; - uint96 stake; - } + /// @notice A checkpoint for marking the stakes from a given block + struct Checkpoint { + uint32 fromBlock; + uint96 stake; + } - /// @notice A record of tokens to be unstaked at a given time in total. - /// For total voting power computation. Voting weights get adjusted bi-weekly. - /// @dev totalStakingCheckpoints[date][index] is a checkpoint. - mapping(uint256 => mapping(uint32 => Checkpoint)) public totalStakingCheckpoints; + /// @notice A record of tokens to be unstaked at a given time in total. + /// For total voting power computation. Voting weights get adjusted bi-weekly. + /// @dev totalStakingCheckpoints[date][index] is a checkpoint. + mapping(uint256 => mapping(uint32 => Checkpoint)) public totalStakingCheckpoints; - /// @notice The number of total staking checkpoints for each date. - /// @dev numTotalStakingCheckpoints[date] is a number. - mapping(uint256 => uint32) public numTotalStakingCheckpoints; + /// @notice The number of total staking checkpoints for each date. + /// @dev numTotalStakingCheckpoints[date] is a number. + mapping(uint256 => uint32) public numTotalStakingCheckpoints; - /// @notice A record of tokens to be unstaked at a given time which were delegated to a certain address. - /// For delegatee voting power computation. Voting weights get adjusted bi-weekly. - /// @dev delegateStakingCheckpoints[delegatee][date][index] is a checkpoint. - mapping(address => mapping(uint256 => mapping(uint32 => Checkpoint))) public delegateStakingCheckpoints; + /// @notice A record of tokens to be unstaked at a given time which were delegated to a certain address. + /// For delegatee voting power computation. Voting weights get adjusted bi-weekly. + /// @dev delegateStakingCheckpoints[delegatee][date][index] is a checkpoint. + mapping(address => mapping(uint256 => mapping(uint32 => Checkpoint))) + public delegateStakingCheckpoints; - /// @notice The number of total staking checkpoints for each date per delegate. - /// @dev numDelegateStakingCheckpoints[delegatee][date] is a number. - mapping(address => mapping(uint256 => uint32)) public numDelegateStakingCheckpoints; + /// @notice The number of total staking checkpoints for each date per delegate. + /// @dev numDelegateStakingCheckpoints[delegatee][date] is a number. + mapping(address => mapping(uint256 => uint32)) public numDelegateStakingCheckpoints; - /// @notice A record of tokens to be unstaked at a given time which per user address (address -> lockDate -> stake checkpoint) - /// @dev userStakingCheckpoints[user][date][index] is a checkpoint. - mapping(address => mapping(uint256 => mapping(uint32 => Checkpoint))) public userStakingCheckpoints; + /// @notice A record of tokens to be unstaked at a given time which per user address (address -> lockDate -> stake checkpoint) + /// @dev userStakingCheckpoints[user][date][index] is a checkpoint. + mapping(address => mapping(uint256 => mapping(uint32 => Checkpoint))) + public userStakingCheckpoints; - /// @notice The number of total staking checkpoints for each date per user. - /// @dev numUserStakingCheckpoints[user][date] is a number. - mapping(address => mapping(uint256 => uint32)) public numUserStakingCheckpoints; + /// @notice The number of total staking checkpoints for each date per user. + /// @dev numUserStakingCheckpoints[user][date] is a number. + mapping(address => mapping(uint256 => uint32)) public numUserStakingCheckpoints; - /// @notice A record of states for signing / validating signatures - /// @dev nonces[user] is a number. - mapping(address => uint256) public nonces; + /// @notice A record of states for signing / validating signatures + /// @dev nonces[user] is a number. + mapping(address => uint256) public nonces; - /*************************** Slashing *******************************/ + /*************************** Slashing *******************************/ - /// @notice the address of FeeSharingProxy contract, we need it for unstaking with slashing. - IFeeSharingProxy public feeSharing; + /// @notice the address of FeeSharingProxy contract, we need it for unstaking with slashing. + IFeeSharingProxy public feeSharing; - /// @notice used for weight scaling when unstaking with slashing. - uint96 public weightScaling = DEFAULT_WEIGHT_SCALING; + /// @notice used for weight scaling when unstaking with slashing. + uint96 public weightScaling = DEFAULT_WEIGHT_SCALING; - /// @notice List of vesting contracts, tokens for these contracts won't be slashed if unstaked by governance. - /// @dev vestingWhitelist[contract] is true/false. - mapping(address => bool) public vestingWhitelist; + /// @notice List of vesting contracts, tokens for these contracts won't be slashed if unstaked by governance. + /// @dev vestingWhitelist[contract] is true/false. + mapping(address => bool) public vestingWhitelist; - /// @dev user => flag whether user has admin role. - /// @dev multisig should be an admin, admin can invoke only governanceWithdrawVesting function, - /// this function works only with Team Vesting contracts - mapping(address => bool) public admins; + /// @dev user => flag whether user has admin role. + /// @dev multisig should be an admin, admin can invoke only governanceWithdrawVesting function, + /// this function works only with Team Vesting contracts + mapping(address => bool) public admins; - /// @dev vesting contract code hash => flag whether it's registered code hash - mapping(bytes32 => bool) public vestingCodeHashes; + /// @dev vesting contract code hash => flag whether it's registered code hash + mapping(bytes32 => bool) public vestingCodeHashes; - /// @notice A record of tokens to be unstaked from vesting contract at a given time (lockDate -> vest checkpoint) - /// @dev vestingCheckpoints[date][index] is a checkpoint. - mapping(uint256 => mapping(uint32 => Checkpoint)) public vestingCheckpoints; + /// @notice A record of tokens to be unstaked from vesting contract at a given time (lockDate -> vest checkpoint) + /// @dev vestingCheckpoints[date][index] is a checkpoint. + mapping(uint256 => mapping(uint32 => Checkpoint)) public vestingCheckpoints; - /// @notice The number of total vesting checkpoints for each date. - /// @dev numVestingCheckpoints[date] is a number. - mapping(uint256 => uint32) public numVestingCheckpoints; + /// @notice The number of total vesting checkpoints for each date. + /// @dev numVestingCheckpoints[date] is a number. + mapping(uint256 => uint32) public numVestingCheckpoints; - ///@notice the vesting registry contract - VestingRegistryLogic public vestingRegistryLogic; + ///@notice the vesting registry contract + VestingRegistryLogic public vestingRegistryLogic; } diff --git a/contracts/governance/Staking/WeightedStaking.sol b/contracts/governance/Staking/WeightedStaking.sol index 4730d8580..dece63bc4 100644 --- a/contracts/governance/Staking/WeightedStaking.sol +++ b/contracts/governance/Staking/WeightedStaking.sol @@ -15,626 +15,662 @@ import "../../openzeppelin/Address.sol"; * FeeSharingProxy and GovernorAlpha invoke Staking instance functions. * */ contract WeightedStaking is Checkpoints { - using Address for address payable; - - /** - * @dev Throws if called by any account other than the owner or admin. - */ - modifier onlyAuthorized() { - require(isOwner() || admins[msg.sender], "unauthorized"); - _; - } - - /** - * @notice sets vesting registry - * @param _vestingRegistryProxy the address of vesting registry proxy contract - * @dev _vestingRegistryProxy can be set to 0 as this function can be reused by - * various other functionalities without the necessity of linking it with Vesting Registry - */ - function setVestingRegistry(address _vestingRegistryProxy) external onlyOwner { - vestingRegistryLogic = VestingRegistryLogic(_vestingRegistryProxy); - } - - /** - * @notice Sets the users' vesting stakes for a giving lock dates and writes checkpoints. - * @param lockedDates The arrays of lock dates. - * @param values The array of values to add to the staked balance. - */ - function setVestingStakes(uint256[] calldata lockedDates, uint96[] calldata values) external onlyAuthorized { - require(lockedDates.length == values.length, "arrays mismatch"); - - uint256 length = lockedDates.length; - for (uint256 i = 0; i < length; i++) { - _setVestingStake(lockedDates[i], values[i]); - } - } - - /** - * @notice Sets the users' vesting stake for a giving lock date and writes a checkpoint. - * @param lockedTS The lock date. - * @param value The value to be set. - */ - function _setVestingStake(uint256 lockedTS, uint96 value) internal { - //delete all checkpoints (shouldn't be any during the first initialization) - uint32 nCheckpoints = numVestingCheckpoints[lockedTS]; - for (uint32 i = 0; i < nCheckpoints; i++) { - delete vestingCheckpoints[lockedTS][i]; - } - delete numVestingCheckpoints[lockedTS]; - - //blockNumber should be in the past - nCheckpoints = 0; - uint32 blockNumber = 0; - vestingCheckpoints[lockedTS][nCheckpoints] = Checkpoint(blockNumber, value); - numVestingCheckpoints[lockedTS] = nCheckpoints + 1; - - emit VestingStakeSet(lockedTS, value); - } - - /************* TOTAL VOTING POWER COMPUTATION ************************/ - - /** - * @notice Compute the total voting power at a given time. - * @param blockNumber The block number, needed for checkpointing. - * @param time The timestamp for which to calculate the total voting power. - * @return The total voting power at the given time. - * */ - function getPriorTotalVotingPower(uint32 blockNumber, uint256 time) public view returns (uint96 totalVotingPower) { - /// @dev Start the computation with the exact or previous unlocking date (voting weight remians the same until the next break point). - uint256 start = timestampToLockDate(time); - uint256 end = start + MAX_DURATION; - - /// @dev Max 78 iterations. - for (uint256 i = start; i <= end; i += TWO_WEEKS) { - totalVotingPower = add96( - totalVotingPower, - _totalPowerByDate(i, start, blockNumber), - "overflow on total voting power computation" - ); - } - } - - /** - * @notice Compute the voting power for a specific date. - * Power = stake * weight - * @param date The staking date to compute the power for. - * @param startDate The date for which we need to know the power of the stake. - * @param blockNumber The block number, needed for checkpointing. - * @return The stacking power. - * */ - function _totalPowerByDate( - uint256 date, - uint256 startDate, - uint256 blockNumber - ) internal view returns (uint96 power) { - uint96 weight = computeWeightByDate(date, startDate); - uint96 staked = getPriorTotalStakesForDate(date, blockNumber); - /// @dev weight is multiplied by some factor to allow decimals. - power = mul96(staked, weight, "multiplication overflow") / WEIGHT_FACTOR; - } - - /** - * @notice Determine the prior number of stake for an unlocking date as of a block number. - * @dev Block number must be a finalized block or else this function will - * revert to prevent misinformation. - * TODO: WeightedStaking::getPriorTotalStakesForDate should probably better - * be internal instead of a public function. - * @param date The date to check the stakes for. - * @param blockNumber The block number to get the vote balance at. - * @return The number of votes the account had as of the given block. - * */ - function getPriorTotalStakesForDate(uint256 date, uint256 blockNumber) public view returns (uint96) { - require(blockNumber < _getCurrentBlockNumber(), "not yet determined"); - - uint32 nCheckpoints = numTotalStakingCheckpoints[date]; - if (nCheckpoints == 0) { - return 0; - } - - // First check most recent balance - if (totalStakingCheckpoints[date][nCheckpoints - 1].fromBlock <= blockNumber) { - return totalStakingCheckpoints[date][nCheckpoints - 1].stake; - } - - // Next check implicit zero balance - if (totalStakingCheckpoints[date][0].fromBlock > blockNumber) { - return 0; - } - - uint32 lower = 0; - uint32 upper = nCheckpoints - 1; - while (upper > lower) { - uint32 center = upper - (upper - lower) / 2; // ceil, avoiding overflow - Checkpoint memory cp = totalStakingCheckpoints[date][center]; - if (cp.fromBlock == blockNumber) { - return cp.stake; - } else if (cp.fromBlock < blockNumber) { - lower = center; - } else { - upper = center - 1; - } - } - return totalStakingCheckpoints[date][lower].stake; - } - - /****************************** DELEGATED VOTING POWER COMPUTATION ************************/ - - /** - * @notice Determine the prior number of votes for a delegatee as of a block number. - * Iterate through checkpoints adding up voting power. - * @dev Block number must be a finalized block or else this function will revert - * to prevent misinformation. - * Used for Voting, not for fee sharing. - * @param account The address of the account to check. - * @param blockNumber The block number to get the vote balance at. - * @param date The staking date to compute the power for. - * @return The number of votes the delegatee had as of the given block. - * */ - function getPriorVotes( - address account, - uint256 blockNumber, - uint256 date - ) public view returns (uint96 votes) { - /// @dev If date is not an exact break point, start weight computation from the previous break point (alternative would be the next). - uint256 start = timestampToLockDate(date); - uint256 end = start + MAX_DURATION; - - /// @dev Max 78 iterations. - for (uint256 i = start; i <= end; i += TWO_WEEKS) { - votes = add96( - votes, - _totalPowerByDateForDelegatee(account, i, start, blockNumber), - "overflow - total voting power computation" - ); - } - } - - /** - * @notice Compute the voting power for a specific date. - * Power = stake * weight - * @param account The address of the account to check. - * @param date The staking date to compute the power for. - * @param startDate The date for which we need to know the power of the stake. - * @param blockNumber The block number, needed for checkpointing. - * @return The stacking power. - * */ - function _totalPowerByDateForDelegatee( - address account, - uint256 date, - uint256 startDate, - uint256 blockNumber - ) internal view returns (uint96 power) { - uint96 weight = computeWeightByDate(date, startDate); - uint96 staked = getPriorStakeByDateForDelegatee(account, date, blockNumber); - power = mul96(staked, weight, "overflow") / WEIGHT_FACTOR; - } - - /** - * @notice Determine the prior number of stake for an account as of a block number. - * @dev Block number must be a finalized block or else this function will - * revert to prevent misinformation. - * TODO: WeightedStaking::getPriorStakeByDateForDelegatee should probably better - * be internal instead of a public function. - * @param account The address of the account to check. - * @param date The staking date to compute the power for. - * @param blockNumber The block number to get the vote balance at. - * @return The number of votes the account had as of the given block. - * */ - function getPriorStakeByDateForDelegatee( - address account, - uint256 date, - uint256 blockNumber - ) public view returns (uint96) { - require(blockNumber < _getCurrentBlockNumber(), "not determined yet"); - - uint32 nCheckpoints = numDelegateStakingCheckpoints[account][date]; - if (nCheckpoints == 0) { - return 0; - } - - /// @dev First check most recent balance. - if (delegateStakingCheckpoints[account][date][nCheckpoints - 1].fromBlock <= blockNumber) { - return delegateStakingCheckpoints[account][date][nCheckpoints - 1].stake; - } - - /// @dev Next check implicit zero balance. - if (delegateStakingCheckpoints[account][date][0].fromBlock > blockNumber) { - return 0; - } - - uint32 lower = 0; - uint32 upper = nCheckpoints - 1; - while (upper > lower) { - uint32 center = upper - (upper - lower) / 2; /// @dev ceil, avoiding overflow. - Checkpoint memory cp = delegateStakingCheckpoints[account][date][center]; - if (cp.fromBlock == blockNumber) { - return cp.stake; - } else if (cp.fromBlock < blockNumber) { - lower = center; - } else { - upper = center - 1; - } - } - return delegateStakingCheckpoints[account][date][lower].stake; - } - - /*************************** User Weighted Stake computation for fee sharing *******************************/ - - /** - * @notice Determine the prior weighted stake for an account as of a block number. - * Iterate through checkpoints adding up voting power. - * @dev Block number must be a finalized block or else this function will - * revert to prevent misinformation. - * Used for fee sharing, not voting. - * TODO: WeightedStaking::getPriorWeightedStake is using the variable name "votes" - * to add up token stake, and that could be misleading. - * - * @param account The address of the account to check. - * @param blockNumber The block number to get the vote balance at. - * @param date The date/timestamp of the unstaking time. - * @return The weighted stake the account had as of the given block. - * */ - function getPriorWeightedStake( - address account, - uint256 blockNumber, - uint256 date - ) public view returns (uint96 votes) { - /// @dev If date is not an exact break point, start weight computation from the previous break point (alternative would be the next). - uint256 start = timestampToLockDate(date); - uint256 end = start + MAX_DURATION; - - /// @dev Max 78 iterations. - for (uint256 i = start; i <= end; i += TWO_WEEKS) { - uint96 weightedStake = weightedStakeByDate(account, i, start, blockNumber); - if (weightedStake > 0) { - votes = add96(votes, weightedStake, "overflow on total weight computation"); - } - } - } - - /** - * @notice Compute the voting power for a specific date. - * Power = stake * weight - * TODO: WeightedStaking::weightedStakeByDate should probably better - * be internal instead of a public function. - * @param account The user address. - * @param date The staking date to compute the power for. - * @param startDate The date for which we need to know the power of the stake. - * @param blockNumber The block number, needed for checkpointing. - * @return The stacking power. - * */ - function weightedStakeByDate( - address account, - uint256 date, - uint256 startDate, - uint256 blockNumber - ) public view returns (uint96 power) { - uint96 staked = _getPriorUserStakeByDate(account, date, blockNumber); - if (staked > 0) { - uint96 weight = computeWeightByDate(date, startDate); - power = mul96(staked, weight, "overflow error") / WEIGHT_FACTOR; - } else { - power = 0; - } - } - - /** - * @notice Determine the prior number of stake for an account until a - * certain lock date as of a block number. - * @dev Block number must be a finalized block or else this function - * will revert to prevent misinformation. - * @param account The address of the account to check. - * @param date The lock date. - * @param blockNumber The block number to get the vote balance at. - * @return The number of votes the account had as of the given block. - * */ - function getPriorUserStakeByDate( - address account, - uint256 date, - uint256 blockNumber - ) external view returns (uint96) { - uint96 priorStake = _getPriorUserStakeByDate(account, date, blockNumber); - // @dev we need to modify function in order to workaround issue with Vesting.withdrawTokens: - // return 1 instead of 0 if message sender is a contract. - if (priorStake == 0 && isVestingContract(msg.sender)) { - priorStake = 1; - } - return priorStake; - } - - /** - * @notice Determine the prior number of stake for an account until a - * certain lock date as of a block number. - * @dev All functions of Staking contract use this internal version, - * we need to modify public function in order to workaround issue with Vesting.withdrawTokens: - * return 1 instead of 0 if message sender is a contract. - * @param account The address of the account to check. - * @param date The lock date. - * @param blockNumber The block number to get the vote balance at. - * @return The number of votes the account had as of the given block. - * */ - function _getPriorUserStakeByDate( - address account, - uint256 date, - uint256 blockNumber - ) internal view returns (uint96) { - require(blockNumber < _getCurrentBlockNumber(), "not determined"); - - date = _adjustDateForOrigin(date); - uint32 nCheckpoints = numUserStakingCheckpoints[account][date]; - if (nCheckpoints == 0) { - return 0; - } - - /// @dev First check most recent balance. - if (userStakingCheckpoints[account][date][nCheckpoints - 1].fromBlock <= blockNumber) { - return userStakingCheckpoints[account][date][nCheckpoints - 1].stake; - } - - /// @dev Next check implicit zero balance. - if (userStakingCheckpoints[account][date][0].fromBlock > blockNumber) { - return 0; - } - - uint32 lower = 0; - uint32 upper = nCheckpoints - 1; - while (upper > lower) { - uint32 center = upper - (upper - lower) / 2; /// @dev ceil, avoiding overflow. - Checkpoint memory cp = userStakingCheckpoints[account][date][center]; - if (cp.fromBlock == blockNumber) { - return cp.stake; - } else if (cp.fromBlock < blockNumber) { - lower = center; - } else { - upper = center - 1; - } - } - return userStakingCheckpoints[account][date][lower].stake; - } - - /*************************** Weighted Vesting Stake computation for fee sharing *******************************/ - - /** - * @notice Determine the prior weighted vested amount for an account as of a block number. - * Iterate through checkpoints adding up voting power. - * @dev Block number must be a finalized block or else this function will - * revert to prevent misinformation. - * Used for fee sharing, not voting. - * TODO: WeightedStaking::getPriorVestingWeightedStake is using the variable name "votes" - * to add up token stake, and that could be misleading. - * - * @param blockNumber The block number to get the vote balance at. - * @param date The staking date to compute the power for. - * @return The weighted stake the account had as of the given block. - * */ - function getPriorVestingWeightedStake(uint256 blockNumber, uint256 date) public view returns (uint96 votes) { - /// @dev If date is not an exact break point, start weight computation from the previous break point (alternative would be the next). - uint256 start = timestampToLockDate(date); - uint256 end = start + MAX_DURATION; - - /// @dev Max 78 iterations. - for (uint256 i = start; i <= end; i += TWO_WEEKS) { - uint96 weightedStake = weightedVestingStakeByDate(i, start, blockNumber); - if (weightedStake > 0) { - votes = add96(votes, weightedStake, "overflow - total weight computation"); - } - } - } - - /** - * @notice Compute the voting power for a specific date. - * Power = stake * weight - * TODO: WeightedStaking::weightedVestingStakeByDate should probably better - * be internal instead of a public function. - * @param date The staking date to compute the power for. - * @param startDate The date for which we need to know the power of the stake. - * @param blockNumber The block number, needed for checkpointing. - * @return The stacking power. - * */ - function weightedVestingStakeByDate( - uint256 date, - uint256 startDate, - uint256 blockNumber - ) public view returns (uint96 power) { - uint96 staked = _getPriorVestingStakeByDate(date, blockNumber); - if (staked > 0) { - uint96 weight = computeWeightByDate(date, startDate); - power = mul96(staked, weight, "WeightedStaking::weightedVestingStakeByDate: multiplication overflow") / WEIGHT_FACTOR; - } else { - power = 0; - } - } - - /** - * @notice Determine the prior number of vested stake for an account until a - * certain lock date as of a block number. - * @dev Block number must be a finalized block or else this function - * will revert to prevent misinformation. - * @param date The lock date. - * @param blockNumber The block number to get the vote balance at. - * @return The number of votes the account had as of the given block. - * */ - function getPriorVestingStakeByDate(uint256 date, uint256 blockNumber) external view returns (uint96) { - return _getPriorVestingStakeByDate(date, blockNumber); - } - - /** - * @notice Determine the prior number of vested stake for an account until a - * certain lock date as of a block number. - * @dev All functions of Staking contract use this internal version, - * we need to modify public function in order to workaround issue with Vesting.withdrawTokens: - * return 1 instead of 0 if message sender is a contract. - * @param date The lock date. - * @param blockNumber The block number to get the vote balance at. - * @return The number of votes the account had as of the given block. - * */ - function _getPriorVestingStakeByDate(uint256 date, uint256 blockNumber) internal view returns (uint96) { - require(blockNumber < _getCurrentBlockNumber(), "WeightedStaking::getPriorVestingStakeByDate: not yet determined"); - - uint32 nCheckpoints = numVestingCheckpoints[date]; - if (nCheckpoints == 0) { - return 0; - } - - /// @dev First check most recent balance. - if (vestingCheckpoints[date][nCheckpoints - 1].fromBlock <= blockNumber) { - return vestingCheckpoints[date][nCheckpoints - 1].stake; - } - - /// @dev Next check implicit zero balance. - if (vestingCheckpoints[date][0].fromBlock > blockNumber) { - return 0; - } - - uint32 lower = 0; - uint32 upper = nCheckpoints - 1; - while (upper > lower) { - uint32 center = upper - (upper - lower) / 2; /// @dev ceil, avoiding overflow. - Checkpoint memory cp = vestingCheckpoints[date][center]; - if (cp.fromBlock == blockNumber) { - return cp.stake; - } else if (cp.fromBlock < blockNumber) { - lower = center; - } else { - upper = center - 1; - } - } - return vestingCheckpoints[date][lower].stake; - } - - /**************** SHARED FUNCTIONS *********************/ - - /** - * @notice Determine the current Block Number - * @dev This is segregated from the _getPriorUserStakeByDate function to better test - * advancing blocks functionality using Mock Contracts - * */ - function _getCurrentBlockNumber() internal view returns (uint256) { - return block.number; - } - - /** - * @notice Compute the weight for a specific date. - * @param date The unlocking date. - * @param startDate We compute the weight for the tokens staked until 'date' on 'startDate'. - * @return The weighted stake the account had as of the given block. - * */ - function computeWeightByDate(uint256 date, uint256 startDate) public pure returns (uint96 weight) { - require(date >= startDate, "date needs to be bigger than startDate"); - uint256 remainingTime = (date - startDate); - require(MAX_DURATION >= remainingTime, "remaining time can't be bigger than max duration"); - /// @dev x = max days - remaining days - uint96 x = uint96(MAX_DURATION - remainingTime) / (1 days); - /// @dev w = (m^2 - x^2)/m^2 +1 (multiplied by the weight factor) - weight = add96( - WEIGHT_FACTOR, - mul96( - MAX_VOTING_WEIGHT * WEIGHT_FACTOR, - sub96(MAX_DURATION_POW_2, x * x, "underflow on weight calculation"), - "multiplication overflow on weight computation" - ) / MAX_DURATION_POW_2, - "overflow on weight computation" - ); - } - - /** - * @notice Unstaking is possible every 2 weeks only. This means, to - * calculate the key value for the staking checkpoints, we need to - * map the intended timestamp to the closest available date. - * @param timestamp The unlocking timestamp. - * @return The actual unlocking date (might be up to 2 weeks shorter than intended). - * */ - function timestampToLockDate(uint256 timestamp) public view returns (uint256 lockDate) { - require(timestamp >= kickoffTS, "timestamp lies before contract creation"); - /** - * @dev If staking timestamp does not match any of the unstaking dates - * , set the lockDate to the closest one before the timestamp. - * E.g. Passed timestamps lies 7 weeks after kickoff -> only stake for 6 weeks. - * */ - uint256 periodFromKickoff = (timestamp - kickoffTS) / TWO_WEEKS; - lockDate = periodFromKickoff * TWO_WEEKS + kickoffTS; - } - - /** - * @dev origin vesting contracts have different dates - * we need to add 2 weeks to get end of period (by default, it's start) - * @param date The staking date to compute the power for. - * @return unlocking date. - */ - function _adjustDateForOrigin(uint256 date) internal view returns (uint256) { - uint256 adjustedDate = timestampToLockDate(date); - //origin vesting contracts have different dates - //we need to add 2 weeks to get end of period (by default, it's start) - if (adjustedDate != date) { - date = adjustedDate + TWO_WEEKS; - } - return date; - } - - /** - * @notice Add account to ACL. - * @param _admin The addresses of the account to grant permissions. - * */ - function addAdmin(address _admin) public onlyOwner { - admins[_admin] = true; - emit AdminAdded(_admin); - } - - /** - * @notice Remove account from ACL. - * @param _admin The addresses of the account to revoke permissions. - * */ - function removeAdmin(address _admin) public onlyOwner { - admins[_admin] = false; - emit AdminRemoved(_admin); - } - - /** - * @notice Add vesting contract's code hash to a map of code hashes. - * @param vesting The address of Vesting contract. - * @dev We need it to use _isVestingContract() function instead of isContract() - */ - function addContractCodeHash(address vesting) public onlyAuthorized { - bytes32 codeHash = _getCodeHash(vesting); - vestingCodeHashes[codeHash] = true; - emit ContractCodeHashAdded(codeHash); - } - - /** - * @notice Add vesting contract's code hash to a map of code hashes. - * @param vesting The address of Vesting contract. - * @dev We need it to use _isVestingContract() function instead of isContract() - */ - function removeContractCodeHash(address vesting) public onlyAuthorized { - bytes32 codeHash = _getCodeHash(vesting); - vestingCodeHashes[codeHash] = false; - emit ContractCodeHashRemoved(codeHash); - } - - /** - * @notice Return flag whether the given address is a registered vesting contract. - * @param stakerAddress the address to check - */ - function isVestingContract(address stakerAddress) public view returns (bool) { - bool isVesting; - bytes32 codeHash = _getCodeHash(stakerAddress); - if (address(vestingRegistryLogic) != address(0)) { - isVesting = vestingRegistryLogic.isVestingAdress(stakerAddress); - } - - if (isVesting) return true; - if (vestingCodeHashes[codeHash]) return true; - return false; - } - - /** - * @notice Return hash of contract code - */ - function _getCodeHash(address _contract) internal view returns (bytes32) { - bytes32 codeHash; - assembly { - codeHash := extcodehash(_contract) - } - return codeHash; - } + using Address for address payable; + + /** + * @dev Throws if called by any account other than the owner or admin. + */ + modifier onlyAuthorized() { + require(isOwner() || admins[msg.sender], "unauthorized"); + _; + } + + /** + * @notice sets vesting registry + * @param _vestingRegistryProxy the address of vesting registry proxy contract + * @dev _vestingRegistryProxy can be set to 0 as this function can be reused by + * various other functionalities without the necessity of linking it with Vesting Registry + */ + function setVestingRegistry(address _vestingRegistryProxy) external onlyOwner { + vestingRegistryLogic = VestingRegistryLogic(_vestingRegistryProxy); + } + + /** + * @notice Sets the users' vesting stakes for a giving lock dates and writes checkpoints. + * @param lockedDates The arrays of lock dates. + * @param values The array of values to add to the staked balance. + */ + function setVestingStakes(uint256[] calldata lockedDates, uint96[] calldata values) + external + onlyAuthorized + { + require(lockedDates.length == values.length, "arrays mismatch"); + + uint256 length = lockedDates.length; + for (uint256 i = 0; i < length; i++) { + _setVestingStake(lockedDates[i], values[i]); + } + } + + /** + * @notice Sets the users' vesting stake for a giving lock date and writes a checkpoint. + * @param lockedTS The lock date. + * @param value The value to be set. + */ + function _setVestingStake(uint256 lockedTS, uint96 value) internal { + //delete all checkpoints (shouldn't be any during the first initialization) + uint32 nCheckpoints = numVestingCheckpoints[lockedTS]; + for (uint32 i = 0; i < nCheckpoints; i++) { + delete vestingCheckpoints[lockedTS][i]; + } + delete numVestingCheckpoints[lockedTS]; + + //blockNumber should be in the past + nCheckpoints = 0; + uint32 blockNumber = 0; + vestingCheckpoints[lockedTS][nCheckpoints] = Checkpoint(blockNumber, value); + numVestingCheckpoints[lockedTS] = nCheckpoints + 1; + + emit VestingStakeSet(lockedTS, value); + } + + /************* TOTAL VOTING POWER COMPUTATION ************************/ + + /** + * @notice Compute the total voting power at a given time. + * @param blockNumber The block number, needed for checkpointing. + * @param time The timestamp for which to calculate the total voting power. + * @return The total voting power at the given time. + * */ + function getPriorTotalVotingPower(uint32 blockNumber, uint256 time) + public + view + returns (uint96 totalVotingPower) + { + /// @dev Start the computation with the exact or previous unlocking date (voting weight remians the same until the next break point). + uint256 start = timestampToLockDate(time); + uint256 end = start + MAX_DURATION; + + /// @dev Max 78 iterations. + for (uint256 i = start; i <= end; i += TWO_WEEKS) { + totalVotingPower = add96( + totalVotingPower, + _totalPowerByDate(i, start, blockNumber), + "overflow on total voting power computation" + ); + } + } + + /** + * @notice Compute the voting power for a specific date. + * Power = stake * weight + * @param date The staking date to compute the power for. + * @param startDate The date for which we need to know the power of the stake. + * @param blockNumber The block number, needed for checkpointing. + * @return The stacking power. + * */ + function _totalPowerByDate( + uint256 date, + uint256 startDate, + uint256 blockNumber + ) internal view returns (uint96 power) { + uint96 weight = computeWeightByDate(date, startDate); + uint96 staked = getPriorTotalStakesForDate(date, blockNumber); + /// @dev weight is multiplied by some factor to allow decimals. + power = mul96(staked, weight, "multiplication overflow") / WEIGHT_FACTOR; + } + + /** + * @notice Determine the prior number of stake for an unlocking date as of a block number. + * @dev Block number must be a finalized block or else this function will + * revert to prevent misinformation. + * TODO: WeightedStaking::getPriorTotalStakesForDate should probably better + * be internal instead of a public function. + * @param date The date to check the stakes for. + * @param blockNumber The block number to get the vote balance at. + * @return The number of votes the account had as of the given block. + * */ + function getPriorTotalStakesForDate(uint256 date, uint256 blockNumber) + public + view + returns (uint96) + { + require(blockNumber < _getCurrentBlockNumber(), "not yet determined"); + + uint32 nCheckpoints = numTotalStakingCheckpoints[date]; + if (nCheckpoints == 0) { + return 0; + } + + // First check most recent balance + if (totalStakingCheckpoints[date][nCheckpoints - 1].fromBlock <= blockNumber) { + return totalStakingCheckpoints[date][nCheckpoints - 1].stake; + } + + // Next check implicit zero balance + if (totalStakingCheckpoints[date][0].fromBlock > blockNumber) { + return 0; + } + + uint32 lower = 0; + uint32 upper = nCheckpoints - 1; + while (upper > lower) { + uint32 center = upper - (upper - lower) / 2; // ceil, avoiding overflow + Checkpoint memory cp = totalStakingCheckpoints[date][center]; + if (cp.fromBlock == blockNumber) { + return cp.stake; + } else if (cp.fromBlock < blockNumber) { + lower = center; + } else { + upper = center - 1; + } + } + return totalStakingCheckpoints[date][lower].stake; + } + + /****************************** DELEGATED VOTING POWER COMPUTATION ************************/ + + /** + * @notice Determine the prior number of votes for a delegatee as of a block number. + * Iterate through checkpoints adding up voting power. + * @dev Block number must be a finalized block or else this function will revert + * to prevent misinformation. + * Used for Voting, not for fee sharing. + * @param account The address of the account to check. + * @param blockNumber The block number to get the vote balance at. + * @param date The staking date to compute the power for. + * @return The number of votes the delegatee had as of the given block. + * */ + function getPriorVotes( + address account, + uint256 blockNumber, + uint256 date + ) public view returns (uint96 votes) { + /// @dev If date is not an exact break point, start weight computation from the previous break point (alternative would be the next). + uint256 start = timestampToLockDate(date); + uint256 end = start + MAX_DURATION; + + /// @dev Max 78 iterations. + for (uint256 i = start; i <= end; i += TWO_WEEKS) { + votes = add96( + votes, + _totalPowerByDateForDelegatee(account, i, start, blockNumber), + "overflow - total voting power computation" + ); + } + } + + /** + * @notice Compute the voting power for a specific date. + * Power = stake * weight + * @param account The address of the account to check. + * @param date The staking date to compute the power for. + * @param startDate The date for which we need to know the power of the stake. + * @param blockNumber The block number, needed for checkpointing. + * @return The stacking power. + * */ + function _totalPowerByDateForDelegatee( + address account, + uint256 date, + uint256 startDate, + uint256 blockNumber + ) internal view returns (uint96 power) { + uint96 weight = computeWeightByDate(date, startDate); + uint96 staked = getPriorStakeByDateForDelegatee(account, date, blockNumber); + power = mul96(staked, weight, "overflow") / WEIGHT_FACTOR; + } + + /** + * @notice Determine the prior number of stake for an account as of a block number. + * @dev Block number must be a finalized block or else this function will + * revert to prevent misinformation. + * TODO: WeightedStaking::getPriorStakeByDateForDelegatee should probably better + * be internal instead of a public function. + * @param account The address of the account to check. + * @param date The staking date to compute the power for. + * @param blockNumber The block number to get the vote balance at. + * @return The number of votes the account had as of the given block. + * */ + function getPriorStakeByDateForDelegatee( + address account, + uint256 date, + uint256 blockNumber + ) public view returns (uint96) { + require(blockNumber < _getCurrentBlockNumber(), "not determined yet"); + + uint32 nCheckpoints = numDelegateStakingCheckpoints[account][date]; + if (nCheckpoints == 0) { + return 0; + } + + /// @dev First check most recent balance. + if (delegateStakingCheckpoints[account][date][nCheckpoints - 1].fromBlock <= blockNumber) { + return delegateStakingCheckpoints[account][date][nCheckpoints - 1].stake; + } + + /// @dev Next check implicit zero balance. + if (delegateStakingCheckpoints[account][date][0].fromBlock > blockNumber) { + return 0; + } + + uint32 lower = 0; + uint32 upper = nCheckpoints - 1; + while (upper > lower) { + uint32 center = upper - (upper - lower) / 2; /// @dev ceil, avoiding overflow. + Checkpoint memory cp = delegateStakingCheckpoints[account][date][center]; + if (cp.fromBlock == blockNumber) { + return cp.stake; + } else if (cp.fromBlock < blockNumber) { + lower = center; + } else { + upper = center - 1; + } + } + return delegateStakingCheckpoints[account][date][lower].stake; + } + + /*************************** User Weighted Stake computation for fee sharing *******************************/ + + /** + * @notice Determine the prior weighted stake for an account as of a block number. + * Iterate through checkpoints adding up voting power. + * @dev Block number must be a finalized block or else this function will + * revert to prevent misinformation. + * Used for fee sharing, not voting. + * TODO: WeightedStaking::getPriorWeightedStake is using the variable name "votes" + * to add up token stake, and that could be misleading. + * + * @param account The address of the account to check. + * @param blockNumber The block number to get the vote balance at. + * @param date The date/timestamp of the unstaking time. + * @return The weighted stake the account had as of the given block. + * */ + function getPriorWeightedStake( + address account, + uint256 blockNumber, + uint256 date + ) public view returns (uint96 votes) { + /// @dev If date is not an exact break point, start weight computation from the previous break point (alternative would be the next). + uint256 start = timestampToLockDate(date); + uint256 end = start + MAX_DURATION; + + /// @dev Max 78 iterations. + for (uint256 i = start; i <= end; i += TWO_WEEKS) { + uint96 weightedStake = weightedStakeByDate(account, i, start, blockNumber); + if (weightedStake > 0) { + votes = add96(votes, weightedStake, "overflow on total weight computation"); + } + } + } + + /** + * @notice Compute the voting power for a specific date. + * Power = stake * weight + * TODO: WeightedStaking::weightedStakeByDate should probably better + * be internal instead of a public function. + * @param account The user address. + * @param date The staking date to compute the power for. + * @param startDate The date for which we need to know the power of the stake. + * @param blockNumber The block number, needed for checkpointing. + * @return The stacking power. + * */ + function weightedStakeByDate( + address account, + uint256 date, + uint256 startDate, + uint256 blockNumber + ) public view returns (uint96 power) { + uint96 staked = _getPriorUserStakeByDate(account, date, blockNumber); + if (staked > 0) { + uint96 weight = computeWeightByDate(date, startDate); + power = mul96(staked, weight, "overflow error") / WEIGHT_FACTOR; + } else { + power = 0; + } + } + + /** + * @notice Determine the prior number of stake for an account until a + * certain lock date as of a block number. + * @dev Block number must be a finalized block or else this function + * will revert to prevent misinformation. + * @param account The address of the account to check. + * @param date The lock date. + * @param blockNumber The block number to get the vote balance at. + * @return The number of votes the account had as of the given block. + * */ + function getPriorUserStakeByDate( + address account, + uint256 date, + uint256 blockNumber + ) external view returns (uint96) { + uint96 priorStake = _getPriorUserStakeByDate(account, date, blockNumber); + // @dev we need to modify function in order to workaround issue with Vesting.withdrawTokens: + // return 1 instead of 0 if message sender is a contract. + if (priorStake == 0 && isVestingContract(msg.sender)) { + priorStake = 1; + } + return priorStake; + } + + /** + * @notice Determine the prior number of stake for an account until a + * certain lock date as of a block number. + * @dev All functions of Staking contract use this internal version, + * we need to modify public function in order to workaround issue with Vesting.withdrawTokens: + * return 1 instead of 0 if message sender is a contract. + * @param account The address of the account to check. + * @param date The lock date. + * @param blockNumber The block number to get the vote balance at. + * @return The number of votes the account had as of the given block. + * */ + function _getPriorUserStakeByDate( + address account, + uint256 date, + uint256 blockNumber + ) internal view returns (uint96) { + require(blockNumber < _getCurrentBlockNumber(), "not determined"); + + date = _adjustDateForOrigin(date); + uint32 nCheckpoints = numUserStakingCheckpoints[account][date]; + if (nCheckpoints == 0) { + return 0; + } + + /// @dev First check most recent balance. + if (userStakingCheckpoints[account][date][nCheckpoints - 1].fromBlock <= blockNumber) { + return userStakingCheckpoints[account][date][nCheckpoints - 1].stake; + } + + /// @dev Next check implicit zero balance. + if (userStakingCheckpoints[account][date][0].fromBlock > blockNumber) { + return 0; + } + + uint32 lower = 0; + uint32 upper = nCheckpoints - 1; + while (upper > lower) { + uint32 center = upper - (upper - lower) / 2; /// @dev ceil, avoiding overflow. + Checkpoint memory cp = userStakingCheckpoints[account][date][center]; + if (cp.fromBlock == blockNumber) { + return cp.stake; + } else if (cp.fromBlock < blockNumber) { + lower = center; + } else { + upper = center - 1; + } + } + return userStakingCheckpoints[account][date][lower].stake; + } + + /*************************** Weighted Vesting Stake computation for fee sharing *******************************/ + + /** + * @notice Determine the prior weighted vested amount for an account as of a block number. + * Iterate through checkpoints adding up voting power. + * @dev Block number must be a finalized block or else this function will + * revert to prevent misinformation. + * Used for fee sharing, not voting. + * TODO: WeightedStaking::getPriorVestingWeightedStake is using the variable name "votes" + * to add up token stake, and that could be misleading. + * + * @param blockNumber The block number to get the vote balance at. + * @param date The staking date to compute the power for. + * @return The weighted stake the account had as of the given block. + * */ + function getPriorVestingWeightedStake(uint256 blockNumber, uint256 date) + public + view + returns (uint96 votes) + { + /// @dev If date is not an exact break point, start weight computation from the previous break point (alternative would be the next). + uint256 start = timestampToLockDate(date); + uint256 end = start + MAX_DURATION; + + /// @dev Max 78 iterations. + for (uint256 i = start; i <= end; i += TWO_WEEKS) { + uint96 weightedStake = weightedVestingStakeByDate(i, start, blockNumber); + if (weightedStake > 0) { + votes = add96(votes, weightedStake, "overflow - total weight computation"); + } + } + } + + /** + * @notice Compute the voting power for a specific date. + * Power = stake * weight + * TODO: WeightedStaking::weightedVestingStakeByDate should probably better + * be internal instead of a public function. + * @param date The staking date to compute the power for. + * @param startDate The date for which we need to know the power of the stake. + * @param blockNumber The block number, needed for checkpointing. + * @return The stacking power. + * */ + function weightedVestingStakeByDate( + uint256 date, + uint256 startDate, + uint256 blockNumber + ) public view returns (uint96 power) { + uint96 staked = _getPriorVestingStakeByDate(date, blockNumber); + if (staked > 0) { + uint96 weight = computeWeightByDate(date, startDate); + power = + mul96( + staked, + weight, + "WeightedStaking::weightedVestingStakeByDate: multiplication overflow" + ) / + WEIGHT_FACTOR; + } else { + power = 0; + } + } + + /** + * @notice Determine the prior number of vested stake for an account until a + * certain lock date as of a block number. + * @dev Block number must be a finalized block or else this function + * will revert to prevent misinformation. + * @param date The lock date. + * @param blockNumber The block number to get the vote balance at. + * @return The number of votes the account had as of the given block. + * */ + function getPriorVestingStakeByDate(uint256 date, uint256 blockNumber) + external + view + returns (uint96) + { + return _getPriorVestingStakeByDate(date, blockNumber); + } + + /** + * @notice Determine the prior number of vested stake for an account until a + * certain lock date as of a block number. + * @dev All functions of Staking contract use this internal version, + * we need to modify public function in order to workaround issue with Vesting.withdrawTokens: + * return 1 instead of 0 if message sender is a contract. + * @param date The lock date. + * @param blockNumber The block number to get the vote balance at. + * @return The number of votes the account had as of the given block. + * */ + function _getPriorVestingStakeByDate(uint256 date, uint256 blockNumber) + internal + view + returns (uint96) + { + require( + blockNumber < _getCurrentBlockNumber(), + "WeightedStaking::getPriorVestingStakeByDate: not yet determined" + ); + + uint32 nCheckpoints = numVestingCheckpoints[date]; + if (nCheckpoints == 0) { + return 0; + } + + /// @dev First check most recent balance. + if (vestingCheckpoints[date][nCheckpoints - 1].fromBlock <= blockNumber) { + return vestingCheckpoints[date][nCheckpoints - 1].stake; + } + + /// @dev Next check implicit zero balance. + if (vestingCheckpoints[date][0].fromBlock > blockNumber) { + return 0; + } + + uint32 lower = 0; + uint32 upper = nCheckpoints - 1; + while (upper > lower) { + uint32 center = upper - (upper - lower) / 2; /// @dev ceil, avoiding overflow. + Checkpoint memory cp = vestingCheckpoints[date][center]; + if (cp.fromBlock == blockNumber) { + return cp.stake; + } else if (cp.fromBlock < blockNumber) { + lower = center; + } else { + upper = center - 1; + } + } + return vestingCheckpoints[date][lower].stake; + } + + /**************** SHARED FUNCTIONS *********************/ + + /** + * @notice Determine the current Block Number + * @dev This is segregated from the _getPriorUserStakeByDate function to better test + * advancing blocks functionality using Mock Contracts + * */ + function _getCurrentBlockNumber() internal view returns (uint256) { + return block.number; + } + + /** + * @notice Compute the weight for a specific date. + * @param date The unlocking date. + * @param startDate We compute the weight for the tokens staked until 'date' on 'startDate'. + * @return The weighted stake the account had as of the given block. + * */ + function computeWeightByDate(uint256 date, uint256 startDate) + public + pure + returns (uint96 weight) + { + require(date >= startDate, "date needs to be bigger than startDate"); + uint256 remainingTime = (date - startDate); + require(MAX_DURATION >= remainingTime, "remaining time can't be bigger than max duration"); + /// @dev x = max days - remaining days + uint96 x = uint96(MAX_DURATION - remainingTime) / (1 days); + /// @dev w = (m^2 - x^2)/m^2 +1 (multiplied by the weight factor) + weight = add96( + WEIGHT_FACTOR, + mul96( + MAX_VOTING_WEIGHT * WEIGHT_FACTOR, + sub96(MAX_DURATION_POW_2, x * x, "underflow on weight calculation"), + "multiplication overflow on weight computation" + ) / MAX_DURATION_POW_2, + "overflow on weight computation" + ); + } + + /** + * @notice Unstaking is possible every 2 weeks only. This means, to + * calculate the key value for the staking checkpoints, we need to + * map the intended timestamp to the closest available date. + * @param timestamp The unlocking timestamp. + * @return The actual unlocking date (might be up to 2 weeks shorter than intended). + * */ + function timestampToLockDate(uint256 timestamp) public view returns (uint256 lockDate) { + require(timestamp >= kickoffTS, "timestamp lies before contract creation"); + /** + * @dev If staking timestamp does not match any of the unstaking dates + * , set the lockDate to the closest one before the timestamp. + * E.g. Passed timestamps lies 7 weeks after kickoff -> only stake for 6 weeks. + * */ + uint256 periodFromKickoff = (timestamp - kickoffTS) / TWO_WEEKS; + lockDate = periodFromKickoff * TWO_WEEKS + kickoffTS; + } + + /** + * @dev origin vesting contracts have different dates + * we need to add 2 weeks to get end of period (by default, it's start) + * @param date The staking date to compute the power for. + * @return unlocking date. + */ + function _adjustDateForOrigin(uint256 date) internal view returns (uint256) { + uint256 adjustedDate = timestampToLockDate(date); + //origin vesting contracts have different dates + //we need to add 2 weeks to get end of period (by default, it's start) + if (adjustedDate != date) { + date = adjustedDate + TWO_WEEKS; + } + return date; + } + + /** + * @notice Add account to ACL. + * @param _admin The addresses of the account to grant permissions. + * */ + function addAdmin(address _admin) public onlyOwner { + admins[_admin] = true; + emit AdminAdded(_admin); + } + + /** + * @notice Remove account from ACL. + * @param _admin The addresses of the account to revoke permissions. + * */ + function removeAdmin(address _admin) public onlyOwner { + admins[_admin] = false; + emit AdminRemoved(_admin); + } + + /** + * @notice Add vesting contract's code hash to a map of code hashes. + * @param vesting The address of Vesting contract. + * @dev We need it to use _isVestingContract() function instead of isContract() + */ + function addContractCodeHash(address vesting) public onlyAuthorized { + bytes32 codeHash = _getCodeHash(vesting); + vestingCodeHashes[codeHash] = true; + emit ContractCodeHashAdded(codeHash); + } + + /** + * @notice Add vesting contract's code hash to a map of code hashes. + * @param vesting The address of Vesting contract. + * @dev We need it to use _isVestingContract() function instead of isContract() + */ + function removeContractCodeHash(address vesting) public onlyAuthorized { + bytes32 codeHash = _getCodeHash(vesting); + vestingCodeHashes[codeHash] = false; + emit ContractCodeHashRemoved(codeHash); + } + + /** + * @notice Return flag whether the given address is a registered vesting contract. + * @param stakerAddress the address to check + */ + function isVestingContract(address stakerAddress) public view returns (bool) { + bool isVesting; + bytes32 codeHash = _getCodeHash(stakerAddress); + if (address(vestingRegistryLogic) != address(0)) { + isVesting = vestingRegistryLogic.isVestingAdress(stakerAddress); + } + + if (isVesting) return true; + if (vestingCodeHashes[codeHash]) return true; + return false; + } + + /** + * @notice Return hash of contract code + */ + function _getCodeHash(address _contract) internal view returns (bytes32) { + bytes32 codeHash; + assembly { + codeHash := extcodehash(_contract) + } + return codeHash; + } } diff --git a/contracts/governance/StakingRewards/StakingRewards.sol b/contracts/governance/StakingRewards/StakingRewards.sol index 2e5d1ac03..5224e2f08 100644 --- a/contracts/governance/StakingRewards/StakingRewards.sol +++ b/contracts/governance/StakingRewards/StakingRewards.sol @@ -17,221 +17,227 @@ import "../../openzeppelin/Address.sol"; * early unstaking. * */ contract StakingRewards is StakingRewardsStorage { - using SafeMath for uint256; - - /// @notice Emitted when SOV is withdrawn - /// @param receiver The address which recieves the SOV - /// @param amount The amount withdrawn from the Smart Contract - event RewardWithdrawn(address indexed receiver, uint256 amount); - - /** - * @notice Replacement of constructor by initialize function for Upgradable Contracts - * This function will be called only once by the owner. - * @param _SOV SOV token address - * @param _staking StakingProxy address should be passed - * */ - function initialize(address _SOV, IStaking _staking) external onlyOwner { - require(_SOV != address(0), "Invalid SOV Address."); - require(Address.isContract(_SOV), "_SOV not a contract"); - SOV = IERC20(_SOV); - staking = _staking; - startTime = staking.timestampToLockDate(block.timestamp); - setMaxDuration(15 * TWO_WEEKS); - deploymentBlock = _getCurrentBlockNumber(); - } - - /** - * @notice Stops the current rewards program. - * @dev All stakes existing on the contract at the point in time of - * cancellation continue accruing rewards until the end of the staking - * period being rewarded - * */ - function stop() external onlyOwner { - require(stopBlock == 0, "Already stopped"); - stopBlock = _getCurrentBlockNumber(); - } - - /** - * @notice Collect rewards - * @dev User calls this function to collect SOV staking rewards as per the SIP-0024 program. - * The weighted stake is calculated using getPriorWeightedStake. Block number sent to the functon - * must be a finalised block, hence we deduct 1 from the current block. User is only allowed to withdraw - * after intervals of 14 days. - * @param restartTime The time from which the staking rewards calculation shall restart. - * The issue is that we can only run for a max duration and if someone stakes for the - * first time after the max duration is over, the reward will always return 0. Thus, we need to restart - * from the duration that elapsed without generating rewards. - * */ - function collectReward(uint256 restartTime) external { - (uint256 withdrawalTime, uint256 amount) = getStakerCurrentReward(true, restartTime); - require(withdrawalTime > 0 && amount > 0, "no valid reward"); - withdrawals[msg.sender] = withdrawalTime; - _payReward(msg.sender, amount); - } - - /** - * @notice Withdraws all token from the contract by Multisig. - * @param _receiverAddress The address where the tokens has to be transferred. - */ - function withdrawTokensByOwner(address _receiverAddress) external onlyOwner { - uint256 value = SOV.balanceOf(address(this)); - _transferSOV(_receiverAddress, value); - } - - /** - * @notice Changes average block time - based on blockchain - * @dev If average block time significantly changes, we can update it here and use for block number calculation - */ - function setAverageBlockTime(uint256 _averageBlockTime) external onlyOwner { - averageBlockTime = _averageBlockTime; - } - - /** - * @notice This function computes the last staking checkpoint and calculates the corresponding - * block number using the average block time which is then added to the mapping `checkpointBlockDetails`. - */ - function setBlock() external { - uint256 lastCheckpointTime = staking.timestampToLockDate(block.timestamp); - _setBlock(lastCheckpointTime); - } - - /** - * @notice This function computes the block number using the average block time for a given historical - * checkpoint which is added to the mapping `checkpointBlockDetails`. - * @param _time Exact staking checkpoint time - */ - function setHistoricalBlock(uint256 _time) external { - _setBlock(_time); - } - - /** - * @notice Sets the max duration - * @dev Rewards can be collected for a maximum duration at a time. This - * is to avoid Block Gas Limit failures. Setting it zero would mean that it will loop - * through the entire duration since the start of rewards program. - * It should ideally be set to a value, for which the rewards can be easily processed. - * @param _duration Max duration for which rewards can be collected at a go (in seconds) - * */ - function setMaxDuration(uint256 _duration) public onlyOwner { - maxDuration = _duration; - } - - /** - * @notice Internal function to calculate weighted stake - * @dev If the rewards program is stopped, the user will still continue to - * earn till the end of staking period based on the stop block. - * @param _staker Staker address - * @param _block Last finalised block - * @param _date The date to compute prior weighted stakes - * @return The weighted stake - * */ - function _computeRewardForDate( - address _staker, - uint256 _block, - uint256 _date - ) internal view returns (uint256 weightedStake) { - weightedStake = staking.getPriorWeightedStake(_staker, _block, _date); - if (stopBlock > 0) { - uint256 previousWeightedStake = staking.getPriorWeightedStake(_staker, stopBlock, _date); - if (previousWeightedStake < weightedStake) { - weightedStake = previousWeightedStake; - } - } - } - - /** - * @notice Internal function to pay rewards - * @dev Base rate is annual, but we pay interest for 14 days, - * which is 1/26 of one staking year (1092 days) - * @param _staker User address - * @param amount the reward amount - * */ - function _payReward(address _staker, uint256 amount) internal { - require(SOV.balanceOf(address(this)) >= amount, "not enough funds to reward user"); - claimedBalances[_staker] = claimedBalances[_staker].add(amount); - _transferSOV(_staker, amount); - } - - /** - * @notice transfers SOV tokens to given address - * @param _receiver the address of the SOV receiver - * @param _amount the amount to be transferred - */ - function _transferSOV(address _receiver, uint256 _amount) internal { - require(_amount != 0, "amount invalid"); - require(SOV.transfer(_receiver, _amount), "transfer failed"); - emit RewardWithdrawn(_receiver, _amount); - } - - /** - * @notice Determine the current Block Number - * @dev This is segregated from the _getPriorUserStakeByDate function to better test - * advancing blocks functionality using Mock Contracts - * */ - function _getCurrentBlockNumber() internal view returns (uint256) { - return block.number; - } - - /** - * @notice Internal function to calculate and set block - * */ - function _setBlock(uint256 _checkpointTime) internal { - uint256 currentTS = block.timestamp; - uint256 lastFinalisedBlock = _getCurrentBlockNumber() - 1; - require(checkpointBlockDetails[_checkpointTime] == 0, "block number already set"); - uint256 checkpointBlock = lastFinalisedBlock.sub(((currentTS.sub(_checkpointTime)).div(averageBlockTime))); - checkpointBlockDetails[_checkpointTime] = checkpointBlock; - } - - /** - * @notice Get staker's current accumulated reward - * @dev The collectReward() function internally calls this function to calculate reward amount - * @param considerMaxDuration True: Runs for the maximum duration - used in tx not to run out of gas - * False - to query total rewards - * @param restartTime The time from which the staking rewards calculation shall restart. - * @return The timestamp of last withdrawal - * @return The accumulated reward - */ - function getStakerCurrentReward(bool considerMaxDuration, uint256 restartTime) - public - view - returns (uint256 lastWithdrawalInterval, uint256 amount) - { - uint256 weightedStake; - uint256 lastFinalisedBlock = _getCurrentBlockNumber() - 1; - uint256 currentTS = block.timestamp; - uint256 duration; - address staker = msg.sender; - uint256 lastWithdrawal = withdrawals[staker]; - - uint256 lastStakingInterval = staking.timestampToLockDate(currentTS); - lastWithdrawalInterval = lastWithdrawal > 0 ? lastWithdrawal : startTime; - if (lastStakingInterval <= lastWithdrawalInterval) return (0, 0); - /* Normally the restart time is 0. If this function returns a valid lastWithdrawalInterval + using SafeMath for uint256; + + /// @notice Emitted when SOV is withdrawn + /// @param receiver The address which recieves the SOV + /// @param amount The amount withdrawn from the Smart Contract + event RewardWithdrawn(address indexed receiver, uint256 amount); + + /** + * @notice Replacement of constructor by initialize function for Upgradable Contracts + * This function will be called only once by the owner. + * @param _SOV SOV token address + * @param _staking StakingProxy address should be passed + * */ + function initialize(address _SOV, IStaking _staking) external onlyOwner { + require(_SOV != address(0), "Invalid SOV Address."); + require(Address.isContract(_SOV), "_SOV not a contract"); + SOV = IERC20(_SOV); + staking = _staking; + startTime = staking.timestampToLockDate(block.timestamp); + setMaxDuration(15 * TWO_WEEKS); + deploymentBlock = _getCurrentBlockNumber(); + } + + /** + * @notice Stops the current rewards program. + * @dev All stakes existing on the contract at the point in time of + * cancellation continue accruing rewards until the end of the staking + * period being rewarded + * */ + function stop() external onlyOwner { + require(stopBlock == 0, "Already stopped"); + stopBlock = _getCurrentBlockNumber(); + } + + /** + * @notice Collect rewards + * @dev User calls this function to collect SOV staking rewards as per the SIP-0024 program. + * The weighted stake is calculated using getPriorWeightedStake. Block number sent to the functon + * must be a finalised block, hence we deduct 1 from the current block. User is only allowed to withdraw + * after intervals of 14 days. + * @param restartTime The time from which the staking rewards calculation shall restart. + * The issue is that we can only run for a max duration and if someone stakes for the + * first time after the max duration is over, the reward will always return 0. Thus, we need to restart + * from the duration that elapsed without generating rewards. + * */ + function collectReward(uint256 restartTime) external { + (uint256 withdrawalTime, uint256 amount) = getStakerCurrentReward(true, restartTime); + require(withdrawalTime > 0 && amount > 0, "no valid reward"); + withdrawals[msg.sender] = withdrawalTime; + _payReward(msg.sender, amount); + } + + /** + * @notice Withdraws all token from the contract by Multisig. + * @param _receiverAddress The address where the tokens has to be transferred. + */ + function withdrawTokensByOwner(address _receiverAddress) external onlyOwner { + uint256 value = SOV.balanceOf(address(this)); + _transferSOV(_receiverAddress, value); + } + + /** + * @notice Changes average block time - based on blockchain + * @dev If average block time significantly changes, we can update it here and use for block number calculation + */ + function setAverageBlockTime(uint256 _averageBlockTime) external onlyOwner { + averageBlockTime = _averageBlockTime; + } + + /** + * @notice This function computes the last staking checkpoint and calculates the corresponding + * block number using the average block time which is then added to the mapping `checkpointBlockDetails`. + */ + function setBlock() external { + uint256 lastCheckpointTime = staking.timestampToLockDate(block.timestamp); + _setBlock(lastCheckpointTime); + } + + /** + * @notice This function computes the block number using the average block time for a given historical + * checkpoint which is added to the mapping `checkpointBlockDetails`. + * @param _time Exact staking checkpoint time + */ + function setHistoricalBlock(uint256 _time) external { + _setBlock(_time); + } + + /** + * @notice Sets the max duration + * @dev Rewards can be collected for a maximum duration at a time. This + * is to avoid Block Gas Limit failures. Setting it zero would mean that it will loop + * through the entire duration since the start of rewards program. + * It should ideally be set to a value, for which the rewards can be easily processed. + * @param _duration Max duration for which rewards can be collected at a go (in seconds) + * */ + function setMaxDuration(uint256 _duration) public onlyOwner { + maxDuration = _duration; + } + + /** + * @notice Internal function to calculate weighted stake + * @dev If the rewards program is stopped, the user will still continue to + * earn till the end of staking period based on the stop block. + * @param _staker Staker address + * @param _block Last finalised block + * @param _date The date to compute prior weighted stakes + * @return The weighted stake + * */ + function _computeRewardForDate( + address _staker, + uint256 _block, + uint256 _date + ) internal view returns (uint256 weightedStake) { + weightedStake = staking.getPriorWeightedStake(_staker, _block, _date); + if (stopBlock > 0) { + uint256 previousWeightedStake = + staking.getPriorWeightedStake(_staker, stopBlock, _date); + if (previousWeightedStake < weightedStake) { + weightedStake = previousWeightedStake; + } + } + } + + /** + * @notice Internal function to pay rewards + * @dev Base rate is annual, but we pay interest for 14 days, + * which is 1/26 of one staking year (1092 days) + * @param _staker User address + * @param amount the reward amount + * */ + function _payReward(address _staker, uint256 amount) internal { + require(SOV.balanceOf(address(this)) >= amount, "not enough funds to reward user"); + claimedBalances[_staker] = claimedBalances[_staker].add(amount); + _transferSOV(_staker, amount); + } + + /** + * @notice transfers SOV tokens to given address + * @param _receiver the address of the SOV receiver + * @param _amount the amount to be transferred + */ + function _transferSOV(address _receiver, uint256 _amount) internal { + require(_amount != 0, "amount invalid"); + require(SOV.transfer(_receiver, _amount), "transfer failed"); + emit RewardWithdrawn(_receiver, _amount); + } + + /** + * @notice Determine the current Block Number + * @dev This is segregated from the _getPriorUserStakeByDate function to better test + * advancing blocks functionality using Mock Contracts + * */ + function _getCurrentBlockNumber() internal view returns (uint256) { + return block.number; + } + + /** + * @notice Internal function to calculate and set block + * */ + function _setBlock(uint256 _checkpointTime) internal { + uint256 currentTS = block.timestamp; + uint256 lastFinalisedBlock = _getCurrentBlockNumber() - 1; + require(checkpointBlockDetails[_checkpointTime] == 0, "block number already set"); + uint256 checkpointBlock = + lastFinalisedBlock.sub(((currentTS.sub(_checkpointTime)).div(averageBlockTime))); + checkpointBlockDetails[_checkpointTime] = checkpointBlock; + } + + /** + * @notice Get staker's current accumulated reward + * @dev The collectReward() function internally calls this function to calculate reward amount + * @param considerMaxDuration True: Runs for the maximum duration - used in tx not to run out of gas + * False - to query total rewards + * @param restartTime The time from which the staking rewards calculation shall restart. + * @return The timestamp of last withdrawal + * @return The accumulated reward + */ + function getStakerCurrentReward(bool considerMaxDuration, uint256 restartTime) + public + view + returns (uint256 lastWithdrawalInterval, uint256 amount) + { + uint256 weightedStake; + uint256 lastFinalisedBlock = _getCurrentBlockNumber() - 1; + uint256 currentTS = block.timestamp; + uint256 duration; + address staker = msg.sender; + uint256 lastWithdrawal = withdrawals[staker]; + + uint256 lastStakingInterval = staking.timestampToLockDate(currentTS); + lastWithdrawalInterval = lastWithdrawal > 0 ? lastWithdrawal : startTime; + if (lastStakingInterval <= lastWithdrawalInterval) return (0, 0); + /* Normally the restart time is 0. If this function returns a valid lastWithdrawalInterval and zero amount - that means there were no valid rewards for that period. So the new period must start from the end of the last interval or till the time no rewards are accumulated i.e. restartTime */ - if (restartTime >= lastWithdrawalInterval) { - uint256 latestRestartTime = staking.timestampToLockDate(restartTime); - lastWithdrawalInterval = latestRestartTime; - } - - if (considerMaxDuration) { - uint256 addedMaxDuration = lastWithdrawalInterval.add(maxDuration); - duration = addedMaxDuration < currentTS ? staking.timestampToLockDate(addedMaxDuration) : lastStakingInterval; - } else { - duration = lastStakingInterval; - } - - for (uint256 i = lastWithdrawalInterval; i < duration; i += TWO_WEEKS) { - uint256 referenceBlock = checkpointBlockDetails[i]; - if (referenceBlock == 0) { - referenceBlock = lastFinalisedBlock.sub(((currentTS.sub(i)).div(averageBlockTime))); - } - if (referenceBlock < deploymentBlock) referenceBlock = deploymentBlock; - weightedStake = weightedStake.add(_computeRewardForDate(staker, referenceBlock, i)); - } - - lastWithdrawalInterval = duration; - amount = weightedStake.mul(BASE_RATE).div(DIVISOR); - } + if (restartTime >= lastWithdrawalInterval) { + uint256 latestRestartTime = staking.timestampToLockDate(restartTime); + lastWithdrawalInterval = latestRestartTime; + } + + if (considerMaxDuration) { + uint256 addedMaxDuration = lastWithdrawalInterval.add(maxDuration); + duration = addedMaxDuration < currentTS + ? staking.timestampToLockDate(addedMaxDuration) + : lastStakingInterval; + } else { + duration = lastStakingInterval; + } + + for (uint256 i = lastWithdrawalInterval; i < duration; i += TWO_WEEKS) { + uint256 referenceBlock = checkpointBlockDetails[i]; + if (referenceBlock == 0) { + referenceBlock = lastFinalisedBlock.sub( + ((currentTS.sub(i)).div(averageBlockTime)) + ); + } + if (referenceBlock < deploymentBlock) referenceBlock = deploymentBlock; + weightedStake = weightedStake.add(_computeRewardForDate(staker, referenceBlock, i)); + } + + lastWithdrawalInterval = duration; + amount = weightedStake.mul(BASE_RATE).div(DIVISOR); + } } diff --git a/contracts/governance/StakingRewards/StakingRewardsStorage.sol b/contracts/governance/StakingRewards/StakingRewardsStorage.sol index ebefa0819..25a42c595 100644 --- a/contracts/governance/StakingRewards/StakingRewardsStorage.sol +++ b/contracts/governance/StakingRewards/StakingRewardsStorage.sol @@ -16,53 +16,53 @@ import "../../openzeppelin/Ownable.sol"; * at the beginning of each new staking interval. * */ contract StakingRewardsStorage is Ownable { - /// @notice The SOV token contract. - IERC20 public SOV; + /// @notice The SOV token contract. + IERC20 public SOV; - ///@notice the staking proxy contract address - IStaking public staking; + ///@notice the staking proxy contract address + IStaking public staking; - /// @notice 2 weeks in seconds. - uint256 public constant TWO_WEEKS = 1209600; + /// @notice 2 weeks in seconds. + uint256 public constant TWO_WEEKS = 1209600; - /// @notice Annual Base Rate - it is the maximum interest rate(APY) - uint256 public constant BASE_RATE = 2975; + /// @notice Annual Base Rate - it is the maximum interest rate(APY) + uint256 public constant BASE_RATE = 2975; - /// @notice DIVISOR is set as 2600000 = 26 (num periods per year) * 10 (max voting weight) * 10000 (2975 -> 0.2975) - uint256 public constant DIVISOR = 2600000; + /// @notice DIVISOR is set as 2600000 = 26 (num periods per year) * 10 (max voting weight) * 10000 (2975 -> 0.2975) + uint256 public constant DIVISOR = 2600000; - /// @notice Maximum duration to collect rewards at one go - uint256 public maxDuration; + /// @notice Maximum duration to collect rewards at one go + uint256 public maxDuration; - /// @notice Represents the time when the contract is deployed - uint256 public startTime; + /// @notice Represents the time when the contract is deployed + uint256 public startTime; - /// @notice Represents the block when the Staking Rewards pogram is stopped - uint256 public stopBlock; + /// @notice Represents the block when the Staking Rewards pogram is stopped + uint256 public stopBlock; - /// @notice User Address -> Last Withdrawn Timestamp - mapping(address => uint256) public withdrawals; + /// @notice User Address -> Last Withdrawn Timestamp + mapping(address => uint256) public withdrawals; - /// @notice User Address -> Claimed Balance - mapping(address => uint256) public claimedBalances; + /// @notice User Address -> Claimed Balance + mapping(address => uint256) public claimedBalances; - /// @notice Represents the block when the StakingRwards Program is started - uint256 public deploymentBlock; + /// @notice Represents the block when the StakingRwards Program is started + uint256 public deploymentBlock; - /// Moved the variables from Initializable contract to resolve issue caused by incorrect Inheritance Order - /** - * @dev Indicates that the contract has been initialized. - */ - bool private _initialized; + /// Moved the variables from Initializable contract to resolve issue caused by incorrect Inheritance Order + /** + * @dev Indicates that the contract has been initialized. + */ + bool private _initialized; - /** - * @dev Indicates that the contract is in the process of being initialized. - */ - bool private _initializing; + /** + * @dev Indicates that the contract is in the process of being initialized. + */ + bool private _initializing; - /// @notice BlockTime -> BlockNumber for a Staking Checkpoint - mapping(uint256 => uint256) public checkpointBlockDetails; + /// @notice BlockTime -> BlockNumber for a Staking Checkpoint + mapping(uint256 => uint256) public checkpointBlockDetails; - /// @notice Average Block Time - making it flexible - uint256 public averageBlockTime; + /// @notice Average Block Time - making it flexible + uint256 public averageBlockTime; } diff --git a/contracts/governance/Timelock.sol b/contracts/governance/Timelock.sol index 973e1e2d0..4c2b670b1 100644 --- a/contracts/governance/Timelock.sol +++ b/contracts/governance/Timelock.sol @@ -4,37 +4,37 @@ import "../openzeppelin/SafeMath.sol"; import "./ErrorDecoder.sol"; interface ITimelock { - function delay() external view returns (uint256); - - function GRACE_PERIOD() external view returns (uint256); - - function acceptAdmin() external; - - function queuedTransactions(bytes32 hash) external view returns (bool); - - function queueTransaction( - address target, - uint256 value, - string calldata signature, - bytes calldata data, - uint256 eta - ) external returns (bytes32); - - function cancelTransaction( - address target, - uint256 value, - string calldata signature, - bytes calldata data, - uint256 eta - ) external; - - function executeTransaction( - address target, - uint256 value, - string calldata signature, - bytes calldata data, - uint256 eta - ) external payable returns (bytes memory); + function delay() external view returns (uint256); + + function GRACE_PERIOD() external view returns (uint256); + + function acceptAdmin() external; + + function queuedTransactions(bytes32 hash) external view returns (bool); + + function queueTransaction( + address target, + uint256 value, + string calldata signature, + bytes calldata data, + uint256 eta + ) external returns (bytes32); + + function cancelTransaction( + address target, + uint256 value, + string calldata signature, + bytes calldata data, + uint256 eta + ) external; + + function executeTransaction( + address target, + uint256 value, + string calldata signature, + bytes calldata data, + uint256 eta + ) external payable returns (bytes memory); } /** @@ -68,185 +68,233 @@ interface ITimelock { * and the overall risk for contracts building on top of it, as GovernorAlpha. * */ contract Timelock is ErrorDecoder, ITimelock { - using SafeMath for uint256; - - uint256 public constant GRACE_PERIOD = 14 days; - uint256 public constant MINIMUM_DELAY = 3 hours; - uint256 public constant MAXIMUM_DELAY = 30 days; - - address public admin; - address public pendingAdmin; - uint256 public delay; - - mapping(bytes32 => bool) public queuedTransactions; - - event NewAdmin(address indexed newAdmin); - event NewPendingAdmin(address indexed newPendingAdmin); - event NewDelay(uint256 indexed newDelay); - event CancelTransaction(bytes32 indexed txHash, address indexed target, uint256 value, string signature, bytes data, uint256 eta); - event ExecuteTransaction(bytes32 indexed txHash, address indexed target, uint256 value, string signature, bytes data, uint256 eta); - event QueueTransaction(bytes32 indexed txHash, address indexed target, uint256 value, string signature, bytes data, uint256 eta); - - /** - * @notice Function called on instance deployment of the contract. - * @param admin_ Governance contract address. - * @param delay_ Time to wait for queued transactions to be executed. - * */ - constructor(address admin_, uint256 delay_) public { - require(delay_ >= MINIMUM_DELAY, "Timelock::constructor: Delay must exceed minimum delay."); - require(delay_ <= MAXIMUM_DELAY, "Timelock::setDelay: Delay must not exceed maximum delay."); - - admin = admin_; - delay = delay_; - } - - /** - * @notice Fallback function is to react to receiving value (rBTC). - * */ - function() external payable {} - - /** - * @notice Set a new delay when executing the contract calls. - * @param delay_ The amount of time to wait until execution. - * */ - function setDelay(uint256 delay_) public { - require(msg.sender == address(this), "Timelock::setDelay: Call must come from Timelock."); - require(delay_ >= MINIMUM_DELAY, "Timelock::setDelay: Delay must exceed minimum delay."); - require(delay_ <= MAXIMUM_DELAY, "Timelock::setDelay: Delay must not exceed maximum delay."); - delay = delay_; - - emit NewDelay(delay); - } - - /** - * @notice Accept a new admin for the timelock. - * */ - function acceptAdmin() public { - require(msg.sender == pendingAdmin, "Timelock::acceptAdmin: Call must come from pendingAdmin."); - admin = msg.sender; - pendingAdmin = address(0); - - emit NewAdmin(admin); - } - - /** - * @notice Set a new pending admin for the timelock. - * @param pendingAdmin_ The new pending admin address. - * */ - function setPendingAdmin(address pendingAdmin_) public { - require(msg.sender == address(this), "Timelock::setPendingAdmin: Call must come from Timelock."); - pendingAdmin = pendingAdmin_; - - emit NewPendingAdmin(pendingAdmin); - } - - /** - * @notice Queue a new transaction from the governance contract. - * @param target The contract to call. - * @param value The amount to send in the transaction. - * @param signature The stanndard representation of the function called. - * @param data The ethereum transaction input data payload. - * @param eta Estimated Time of Accomplishment. The timestamp that the - * proposal will be available for execution, set once the vote succeeds. - * */ - function queueTransaction( - address target, - uint256 value, - string memory signature, - bytes memory data, - uint256 eta - ) public returns (bytes32) { - require(msg.sender == admin, "Timelock::queueTransaction: Call must come from admin."); - require(eta >= getBlockTimestamp().add(delay), "Timelock::queueTransaction: Estimated execution block must satisfy delay."); - - bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); - queuedTransactions[txHash] = true; - - emit QueueTransaction(txHash, target, value, signature, data, eta); - return txHash; - } - - /** - * @notice Cancel a transaction. - * @param target The contract to call. - * @param value The amount to send in the transaction. - * @param signature The stanndard representation of the function called. - * @param data The ethereum transaction input data payload. - * @param eta Estimated Time of Accomplishment. The timestamp that the - * proposal will be available for execution, set once the vote succeeds. - * */ - function cancelTransaction( - address target, - uint256 value, - string memory signature, - bytes memory data, - uint256 eta - ) public { - require(msg.sender == admin, "Timelock::cancelTransaction: Call must come from admin."); - - bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); - queuedTransactions[txHash] = false; - - emit CancelTransaction(txHash, target, value, signature, data, eta); - } - - /** - * @notice Executes a previously queued transaction from the governance. - * @param target The contract to call. - * @param value The amount to send in the transaction. - * @param signature The stanndard representation of the function called. - * @param data The ethereum transaction input data payload. - * @param eta Estimated Time of Accomplishment. The timestamp that the - * proposal will be available for execution, set once the vote succeeds. - * */ - function executeTransaction( - address target, - uint256 value, - string memory signature, - bytes memory data, - uint256 eta - ) public payable returns (bytes memory) { - require(msg.sender == admin, "Timelock::executeTransaction: Call must come from admin."); - - bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); - require(queuedTransactions[txHash], "Timelock::executeTransaction: Transaction hasn't been queued."); - require(getBlockTimestamp() >= eta, "Timelock::executeTransaction: Transaction hasn't surpassed time lock."); - require(getBlockTimestamp() <= eta.add(GRACE_PERIOD), "Timelock::executeTransaction: Transaction is stale."); - - queuedTransactions[txHash] = false; - - bytes memory callData; - - if (bytes(signature).length == 0) { - callData = data; - } else { - callData = abi.encodePacked(bytes4(keccak256(bytes(signature))), data); - } - - // solium-disable-next-line security/no-call-value - (bool success, bytes memory returnData) = target.call.value(value)(callData); - if (!success) { - if (returnData.length <= ERROR_MESSAGE_SHIFT) { - revert("Timelock::executeTransaction: Transaction execution reverted."); - } else { - revert(_addErrorMessage("Timelock::executeTransaction: ", string(returnData))); - } - } - - emit ExecuteTransaction(txHash, target, value, signature, data, eta); - - return returnData; - } - - /** - * @notice A function used to get the current Block Timestamp. - * @dev Timestamp of the current block in seconds since the epoch. - * It is a Unix time stamp. So, it has the complete information about - * the date, hours, minutes, and seconds (in UTC) when the block was - * created. - * */ - function getBlockTimestamp() internal view returns (uint256) { - // solium-disable-next-line security/no-block-members - return block.timestamp; - } + using SafeMath for uint256; + + uint256 public constant GRACE_PERIOD = 14 days; + uint256 public constant MINIMUM_DELAY = 3 hours; + uint256 public constant MAXIMUM_DELAY = 30 days; + + address public admin; + address public pendingAdmin; + uint256 public delay; + + mapping(bytes32 => bool) public queuedTransactions; + + event NewAdmin(address indexed newAdmin); + event NewPendingAdmin(address indexed newPendingAdmin); + event NewDelay(uint256 indexed newDelay); + event CancelTransaction( + bytes32 indexed txHash, + address indexed target, + uint256 value, + string signature, + bytes data, + uint256 eta + ); + event ExecuteTransaction( + bytes32 indexed txHash, + address indexed target, + uint256 value, + string signature, + bytes data, + uint256 eta + ); + event QueueTransaction( + bytes32 indexed txHash, + address indexed target, + uint256 value, + string signature, + bytes data, + uint256 eta + ); + + /** + * @notice Function called on instance deployment of the contract. + * @param admin_ Governance contract address. + * @param delay_ Time to wait for queued transactions to be executed. + * */ + constructor(address admin_, uint256 delay_) public { + require( + delay_ >= MINIMUM_DELAY, + "Timelock::constructor: Delay must exceed minimum delay." + ); + require( + delay_ <= MAXIMUM_DELAY, + "Timelock::setDelay: Delay must not exceed maximum delay." + ); + + admin = admin_; + delay = delay_; + } + + /** + * @notice Fallback function is to react to receiving value (rBTC). + * */ + function() external payable {} + + /** + * @notice Set a new delay when executing the contract calls. + * @param delay_ The amount of time to wait until execution. + * */ + function setDelay(uint256 delay_) public { + require(msg.sender == address(this), "Timelock::setDelay: Call must come from Timelock."); + require(delay_ >= MINIMUM_DELAY, "Timelock::setDelay: Delay must exceed minimum delay."); + require( + delay_ <= MAXIMUM_DELAY, + "Timelock::setDelay: Delay must not exceed maximum delay." + ); + delay = delay_; + + emit NewDelay(delay); + } + + /** + * @notice Accept a new admin for the timelock. + * */ + function acceptAdmin() public { + require( + msg.sender == pendingAdmin, + "Timelock::acceptAdmin: Call must come from pendingAdmin." + ); + admin = msg.sender; + pendingAdmin = address(0); + + emit NewAdmin(admin); + } + + /** + * @notice Set a new pending admin for the timelock. + * @param pendingAdmin_ The new pending admin address. + * */ + function setPendingAdmin(address pendingAdmin_) public { + require( + msg.sender == address(this), + "Timelock::setPendingAdmin: Call must come from Timelock." + ); + pendingAdmin = pendingAdmin_; + + emit NewPendingAdmin(pendingAdmin); + } + + /** + * @notice Queue a new transaction from the governance contract. + * @param target The contract to call. + * @param value The amount to send in the transaction. + * @param signature The stanndard representation of the function called. + * @param data The ethereum transaction input data payload. + * @param eta Estimated Time of Accomplishment. The timestamp that the + * proposal will be available for execution, set once the vote succeeds. + * */ + function queueTransaction( + address target, + uint256 value, + string memory signature, + bytes memory data, + uint256 eta + ) public returns (bytes32) { + require(msg.sender == admin, "Timelock::queueTransaction: Call must come from admin."); + require( + eta >= getBlockTimestamp().add(delay), + "Timelock::queueTransaction: Estimated execution block must satisfy delay." + ); + + bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); + queuedTransactions[txHash] = true; + + emit QueueTransaction(txHash, target, value, signature, data, eta); + return txHash; + } + + /** + * @notice Cancel a transaction. + * @param target The contract to call. + * @param value The amount to send in the transaction. + * @param signature The stanndard representation of the function called. + * @param data The ethereum transaction input data payload. + * @param eta Estimated Time of Accomplishment. The timestamp that the + * proposal will be available for execution, set once the vote succeeds. + * */ + function cancelTransaction( + address target, + uint256 value, + string memory signature, + bytes memory data, + uint256 eta + ) public { + require(msg.sender == admin, "Timelock::cancelTransaction: Call must come from admin."); + + bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); + queuedTransactions[txHash] = false; + + emit CancelTransaction(txHash, target, value, signature, data, eta); + } + + /** + * @notice Executes a previously queued transaction from the governance. + * @param target The contract to call. + * @param value The amount to send in the transaction. + * @param signature The stanndard representation of the function called. + * @param data The ethereum transaction input data payload. + * @param eta Estimated Time of Accomplishment. The timestamp that the + * proposal will be available for execution, set once the vote succeeds. + * */ + function executeTransaction( + address target, + uint256 value, + string memory signature, + bytes memory data, + uint256 eta + ) public payable returns (bytes memory) { + require(msg.sender == admin, "Timelock::executeTransaction: Call must come from admin."); + + bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); + require( + queuedTransactions[txHash], + "Timelock::executeTransaction: Transaction hasn't been queued." + ); + require( + getBlockTimestamp() >= eta, + "Timelock::executeTransaction: Transaction hasn't surpassed time lock." + ); + require( + getBlockTimestamp() <= eta.add(GRACE_PERIOD), + "Timelock::executeTransaction: Transaction is stale." + ); + + queuedTransactions[txHash] = false; + + bytes memory callData; + + if (bytes(signature).length == 0) { + callData = data; + } else { + callData = abi.encodePacked(bytes4(keccak256(bytes(signature))), data); + } + + // solium-disable-next-line security/no-call-value + (bool success, bytes memory returnData) = target.call.value(value)(callData); + if (!success) { + if (returnData.length <= ERROR_MESSAGE_SHIFT) { + revert("Timelock::executeTransaction: Transaction execution reverted."); + } else { + revert(_addErrorMessage("Timelock::executeTransaction: ", string(returnData))); + } + } + + emit ExecuteTransaction(txHash, target, value, signature, data, eta); + + return returnData; + } + + /** + * @notice A function used to get the current Block Timestamp. + * @dev Timestamp of the current block in seconds since the epoch. + * It is a Unix time stamp. So, it has the complete information about + * the date, hours, minutes, and seconds (in UTC) when the block was + * created. + * */ + function getBlockTimestamp() internal view returns (uint256) { + // solium-disable-next-line security/no-block-members + return block.timestamp; + } } diff --git a/contracts/governance/Vesting/DevelopmentFund.sol b/contracts/governance/Vesting/DevelopmentFund.sol index 1438a9acd..403cbb7f3 100644 --- a/contracts/governance/Vesting/DevelopmentFund.sol +++ b/contracts/governance/Vesting/DevelopmentFund.sol @@ -9,366 +9,427 @@ import "../../interfaces/IERC20.sol"; * @notice You can use this contract for timed token release from Dev Fund. */ contract DevelopmentFund { - using SafeMath for uint256; - - /* Storage */ - - /// @notice The SOV token contract. - IERC20 public SOV; - - /// @notice The current contract status. - enum Status { Deployed, Active, Expired } - Status public status; - - /// @notice The owner of the locked tokens (usually Governance). - address public lockedTokenOwner; - /// @notice The owner of the unlocked tokens (usually MultiSig). - address public unlockedTokenOwner; - /// @notice The emergency transfer wallet/contract. - address public safeVault; - /// @notice The new locked token owner waiting to be approved. - address public newLockedTokenOwner; - - /// @notice The last token release timestamp or the time of contract creation. - uint256 public lastReleaseTime; - - /// @notice The release duration array in seconds. - uint256[] public releaseDuration; - /// @notice The release token amount. - uint256[] public releaseTokenAmount; - - /* Events */ - - /// @notice Emitted when the contract is activated. - event DevelopmentFundActivated(); - - /// @notice Emitted when the contract is expired due to total token transfer. - event DevelopmentFundExpired(); - - /// @notice Emitted when a new locked owner is added to the contract. - /// @param _initiator The address which initiated this event to be emitted. - /// @param _newLockedOwner The address which is added as the new locked owner. - /// @dev Can only be initiated by the current locked owner. - event NewLockedOwnerAdded(address indexed _initiator, address indexed _newLockedOwner); - - /// @notice Emitted when a new locked owner is approved to the contract. - /// @param _initiator The address which initiated this event to be emitted. - /// @param _oldLockedOwner The address of the previous locked owner. - /// @param _newLockedOwner The address which is added as the new locked owner. - /// @dev Can only be initiated by the current unlocked owner. - event NewLockedOwnerApproved(address indexed _initiator, address indexed _oldLockedOwner, address indexed _newLockedOwner); - - /// @notice Emitted when a new unlocked owner is updated in the contract. - /// @param _initiator The address which initiated this event to be emitted. - /// @param _newUnlockedOwner The address which is updated as the new unlocked owner. - /// @dev Can only be initiated by the current locked owner. - event UnlockedOwnerUpdated(address indexed _initiator, address indexed _newUnlockedOwner); - - /// @notice Emitted when a new token deposit is done. - /// @param _initiator The address which initiated this event to be emitted. - /// @param _amount The total amount of token deposited. - event TokenDeposit(address indexed _initiator, uint256 _amount); - - /// @notice Emitted when a new release schedule is created. - /// @param _initiator The address which initiated this event to be emitted. - /// @param _releaseCount The number of releases planned in the schedule. - event TokenReleaseChanged(address indexed _initiator, uint256 _releaseCount); - - /// @notice Emitted when a unlocked owner transfers all the tokens to a safe vault. - /// @param _initiator The address which initiated this event to be emitted. - /// @param _receiver The address which receives this token withdrawn. - /// @param _amount The total amount of token transferred. - /// @dev This is done in an emergency situation only to a predetermined wallet by locked token owner. - event LockedTokenTransferByUnlockedOwner(address indexed _initiator, address indexed _receiver, uint256 _amount); - - /// @notice Emitted when a unlocked owner withdraws the released tokens. - /// @param _initiator The address which initiated this event to be emitted. - /// @param _amount The total amount of token withdrawn. - /// @param _releaseCount The total number of releases done based on duration. - event UnlockedTokenWithdrawalByUnlockedOwner(address indexed _initiator, uint256 _amount, uint256 _releaseCount); - - /// @notice Emitted when a locked owner transfers all the tokens to a receiver. - /// @param _initiator The address which initiated this event to be emitted. - /// @param _receiver The address which receives this token transfer. - /// @param _amount The total amount of token transferred. - /// @dev This is done only by locked token owner. - event LockedTokenTransferByLockedOwner(address indexed _initiator, address indexed _receiver, uint256 _amount); - - /* Modifiers */ - - modifier onlyLockedTokenOwner() { - require(msg.sender == lockedTokenOwner, "Only Locked Token Owner can call this."); - _; - } - - modifier onlyUnlockedTokenOwner() { - require(msg.sender == unlockedTokenOwner, "Only Unlocked Token Owner can call this."); - _; - } - - modifier checkStatus(Status s) { - require(status == s, "The contract is not in the right state."); - _; - } - - /* Functions */ - - /** - * @notice Setup the required parameters. - * @param _SOV The SOV token address. - * @param _lockedTokenOwner The owner of the locked tokens & contract. - * @param _safeVault The emergency wallet/contract to transfer token. - * @param _unlockedTokenOwner The owner of the unlocked tokens. - * @param _lastReleaseTime If the last release time is to be changed, zero if no change required. - * @param _releaseDuration The time duration between each release calculated from `lastReleaseTime` in seconds. - * @param _releaseTokenAmount The amount of token to be released in each duration/interval. - * @dev Initial release schedule should be verified, error will result in either redeployment or calling changeTokenReleaseSchedule() after init() along with token transfer. - */ - constructor( - address _SOV, - address _lockedTokenOwner, - address _safeVault, - address _unlockedTokenOwner, - uint256 _lastReleaseTime, - uint256[] memory _releaseDuration, - uint256[] memory _releaseTokenAmount - ) public { - require(_SOV != address(0), "Invalid SOV Address."); - require(_lockedTokenOwner != address(0), "Locked token & contract owner address invalid."); - require(_safeVault != address(0), "Safe Vault address invalid."); - require(_unlockedTokenOwner != address(0), "Unlocked token address invalid."); - - SOV = IERC20(_SOV); - lockedTokenOwner = _lockedTokenOwner; - safeVault = _safeVault; - unlockedTokenOwner = _unlockedTokenOwner; - - lastReleaseTime = _lastReleaseTime; - /// If last release time passed is zero, then current time stamp will be used as the last release time. - if (_lastReleaseTime == 0) { - lastReleaseTime = block.timestamp; - } - - /// Checking if the schedule duration and token allocation length matches. - require(_releaseDuration.length == _releaseTokenAmount.length, "Release Schedule does not match."); - - /// Finally we update the token release schedule. - releaseDuration = _releaseDuration; - releaseTokenAmount = _releaseTokenAmount; - } - - /** - * @notice This function is called once after deployment for token transfer based on schedule. - * @dev Without calling this function, the contract will not work. - */ - function init() public checkStatus(Status.Deployed) { - uint256[] memory _releaseTokenAmount = releaseTokenAmount; - require(_releaseTokenAmount.length != 0, "Release Schedule not set."); - - /// Getting the current release schedule total token amount. - uint256 _releaseTotalTokenAmount; - for (uint256 amountIndex = 0; amountIndex < _releaseTokenAmount.length; amountIndex++) { - _releaseTotalTokenAmount = _releaseTotalTokenAmount.add(_releaseTokenAmount[amountIndex]); - } - - bool txStatus = SOV.transferFrom(msg.sender, address(this), _releaseTotalTokenAmount); - require(txStatus, "Not enough token sent to change release schedule."); - - status = Status.Active; - - emit DevelopmentFundActivated(); - } - - /** - * @notice Update Locked Token Owner. - * @param _newLockedTokenOwner The owner of the locked tokens & contract. - */ - function updateLockedTokenOwner(address _newLockedTokenOwner) public onlyLockedTokenOwner checkStatus(Status.Active) { - require(_newLockedTokenOwner != address(0), "New locked token owner address invalid."); - - newLockedTokenOwner = _newLockedTokenOwner; - - emit NewLockedOwnerAdded(msg.sender, _newLockedTokenOwner); - } - - /** - * @notice Approve Locked Token Owner. - * @dev This approval is an added security to avoid development fund takeover by a compromised locked token owner. - */ - function approveLockedTokenOwner() public onlyUnlockedTokenOwner checkStatus(Status.Active) { - require(newLockedTokenOwner != address(0), "No new locked owner added."); - - emit NewLockedOwnerApproved(msg.sender, lockedTokenOwner, newLockedTokenOwner); - - lockedTokenOwner = newLockedTokenOwner; - - newLockedTokenOwner = address(0); - } - - /** - * @notice Update Unlocked Token Owner. - * @param _newUnlockedTokenOwner The new unlocked token owner. - */ - function updateUnlockedTokenOwner(address _newUnlockedTokenOwner) public onlyLockedTokenOwner checkStatus(Status.Active) { - require(_newUnlockedTokenOwner != address(0), "New unlocked token owner address invalid."); - - unlockedTokenOwner = _newUnlockedTokenOwner; - - emit UnlockedOwnerUpdated(msg.sender, _newUnlockedTokenOwner); - } - - /** - * @notice Deposit tokens to this contract. - * @param _amount the amount of tokens deposited. - * @dev These tokens can be withdrawn/transferred any time by the lockedTokenOwner. - */ - function depositTokens(uint256 _amount) public checkStatus(Status.Active) { - require(_amount > 0, "Amount needs to be bigger than zero."); - - bool txStatus = SOV.transferFrom(msg.sender, address(this), _amount); - require(txStatus, "Token transfer was not successful."); - - emit TokenDeposit(msg.sender, _amount); - } - - /** - * @notice Change the Token release schedule. It creates a completely new schedule, and does not append on the previous one. - * @param _newLastReleaseTime If the last release time is to be changed, zero if no change required. - * @param _releaseDuration The time duration between each release calculated from `lastReleaseTime` in seconds. - * @param _releaseTokenAmount The amount of token to be released in each duration/interval. - * @dev _releaseDuration and _releaseTokenAmount should be specified in reverse order of release. - */ - function changeTokenReleaseSchedule( - uint256 _newLastReleaseTime, - uint256[] memory _releaseDuration, - uint256[] memory _releaseTokenAmount - ) public onlyLockedTokenOwner checkStatus(Status.Active) { - /// Checking if the schedule duration and token allocation length matches. - require(_releaseDuration.length == _releaseTokenAmount.length, "Release Schedule does not match."); - - /// If the last release time has to be changed, then you can pass a new one here. - /// Or else, the duration of release will be calculated based on this timestamp. - /// Even a future timestamp can be mentioned here. - if (_newLastReleaseTime != 0) { - lastReleaseTime = _newLastReleaseTime; - } - - /// Checking if the contract have enough token balance for the release. - uint256 _releaseTotalTokenAmount; - for (uint256 amountIndex = 0; amountIndex < _releaseTokenAmount.length; amountIndex++) { - _releaseTotalTokenAmount = _releaseTotalTokenAmount.add(_releaseTokenAmount[amountIndex]); - } - - /// Getting the current token balance of the contract. - uint256 remainingTokens = SOV.balanceOf(address(this)); - - /// If the token balance is not sufficient, then we transfer the change to contract. - if (remainingTokens < _releaseTotalTokenAmount) { - bool txStatus = SOV.transferFrom(msg.sender, address(this), _releaseTotalTokenAmount.sub(remainingTokens)); - require(txStatus, "Not enough token sent to change release schedule."); - } else if (remainingTokens > _releaseTotalTokenAmount) { - /// If there are more tokens than required, send the extra tokens back. - bool txStatus = SOV.transfer(msg.sender, remainingTokens.sub(_releaseTotalTokenAmount)); - require(txStatus, "Token not received by the Locked Owner."); - } - - /// Finally we update the token release schedule. - releaseDuration = _releaseDuration; - releaseTokenAmount = _releaseTokenAmount; - - emit TokenReleaseChanged(msg.sender, _releaseDuration.length); - } - - /** - * @notice Transfers all of the remaining tokens in an emergency situation. - * @dev This could be called when governance or development fund might be compromised. - */ - function transferTokensByUnlockedTokenOwner() public onlyUnlockedTokenOwner checkStatus(Status.Active) { - uint256 remainingTokens = SOV.balanceOf(address(this)); - bool txStatus = SOV.transfer(safeVault, remainingTokens); - require(txStatus, "Token transfer was not successful. Check receiver address."); - status = Status.Expired; - - emit LockedTokenTransferByUnlockedOwner(msg.sender, safeVault, remainingTokens); - emit DevelopmentFundExpired(); - } - - /** - * @notice Withdraws all unlocked/released token. - * @param _amount The amount to be withdrawn. - */ - function withdrawTokensByUnlockedTokenOwner(uint256 _amount) public onlyUnlockedTokenOwner checkStatus(Status.Active) { - require(_amount > 0, "Zero can't be withdrawn."); - - uint256 count; /// To know how many elements to be removed from the release schedule. - uint256 amount = _amount; /// To know the total amount to be transferred. - uint256 newLastReleaseTimeMemory = lastReleaseTime; /// Better to use memory than storage. - uint256 releaseLength = releaseDuration.length.sub(1); /// Also checks if there are any elements in the release schedule. - - /// Getting the amount of tokens, the number of releases and calculating the total duration. - while (amount > 0 && newLastReleaseTimeMemory.add(releaseDuration[releaseLength]) < block.timestamp) { - if (amount >= releaseTokenAmount[releaseLength]) { - amount = amount.sub(releaseTokenAmount[releaseLength]); - newLastReleaseTimeMemory = newLastReleaseTimeMemory.add(releaseDuration[releaseLength]); - count++; - } else { - /// This will be the last case, if correct amount is passed. - releaseTokenAmount[releaseLength] = releaseTokenAmount[releaseLength].sub(amount); - amount = 0; - } - releaseLength--; - } - - /// Checking to see if atleast a single schedule was reached or not. - require(count > 0 || amount == 0, "No release schedule reached."); - - /// If locked token owner tries to send a higher amount that schedule - uint256 value = _amount.sub(amount); - - /// Now clearing up the release schedule. - releaseDuration.length -= count; - releaseTokenAmount.length -= count; - - /// Updating the last release time. - lastReleaseTime = newLastReleaseTimeMemory; - - /// Sending the amount to unlocked token owner. - bool txStatus = SOV.transfer(msg.sender, value); - require(txStatus, "Token transfer was not successful. Check receiver address."); - - emit UnlockedTokenWithdrawalByUnlockedOwner(msg.sender, value, count); - } - - /** - * @notice Transfers all of the remaining tokens by the owner maybe for an upgrade. - * @dev This could be called when the current development fund has to be upgraded. - * @param _receiver The address which receives this token transfer. - */ - function transferTokensByLockedTokenOwner(address _receiver) public onlyLockedTokenOwner checkStatus(Status.Active) { - uint256 remainingTokens = SOV.balanceOf(address(this)); - bool txStatus = SOV.transfer(_receiver, remainingTokens); - require(txStatus, "Token transfer was not successful. Check receiver address."); - status = Status.Expired; - - emit LockedTokenTransferByLockedOwner(msg.sender, _receiver, remainingTokens); - emit DevelopmentFundExpired(); - } - - /* Getter Functions */ - - /** - * @notice Function to read the current token release duration. - * @return _currentReleaseDuration The current release duration. - */ - function getReleaseDuration() public view returns (uint256[] memory _releaseTokenDuration) { - return releaseDuration; - } - - /** - * @notice Function to read the current token release amount. - * @return _currentReleaseTokenAmount The current release token amount. - */ - function getReleaseTokenAmount() public view returns (uint256[] memory _currentReleaseTokenAmount) { - return releaseTokenAmount; - } + using SafeMath for uint256; + + /* Storage */ + + /// @notice The SOV token contract. + IERC20 public SOV; + + /// @notice The current contract status. + enum Status { Deployed, Active, Expired } + Status public status; + + /// @notice The owner of the locked tokens (usually Governance). + address public lockedTokenOwner; + /// @notice The owner of the unlocked tokens (usually MultiSig). + address public unlockedTokenOwner; + /// @notice The emergency transfer wallet/contract. + address public safeVault; + /// @notice The new locked token owner waiting to be approved. + address public newLockedTokenOwner; + + /// @notice The last token release timestamp or the time of contract creation. + uint256 public lastReleaseTime; + + /// @notice The release duration array in seconds. + uint256[] public releaseDuration; + /// @notice The release token amount. + uint256[] public releaseTokenAmount; + + /* Events */ + + /// @notice Emitted when the contract is activated. + event DevelopmentFundActivated(); + + /// @notice Emitted when the contract is expired due to total token transfer. + event DevelopmentFundExpired(); + + /// @notice Emitted when a new locked owner is added to the contract. + /// @param _initiator The address which initiated this event to be emitted. + /// @param _newLockedOwner The address which is added as the new locked owner. + /// @dev Can only be initiated by the current locked owner. + event NewLockedOwnerAdded(address indexed _initiator, address indexed _newLockedOwner); + + /// @notice Emitted when a new locked owner is approved to the contract. + /// @param _initiator The address which initiated this event to be emitted. + /// @param _oldLockedOwner The address of the previous locked owner. + /// @param _newLockedOwner The address which is added as the new locked owner. + /// @dev Can only be initiated by the current unlocked owner. + event NewLockedOwnerApproved( + address indexed _initiator, + address indexed _oldLockedOwner, + address indexed _newLockedOwner + ); + + /// @notice Emitted when a new unlocked owner is updated in the contract. + /// @param _initiator The address which initiated this event to be emitted. + /// @param _newUnlockedOwner The address which is updated as the new unlocked owner. + /// @dev Can only be initiated by the current locked owner. + event UnlockedOwnerUpdated(address indexed _initiator, address indexed _newUnlockedOwner); + + /// @notice Emitted when a new token deposit is done. + /// @param _initiator The address which initiated this event to be emitted. + /// @param _amount The total amount of token deposited. + event TokenDeposit(address indexed _initiator, uint256 _amount); + + /// @notice Emitted when a new release schedule is created. + /// @param _initiator The address which initiated this event to be emitted. + /// @param _releaseCount The number of releases planned in the schedule. + event TokenReleaseChanged(address indexed _initiator, uint256 _releaseCount); + + /// @notice Emitted when a unlocked owner transfers all the tokens to a safe vault. + /// @param _initiator The address which initiated this event to be emitted. + /// @param _receiver The address which receives this token withdrawn. + /// @param _amount The total amount of token transferred. + /// @dev This is done in an emergency situation only to a predetermined wallet by locked token owner. + event LockedTokenTransferByUnlockedOwner( + address indexed _initiator, + address indexed _receiver, + uint256 _amount + ); + + /// @notice Emitted when a unlocked owner withdraws the released tokens. + /// @param _initiator The address which initiated this event to be emitted. + /// @param _amount The total amount of token withdrawn. + /// @param _releaseCount The total number of releases done based on duration. + event UnlockedTokenWithdrawalByUnlockedOwner( + address indexed _initiator, + uint256 _amount, + uint256 _releaseCount + ); + + /// @notice Emitted when a locked owner transfers all the tokens to a receiver. + /// @param _initiator The address which initiated this event to be emitted. + /// @param _receiver The address which receives this token transfer. + /// @param _amount The total amount of token transferred. + /// @dev This is done only by locked token owner. + event LockedTokenTransferByLockedOwner( + address indexed _initiator, + address indexed _receiver, + uint256 _amount + ); + + /* Modifiers */ + + modifier onlyLockedTokenOwner() { + require(msg.sender == lockedTokenOwner, "Only Locked Token Owner can call this."); + _; + } + + modifier onlyUnlockedTokenOwner() { + require(msg.sender == unlockedTokenOwner, "Only Unlocked Token Owner can call this."); + _; + } + + modifier checkStatus(Status s) { + require(status == s, "The contract is not in the right state."); + _; + } + + /* Functions */ + + /** + * @notice Setup the required parameters. + * @param _SOV The SOV token address. + * @param _lockedTokenOwner The owner of the locked tokens & contract. + * @param _safeVault The emergency wallet/contract to transfer token. + * @param _unlockedTokenOwner The owner of the unlocked tokens. + * @param _lastReleaseTime If the last release time is to be changed, zero if no change required. + * @param _releaseDuration The time duration between each release calculated from `lastReleaseTime` in seconds. + * @param _releaseTokenAmount The amount of token to be released in each duration/interval. + * @dev Initial release schedule should be verified, error will result in either redeployment or calling changeTokenReleaseSchedule() after init() along with token transfer. + */ + constructor( + address _SOV, + address _lockedTokenOwner, + address _safeVault, + address _unlockedTokenOwner, + uint256 _lastReleaseTime, + uint256[] memory _releaseDuration, + uint256[] memory _releaseTokenAmount + ) public { + require(_SOV != address(0), "Invalid SOV Address."); + require(_lockedTokenOwner != address(0), "Locked token & contract owner address invalid."); + require(_safeVault != address(0), "Safe Vault address invalid."); + require(_unlockedTokenOwner != address(0), "Unlocked token address invalid."); + + SOV = IERC20(_SOV); + lockedTokenOwner = _lockedTokenOwner; + safeVault = _safeVault; + unlockedTokenOwner = _unlockedTokenOwner; + + lastReleaseTime = _lastReleaseTime; + /// If last release time passed is zero, then current time stamp will be used as the last release time. + if (_lastReleaseTime == 0) { + lastReleaseTime = block.timestamp; + } + + /// Checking if the schedule duration and token allocation length matches. + require( + _releaseDuration.length == _releaseTokenAmount.length, + "Release Schedule does not match." + ); + + /// Finally we update the token release schedule. + releaseDuration = _releaseDuration; + releaseTokenAmount = _releaseTokenAmount; + } + + /** + * @notice This function is called once after deployment for token transfer based on schedule. + * @dev Without calling this function, the contract will not work. + */ + function init() public checkStatus(Status.Deployed) { + uint256[] memory _releaseTokenAmount = releaseTokenAmount; + require(_releaseTokenAmount.length != 0, "Release Schedule not set."); + + /// Getting the current release schedule total token amount. + uint256 _releaseTotalTokenAmount; + for (uint256 amountIndex = 0; amountIndex < _releaseTokenAmount.length; amountIndex++) { + _releaseTotalTokenAmount = _releaseTotalTokenAmount.add( + _releaseTokenAmount[amountIndex] + ); + } + + bool txStatus = SOV.transferFrom(msg.sender, address(this), _releaseTotalTokenAmount); + require(txStatus, "Not enough token sent to change release schedule."); + + status = Status.Active; + + emit DevelopmentFundActivated(); + } + + /** + * @notice Update Locked Token Owner. + * @param _newLockedTokenOwner The owner of the locked tokens & contract. + */ + function updateLockedTokenOwner(address _newLockedTokenOwner) + public + onlyLockedTokenOwner + checkStatus(Status.Active) + { + require(_newLockedTokenOwner != address(0), "New locked token owner address invalid."); + + newLockedTokenOwner = _newLockedTokenOwner; + + emit NewLockedOwnerAdded(msg.sender, _newLockedTokenOwner); + } + + /** + * @notice Approve Locked Token Owner. + * @dev This approval is an added security to avoid development fund takeover by a compromised locked token owner. + */ + function approveLockedTokenOwner() public onlyUnlockedTokenOwner checkStatus(Status.Active) { + require(newLockedTokenOwner != address(0), "No new locked owner added."); + + emit NewLockedOwnerApproved(msg.sender, lockedTokenOwner, newLockedTokenOwner); + + lockedTokenOwner = newLockedTokenOwner; + + newLockedTokenOwner = address(0); + } + + /** + * @notice Update Unlocked Token Owner. + * @param _newUnlockedTokenOwner The new unlocked token owner. + */ + function updateUnlockedTokenOwner(address _newUnlockedTokenOwner) + public + onlyLockedTokenOwner + checkStatus(Status.Active) + { + require(_newUnlockedTokenOwner != address(0), "New unlocked token owner address invalid."); + + unlockedTokenOwner = _newUnlockedTokenOwner; + + emit UnlockedOwnerUpdated(msg.sender, _newUnlockedTokenOwner); + } + + /** + * @notice Deposit tokens to this contract. + * @param _amount the amount of tokens deposited. + * @dev These tokens can be withdrawn/transferred any time by the lockedTokenOwner. + */ + function depositTokens(uint256 _amount) public checkStatus(Status.Active) { + require(_amount > 0, "Amount needs to be bigger than zero."); + + bool txStatus = SOV.transferFrom(msg.sender, address(this), _amount); + require(txStatus, "Token transfer was not successful."); + + emit TokenDeposit(msg.sender, _amount); + } + + /** + * @notice Change the Token release schedule. It creates a completely new schedule, and does not append on the previous one. + * @param _newLastReleaseTime If the last release time is to be changed, zero if no change required. + * @param _releaseDuration The time duration between each release calculated from `lastReleaseTime` in seconds. + * @param _releaseTokenAmount The amount of token to be released in each duration/interval. + * @dev _releaseDuration and _releaseTokenAmount should be specified in reverse order of release. + */ + function changeTokenReleaseSchedule( + uint256 _newLastReleaseTime, + uint256[] memory _releaseDuration, + uint256[] memory _releaseTokenAmount + ) public onlyLockedTokenOwner checkStatus(Status.Active) { + /// Checking if the schedule duration and token allocation length matches. + require( + _releaseDuration.length == _releaseTokenAmount.length, + "Release Schedule does not match." + ); + + /// If the last release time has to be changed, then you can pass a new one here. + /// Or else, the duration of release will be calculated based on this timestamp. + /// Even a future timestamp can be mentioned here. + if (_newLastReleaseTime != 0) { + lastReleaseTime = _newLastReleaseTime; + } + + /// Checking if the contract have enough token balance for the release. + uint256 _releaseTotalTokenAmount; + for (uint256 amountIndex = 0; amountIndex < _releaseTokenAmount.length; amountIndex++) { + _releaseTotalTokenAmount = _releaseTotalTokenAmount.add( + _releaseTokenAmount[amountIndex] + ); + } + + /// Getting the current token balance of the contract. + uint256 remainingTokens = SOV.balanceOf(address(this)); + + /// If the token balance is not sufficient, then we transfer the change to contract. + if (remainingTokens < _releaseTotalTokenAmount) { + bool txStatus = + SOV.transferFrom( + msg.sender, + address(this), + _releaseTotalTokenAmount.sub(remainingTokens) + ); + require(txStatus, "Not enough token sent to change release schedule."); + } else if (remainingTokens > _releaseTotalTokenAmount) { + /// If there are more tokens than required, send the extra tokens back. + bool txStatus = + SOV.transfer(msg.sender, remainingTokens.sub(_releaseTotalTokenAmount)); + require(txStatus, "Token not received by the Locked Owner."); + } + + /// Finally we update the token release schedule. + releaseDuration = _releaseDuration; + releaseTokenAmount = _releaseTokenAmount; + + emit TokenReleaseChanged(msg.sender, _releaseDuration.length); + } + + /** + * @notice Transfers all of the remaining tokens in an emergency situation. + * @dev This could be called when governance or development fund might be compromised. + */ + function transferTokensByUnlockedTokenOwner() + public + onlyUnlockedTokenOwner + checkStatus(Status.Active) + { + uint256 remainingTokens = SOV.balanceOf(address(this)); + bool txStatus = SOV.transfer(safeVault, remainingTokens); + require(txStatus, "Token transfer was not successful. Check receiver address."); + status = Status.Expired; + + emit LockedTokenTransferByUnlockedOwner(msg.sender, safeVault, remainingTokens); + emit DevelopmentFundExpired(); + } + + /** + * @notice Withdraws all unlocked/released token. + * @param _amount The amount to be withdrawn. + */ + function withdrawTokensByUnlockedTokenOwner(uint256 _amount) + public + onlyUnlockedTokenOwner + checkStatus(Status.Active) + { + require(_amount > 0, "Zero can't be withdrawn."); + + uint256 count; /// To know how many elements to be removed from the release schedule. + uint256 amount = _amount; /// To know the total amount to be transferred. + uint256 newLastReleaseTimeMemory = lastReleaseTime; /// Better to use memory than storage. + uint256 releaseLength = releaseDuration.length.sub(1); /// Also checks if there are any elements in the release schedule. + + /// Getting the amount of tokens, the number of releases and calculating the total duration. + while ( + amount > 0 && + newLastReleaseTimeMemory.add(releaseDuration[releaseLength]) < block.timestamp + ) { + if (amount >= releaseTokenAmount[releaseLength]) { + amount = amount.sub(releaseTokenAmount[releaseLength]); + newLastReleaseTimeMemory = newLastReleaseTimeMemory.add( + releaseDuration[releaseLength] + ); + count++; + } else { + /// This will be the last case, if correct amount is passed. + releaseTokenAmount[releaseLength] = releaseTokenAmount[releaseLength].sub(amount); + amount = 0; + } + releaseLength--; + } + + /// Checking to see if atleast a single schedule was reached or not. + require(count > 0 || amount == 0, "No release schedule reached."); + + /// If locked token owner tries to send a higher amount that schedule + uint256 value = _amount.sub(amount); + + /// Now clearing up the release schedule. + releaseDuration.length -= count; + releaseTokenAmount.length -= count; + + /// Updating the last release time. + lastReleaseTime = newLastReleaseTimeMemory; + + /// Sending the amount to unlocked token owner. + bool txStatus = SOV.transfer(msg.sender, value); + require(txStatus, "Token transfer was not successful. Check receiver address."); + + emit UnlockedTokenWithdrawalByUnlockedOwner(msg.sender, value, count); + } + + /** + * @notice Transfers all of the remaining tokens by the owner maybe for an upgrade. + * @dev This could be called when the current development fund has to be upgraded. + * @param _receiver The address which receives this token transfer. + */ + function transferTokensByLockedTokenOwner(address _receiver) + public + onlyLockedTokenOwner + checkStatus(Status.Active) + { + uint256 remainingTokens = SOV.balanceOf(address(this)); + bool txStatus = SOV.transfer(_receiver, remainingTokens); + require(txStatus, "Token transfer was not successful. Check receiver address."); + status = Status.Expired; + + emit LockedTokenTransferByLockedOwner(msg.sender, _receiver, remainingTokens); + emit DevelopmentFundExpired(); + } + + /* Getter Functions */ + + /** + * @notice Function to read the current token release duration. + * @return _currentReleaseDuration The current release duration. + */ + function getReleaseDuration() public view returns (uint256[] memory _releaseTokenDuration) { + return releaseDuration; + } + + /** + * @notice Function to read the current token release amount. + * @return _currentReleaseTokenAmount The current release token amount. + */ + function getReleaseTokenAmount() + public + view + returns (uint256[] memory _currentReleaseTokenAmount) + { + return releaseTokenAmount; + } } diff --git a/contracts/governance/Vesting/GenericTokenSender.sol b/contracts/governance/Vesting/GenericTokenSender.sol index 8e49325eb..70317ff21 100644 --- a/contracts/governance/Vesting/GenericTokenSender.sol +++ b/contracts/governance/Vesting/GenericTokenSender.sol @@ -13,54 +13,54 @@ import "../../utils/AdminRole.sol"; * */ contract GenericTokenSender is AdminRole { - /* Events */ + /* Events */ - event TokensTransferred(address indexed token, address indexed receiver, uint256 amount); + event TokensTransferred(address indexed token, address indexed receiver, uint256 amount); - /* Functions */ + /* Functions */ - /** - * @notice Transfer given amounts of tokens to the given addresses. - * @param _token The address of the token. - * @param _receivers The addresses of the receivers. - * @param _amounts The amounts to be transferred. - * */ - function transferTokensUsingList( - address _token, - address[] calldata _receivers, - uint256[] calldata _amounts - ) external onlyAuthorized { - require(_receivers.length == _amounts.length, "arrays mismatch"); + /** + * @notice Transfer given amounts of tokens to the given addresses. + * @param _token The address of the token. + * @param _receivers The addresses of the receivers. + * @param _amounts The amounts to be transferred. + * */ + function transferTokensUsingList( + address _token, + address[] calldata _receivers, + uint256[] calldata _amounts + ) external onlyAuthorized { + require(_receivers.length == _amounts.length, "arrays mismatch"); - for (uint256 i = 0; i < _receivers.length; i++) { - _transferTokens(_token, _receivers[i], _amounts[i]); - } - } + for (uint256 i = 0; i < _receivers.length; i++) { + _transferTokens(_token, _receivers[i], _amounts[i]); + } + } - /** - * @notice Transfer tokens to given address. - * @param _token The address of the token. - * @param _receiver The address of the token receiver. - * @param _amount The amount to be transferred. - * */ - function transferTokens( - address _token, - address _receiver, - uint256 _amount - ) external onlyAuthorized { - _transferTokens(_token, _receiver, _amount); - } + /** + * @notice Transfer tokens to given address. + * @param _token The address of the token. + * @param _receiver The address of the token receiver. + * @param _amount The amount to be transferred. + * */ + function transferTokens( + address _token, + address _receiver, + uint256 _amount + ) external onlyAuthorized { + _transferTokens(_token, _receiver, _amount); + } - function _transferTokens( - address _token, - address _receiver, - uint256 _amount - ) internal { - require(_token != address(0), "token address invalid"); - require(_receiver != address(0), "receiver address invalid"); - require(_amount != 0, "amount invalid"); + function _transferTokens( + address _token, + address _receiver, + uint256 _amount + ) internal { + require(_token != address(0), "token address invalid"); + require(_receiver != address(0), "receiver address invalid"); + require(_amount != 0, "amount invalid"); - require(IERC20(_token).transfer(_receiver, _amount), "transfer failed"); - emit TokensTransferred(_token, _receiver, _amount); - } + require(IERC20(_token).transfer(_receiver, _amount), "transfer failed"); + emit TokensTransferred(_token, _receiver, _amount); + } } diff --git a/contracts/governance/Vesting/ITeamVesting.sol b/contracts/governance/Vesting/ITeamVesting.sol index 006d0c4ee..e2c436e6b 100644 --- a/contracts/governance/Vesting/ITeamVesting.sol +++ b/contracts/governance/Vesting/ITeamVesting.sol @@ -7,5 +7,5 @@ pragma solidity ^0.5.17; * function having the vesting contract instance address. */ interface ITeamVesting { - function governanceWithdrawTokens(address receiver) external; + function governanceWithdrawTokens(address receiver) external; } diff --git a/contracts/governance/Vesting/IVesting.sol b/contracts/governance/Vesting/IVesting.sol index 12a68af1f..f23df1635 100644 --- a/contracts/governance/Vesting/IVesting.sol +++ b/contracts/governance/Vesting/IVesting.sol @@ -8,9 +8,9 @@ pragma solidity ^0.5.17; * at a vesting instance. */ interface IVesting { - function duration() external returns (uint256); + function duration() external returns (uint256); - function endDate() external returns (uint256); + function endDate() external returns (uint256); - function stakeTokens(uint256 amount) external; + function stakeTokens(uint256 amount) external; } diff --git a/contracts/governance/Vesting/IVestingFactory.sol b/contracts/governance/Vesting/IVestingFactory.sol index 05fc882ce..054679f88 100644 --- a/contracts/governance/Vesting/IVestingFactory.sol +++ b/contracts/governance/Vesting/IVestingFactory.sol @@ -8,23 +8,23 @@ pragma solidity ^0.5.17; * and on VestingRegistry contract to use an instance of VestingFactory. */ interface IVestingFactory { - function deployVesting( - address _SOV, - address _staking, - address _tokenOwner, - uint256 _cliff, - uint256 _duration, - address _feeSharing, - address _owner - ) external returns (address); + function deployVesting( + address _SOV, + address _staking, + address _tokenOwner, + uint256 _cliff, + uint256 _duration, + address _feeSharing, + address _owner + ) external returns (address); - function deployTeamVesting( - address _SOV, - address _staking, - address _tokenOwner, - uint256 _cliff, - uint256 _duration, - address _feeSharing, - address _owner - ) external returns (address); + function deployTeamVesting( + address _SOV, + address _staking, + address _tokenOwner, + uint256 _cliff, + uint256 _duration, + address _feeSharing, + address _owner + ) external returns (address); } diff --git a/contracts/governance/Vesting/IVestingRegistry.sol b/contracts/governance/Vesting/IVestingRegistry.sol index 2fcc8a7bc..bf6b3deea 100644 --- a/contracts/governance/Vesting/IVestingRegistry.sol +++ b/contracts/governance/Vesting/IVestingRegistry.sol @@ -5,7 +5,7 @@ pragma solidity ^0.5.17; * @dev Interfaces are used to cast a contract address into a callable instance. */ interface IVestingRegistry { - function getVesting(address _tokenOwner) external view returns (address); + function getVesting(address _tokenOwner) external view returns (address); - function getTeamVesting(address _tokenOwner) external view returns (address); + function getTeamVesting(address _tokenOwner) external view returns (address); } diff --git a/contracts/governance/Vesting/OriginInvestorsClaim.sol b/contracts/governance/Vesting/OriginInvestorsClaim.sol index 636b5bef4..7ef532339 100644 --- a/contracts/governance/Vesting/OriginInvestorsClaim.sol +++ b/contracts/governance/Vesting/OriginInvestorsClaim.sol @@ -9,214 +9,234 @@ import "../Staking/Staking.sol"; * @notice // TODO: fund this contract with a total amount of SOV needed to distribute. * */ contract OriginInvestorsClaim is Ownable { - using SafeMath for uint256; - - /* Storage */ - - /// VestingRegistry public constant vestingRegistry = VestingRegistry(0x80B036ae59B3e38B573837c01BB1DB95515b7E6B); - - uint256 public totalAmount; - - /// @notice Constant used for computing the vesting dates. - uint256 public constant SOV_VESTING_CLIFF = 6 weeks; - - uint256 public kickoffTS; - uint256 public vestingTerm; - uint256 public investorsQty; - bool public investorsListInitialized; - VestingRegistry public vestingRegistry; - Staking public staking; - IERC20 public SOVToken; - - /// @dev user => flag : Whether user has admin role. - mapping(address => bool) public admins; - - /// @dev investor => Amount : Origin investors entitled to claim SOV. - mapping(address => uint256) public investorsAmountsList; - - /* Events */ - - event AdminAdded(address admin); - event AdminRemoved(address admin); - event InvestorsAmountsListAppended(uint256 qty, uint256 amount); - event ClaimVested(address indexed investor, uint256 amount); - event ClaimTransferred(address indexed investor, uint256 amount); - event InvestorsAmountsListInitialized(uint256 qty, uint256 totalAmount); - - /* Modifiers */ - - /// @dev Throws if called by any account other than the owner or admin. - modifier onlyAuthorized() { - require(isOwner() || admins[msg.sender], "OriginInvestorsClaim::onlyAuthorized: should be authorized"); - _; - } - - /// @dev Throws if called by any account not whitelisted. - modifier onlyWhitelisted() { - require(investorsAmountsList[msg.sender] != 0, "OriginInvestorsClaim::onlyWhitelisted: not whitelisted or already claimed"); - _; - } - - /// @dev Throws if called w/ an initialized investors list. - modifier notInitialized() { - require(!investorsListInitialized, "OriginInvestorsClaim::notInitialized: the investors list should not be set as initialized"); - _; - } - - /// @dev Throws if called w/ an uninitialized investors list. - modifier initialized() { - require(investorsListInitialized, "OriginInvestorsClaim::initialized: the investors list has not been set yet"); - _; - } - - /* Functions */ - - /** - * @notice Contract deployment requires one parameter: - * @param vestingRegistryAddress The vestingRegistry contract instance address. - * */ - constructor(address vestingRegistryAddress) public { - vestingRegistry = VestingRegistry(vestingRegistryAddress); - staking = Staking(vestingRegistry.staking()); - kickoffTS = staking.kickoffTS(); - SOVToken = staking.SOVToken(); - vestingTerm = kickoffTS + SOV_VESTING_CLIFF; - } - - /** - * @notice Add account to ACL. - * @param _admin The addresses of the account to grant permissions. - * */ - function addAdmin(address _admin) public onlyOwner { - admins[_admin] = true; - emit AdminAdded(_admin); - } - - /** - * @notice Remove account from ACL. - * @param _admin The addresses of the account to revoke permissions. - * */ - function removeAdmin(address _admin) public onlyOwner { - admins[_admin] = false; - emit AdminRemoved(_admin); - } - - /** - * @notice In case we have unclaimed tokens or in emergency case - * this function transfers all SOV tokens to a given address. - * @param toAddress The recipient address of all this contract tokens. - * */ - function authorizedBalanceWithdraw(address toAddress) public onlyAuthorized { - require( - SOVToken.transfer(toAddress, SOVToken.balanceOf(address(this))), - "OriginInvestorsClaim::authorizedTransferBalance: transfer failed" - ); - } - - /** - * @notice Should be called after the investors list setup completed. - * This function checks whether the SOV token balance of the contract is - * enough and sets status list to initialized. - * */ - function setInvestorsAmountsListInitialized() public onlyAuthorized notInitialized { - require( - SOVToken.balanceOf(address(this)) >= totalAmount, - "OriginInvestorsClaim::setInvestorsAmountsList: the contract is not enough financed" - ); - - investorsListInitialized = true; - - emit InvestorsAmountsListInitialized(investorsQty, totalAmount); - } - - /** - * @notice The contract should be approved or transferred necessary - * amount of SOV prior to calling the function. - * @param investors The list of investors addresses to add to the list. - * Duplicates will be skipped. - * @param claimAmounts The list of amounts for investors investors[i] - * will receive claimAmounts[i] of SOV. - * */ - function appendInvestorsAmountsList(address[] calldata investors, uint256[] calldata claimAmounts) - external - onlyAuthorized - notInitialized - { - uint256 subQty; - uint256 sumAmount; - require( - investors.length == claimAmounts.length, - "OriginInvestorsClaim::appendInvestorsAmountsList: investors.length != claimAmounts.length" - ); - - for (uint256 i = 0; i < investors.length; i++) { - if (investorsAmountsList[investors[i]] == 0) { - investorsAmountsList[investors[i]] = claimAmounts[i]; - sumAmount = sumAmount.add(claimAmounts[i]); - } else { - subQty = subQty.add(1); - } - } - - investorsQty = investorsQty.add(investors.length.sub(subQty)); - totalAmount = totalAmount.add(sumAmount); - emit InvestorsAmountsListAppended(investors.length.sub(subQty), sumAmount); - } - - /** - * @notice Claim tokens from this contract. - * If vestingTerm is not yet achieved a vesting is created. - * Otherwise tokens are tranferred. - * */ - function claim() external onlyWhitelisted initialized { - if (now < vestingTerm) { - createVesting(); - } else { - transfer(); - } - } - - /** - * @notice Transfer tokens from this contract to a vestingRegistry contract. - * Sender is removed from investor list and all its unvested tokens - * are sent to vesting contract. - * */ - function createVesting() internal { - uint256 cliff = vestingTerm.sub(now); - uint256 duration = cliff; - uint256 amount = investorsAmountsList[msg.sender]; - address vestingContractAddress; - - vestingContractAddress = vestingRegistry.getVesting(msg.sender); - require(vestingContractAddress == address(0), "OriginInvestorsClaim::withdraw: the claimer has an active vesting contract"); - - delete investorsAmountsList[msg.sender]; - - vestingRegistry.createVesting(msg.sender, amount, cliff, duration); - vestingContractAddress = vestingRegistry.getVesting(msg.sender); - require(SOVToken.transfer(address(vestingRegistry), amount), "OriginInvestorsClaim::withdraw: SOV transfer failed"); - vestingRegistry.stakeTokens(vestingContractAddress, amount); - - emit ClaimVested(msg.sender, amount); - } - - /** - * @notice Transfer tokens from this contract to the sender. - * Sender is removed from investor list and all its unvested tokens - * are sent to its account. - * */ - function transfer() internal { - uint256 amount = investorsAmountsList[msg.sender]; - - delete investorsAmountsList[msg.sender]; - - /** - * @dev Withdraw only for those claiming after the cliff, i.e. without vesting contracts. - * Those with vestingContracts should withdraw using Vesting.withdrawTokens - * from Vesting (VestingLogic) contract. - * */ - require(SOVToken.transfer(msg.sender, amount), "OriginInvestorsClaim::withdraw: SOV transfer failed"); - - emit ClaimTransferred(msg.sender, amount); - } + using SafeMath for uint256; + + /* Storage */ + + /// VestingRegistry public constant vestingRegistry = VestingRegistry(0x80B036ae59B3e38B573837c01BB1DB95515b7E6B); + + uint256 public totalAmount; + + /// @notice Constant used for computing the vesting dates. + uint256 public constant SOV_VESTING_CLIFF = 6 weeks; + + uint256 public kickoffTS; + uint256 public vestingTerm; + uint256 public investorsQty; + bool public investorsListInitialized; + VestingRegistry public vestingRegistry; + Staking public staking; + IERC20 public SOVToken; + + /// @dev user => flag : Whether user has admin role. + mapping(address => bool) public admins; + + /// @dev investor => Amount : Origin investors entitled to claim SOV. + mapping(address => uint256) public investorsAmountsList; + + /* Events */ + + event AdminAdded(address admin); + event AdminRemoved(address admin); + event InvestorsAmountsListAppended(uint256 qty, uint256 amount); + event ClaimVested(address indexed investor, uint256 amount); + event ClaimTransferred(address indexed investor, uint256 amount); + event InvestorsAmountsListInitialized(uint256 qty, uint256 totalAmount); + + /* Modifiers */ + + /// @dev Throws if called by any account other than the owner or admin. + modifier onlyAuthorized() { + require( + isOwner() || admins[msg.sender], + "OriginInvestorsClaim::onlyAuthorized: should be authorized" + ); + _; + } + + /// @dev Throws if called by any account not whitelisted. + modifier onlyWhitelisted() { + require( + investorsAmountsList[msg.sender] != 0, + "OriginInvestorsClaim::onlyWhitelisted: not whitelisted or already claimed" + ); + _; + } + + /// @dev Throws if called w/ an initialized investors list. + modifier notInitialized() { + require( + !investorsListInitialized, + "OriginInvestorsClaim::notInitialized: the investors list should not be set as initialized" + ); + _; + } + + /// @dev Throws if called w/ an uninitialized investors list. + modifier initialized() { + require( + investorsListInitialized, + "OriginInvestorsClaim::initialized: the investors list has not been set yet" + ); + _; + } + + /* Functions */ + + /** + * @notice Contract deployment requires one parameter: + * @param vestingRegistryAddress The vestingRegistry contract instance address. + * */ + constructor(address vestingRegistryAddress) public { + vestingRegistry = VestingRegistry(vestingRegistryAddress); + staking = Staking(vestingRegistry.staking()); + kickoffTS = staking.kickoffTS(); + SOVToken = staking.SOVToken(); + vestingTerm = kickoffTS + SOV_VESTING_CLIFF; + } + + /** + * @notice Add account to ACL. + * @param _admin The addresses of the account to grant permissions. + * */ + function addAdmin(address _admin) public onlyOwner { + admins[_admin] = true; + emit AdminAdded(_admin); + } + + /** + * @notice Remove account from ACL. + * @param _admin The addresses of the account to revoke permissions. + * */ + function removeAdmin(address _admin) public onlyOwner { + admins[_admin] = false; + emit AdminRemoved(_admin); + } + + /** + * @notice In case we have unclaimed tokens or in emergency case + * this function transfers all SOV tokens to a given address. + * @param toAddress The recipient address of all this contract tokens. + * */ + function authorizedBalanceWithdraw(address toAddress) public onlyAuthorized { + require( + SOVToken.transfer(toAddress, SOVToken.balanceOf(address(this))), + "OriginInvestorsClaim::authorizedTransferBalance: transfer failed" + ); + } + + /** + * @notice Should be called after the investors list setup completed. + * This function checks whether the SOV token balance of the contract is + * enough and sets status list to initialized. + * */ + function setInvestorsAmountsListInitialized() public onlyAuthorized notInitialized { + require( + SOVToken.balanceOf(address(this)) >= totalAmount, + "OriginInvestorsClaim::setInvestorsAmountsList: the contract is not enough financed" + ); + + investorsListInitialized = true; + + emit InvestorsAmountsListInitialized(investorsQty, totalAmount); + } + + /** + * @notice The contract should be approved or transferred necessary + * amount of SOV prior to calling the function. + * @param investors The list of investors addresses to add to the list. + * Duplicates will be skipped. + * @param claimAmounts The list of amounts for investors investors[i] + * will receive claimAmounts[i] of SOV. + * */ + function appendInvestorsAmountsList( + address[] calldata investors, + uint256[] calldata claimAmounts + ) external onlyAuthorized notInitialized { + uint256 subQty; + uint256 sumAmount; + require( + investors.length == claimAmounts.length, + "OriginInvestorsClaim::appendInvestorsAmountsList: investors.length != claimAmounts.length" + ); + + for (uint256 i = 0; i < investors.length; i++) { + if (investorsAmountsList[investors[i]] == 0) { + investorsAmountsList[investors[i]] = claimAmounts[i]; + sumAmount = sumAmount.add(claimAmounts[i]); + } else { + subQty = subQty.add(1); + } + } + + investorsQty = investorsQty.add(investors.length.sub(subQty)); + totalAmount = totalAmount.add(sumAmount); + emit InvestorsAmountsListAppended(investors.length.sub(subQty), sumAmount); + } + + /** + * @notice Claim tokens from this contract. + * If vestingTerm is not yet achieved a vesting is created. + * Otherwise tokens are tranferred. + * */ + function claim() external onlyWhitelisted initialized { + if (now < vestingTerm) { + createVesting(); + } else { + transfer(); + } + } + + /** + * @notice Transfer tokens from this contract to a vestingRegistry contract. + * Sender is removed from investor list and all its unvested tokens + * are sent to vesting contract. + * */ + function createVesting() internal { + uint256 cliff = vestingTerm.sub(now); + uint256 duration = cliff; + uint256 amount = investorsAmountsList[msg.sender]; + address vestingContractAddress; + + vestingContractAddress = vestingRegistry.getVesting(msg.sender); + require( + vestingContractAddress == address(0), + "OriginInvestorsClaim::withdraw: the claimer has an active vesting contract" + ); + + delete investorsAmountsList[msg.sender]; + + vestingRegistry.createVesting(msg.sender, amount, cliff, duration); + vestingContractAddress = vestingRegistry.getVesting(msg.sender); + require( + SOVToken.transfer(address(vestingRegistry), amount), + "OriginInvestorsClaim::withdraw: SOV transfer failed" + ); + vestingRegistry.stakeTokens(vestingContractAddress, amount); + + emit ClaimVested(msg.sender, amount); + } + + /** + * @notice Transfer tokens from this contract to the sender. + * Sender is removed from investor list and all its unvested tokens + * are sent to its account. + * */ + function transfer() internal { + uint256 amount = investorsAmountsList[msg.sender]; + + delete investorsAmountsList[msg.sender]; + + /** + * @dev Withdraw only for those claiming after the cliff, i.e. without vesting contracts. + * Those with vestingContracts should withdraw using Vesting.withdrawTokens + * from Vesting (VestingLogic) contract. + * */ + require( + SOVToken.transfer(msg.sender, amount), + "OriginInvestorsClaim::withdraw: SOV transfer failed" + ); + + emit ClaimTransferred(msg.sender, amount); + } } diff --git a/contracts/governance/Vesting/OrigingVestingCreator.sol b/contracts/governance/Vesting/OrigingVestingCreator.sol index a14dab7b0..d1aee2dcf 100644 --- a/contracts/governance/Vesting/OrigingVestingCreator.sol +++ b/contracts/governance/Vesting/OrigingVestingCreator.sol @@ -10,34 +10,34 @@ import "./VestingRegistry.sol"; * function it creates a vesting, gets it and stakes some tokens w/ this vesting. * */ contract OrigingVestingCreator is Ownable { - VestingRegistry public vestingRegistry; + VestingRegistry public vestingRegistry; - mapping(address => bool) processedList; + mapping(address => bool) processedList; - constructor(address _vestingRegistry) public { - vestingRegistry = VestingRegistry(_vestingRegistry); - } + constructor(address _vestingRegistry) public { + vestingRegistry = VestingRegistry(_vestingRegistry); + } - /** - * @notice Create a vesting, get it and stake some tokens w/ this vesting. - * @param _tokenOwner The owner of the tokens. - * @param _amount The amount of tokens to be vested. - * @param _cliff The time interval to the first withdraw in seconds. - * @param _duration The total duration in seconds. - * */ - function createVesting( - address _tokenOwner, - uint256 _amount, - uint256 _cliff, - uint256 _duration - ) public onlyOwner { - require(_tokenOwner != address(0), "Invalid address"); - require(!processedList[_tokenOwner], "Already processed"); + /** + * @notice Create a vesting, get it and stake some tokens w/ this vesting. + * @param _tokenOwner The owner of the tokens. + * @param _amount The amount of tokens to be vested. + * @param _cliff The time interval to the first withdraw in seconds. + * @param _duration The total duration in seconds. + * */ + function createVesting( + address _tokenOwner, + uint256 _amount, + uint256 _cliff, + uint256 _duration + ) public onlyOwner { + require(_tokenOwner != address(0), "Invalid address"); + require(!processedList[_tokenOwner], "Already processed"); - processedList[_tokenOwner] = true; + processedList[_tokenOwner] = true; - vestingRegistry.createVesting(_tokenOwner, _amount, _cliff, _duration); - address vesting = vestingRegistry.getVesting(_tokenOwner); - vestingRegistry.stakeTokens(vesting, _amount); - } + vestingRegistry.createVesting(_tokenOwner, _amount, _cliff, _duration); + address vesting = vestingRegistry.getVesting(_tokenOwner); + vestingRegistry.stakeTokens(vesting, _amount); + } } diff --git a/contracts/governance/Vesting/SVR.sol b/contracts/governance/Vesting/SVR.sol index 0089400d0..6d54e044b 100644 --- a/contracts/governance/Vesting/SVR.sol +++ b/contracts/governance/Vesting/SVR.sol @@ -22,124 +22,124 @@ import "../ApprovalReceiver.sol"; * 1 year duration. * */ contract SVR is ERC20, ERC20Detailed, Ownable, SafeMath96, ApprovalReceiver { - /* Storage */ - - string constant NAME = "Sovryn Vesting Reward Token"; - string constant SYMBOL = "SVR"; - uint8 constant DECIMALS = 18; - - /// @notice Constants used for computing the vesting dates. - uint256 constant FOUR_WEEKS = 4 weeks; - uint256 constant YEAR = 52 weeks; - /// @notice Amount of tokens divided by this constant will be transferred. - uint96 constant DIRECT_TRANSFER_PART = 14; - - /// @notice The SOV token contract. - IERC20_ public SOV; - /// @notice The staking contract. - IStaking public staking; - - /* Events */ - - event Mint(address indexed sender, uint256 amount); - event Burn(address indexed sender, uint256 amount); - - /* Functions */ - - /** - * @notice Create reward token RSOV. - * @param _SOV The SOV token address. - * @param _staking The staking contract address. - * */ - constructor(address _SOV, address _staking) public ERC20Detailed(NAME, SYMBOL, DECIMALS) { - require(_SOV != address(0), "SVR::SOV address invalid"); - require(_staking != address(0), "SVR::staking address invalid"); - - SOV = IERC20_(_SOV); - staking = IStaking(_staking); - } - - /** - * @notice Hold SOV tokens and mint the respective amount of SVR tokens. - * @param _amount The amount of tokens to be mint. - * */ - function mint(uint96 _amount) public { - _mintTo(msg.sender, _amount); - } - - /** - * @notice Hold SOV tokens and mint the respective amount of SVR tokens. - * @dev This function will be invoked from receiveApproval. - * @dev SOV.approveAndCall -> this.receiveApproval -> this.mintWithApproval - * @param _sender The sender of SOV.approveAndCall - * @param _amount The amount of tokens to be mint. - * */ - function mintWithApproval(address _sender, uint96 _amount) public onlyThisContract { - _mintTo(_sender, _amount); - } - - /** - * @notice The actual minting process, holding SOV and minting RSOV tokens. - * @param _sender The recipient of the minted tokens. - * @param _amount The amount of tokens to be minted. - * */ - function _mintTo(address _sender, uint96 _amount) internal { - require(_amount > 0, "SVR::mint: amount invalid"); - - /// @notice Holds SOV tokens. - bool success = SOV.transferFrom(_sender, address(this), _amount); - require(success); - - /// @notice Mints SVR tokens. - /// @dev uses openzeppelin/ERC20.sol internal _mint function - _mint(_sender, _amount); - - emit Mint(_sender, _amount); - } - - /** - * @notice burns SVR tokens and stakes the respective amount SOV tokens in the user's behalf - * @param _amount the amount of tokens to be burnt - */ - function burn(uint96 _amount) public { - require(_amount > 0, "SVR:: burn: amount invalid"); - - /// @notice Burns RSOV tokens. - _burn(msg.sender, _amount); - - /// @notice Transfer 1/14 of amount directly to the user. - /// If amount is too small it won't be transferred. - uint96 transferAmount = _amount / DIRECT_TRANSFER_PART; - if (transferAmount > 0) { - SOV.transfer(msg.sender, transferAmount); - _amount -= transferAmount; - } - - /// @notice Stakes SOV tokens in the user's behalf. - SOV.approve(address(staking), _amount); - - staking.stakesBySchedule(_amount, FOUR_WEEKS, YEAR, FOUR_WEEKS, msg.sender, msg.sender); - - emit Burn(msg.sender, _amount); - } - - /** - * @notice Override default ApprovalReceiver._getToken function to - * register SOV token on this contract. - * @return The address of SOV token. - * */ - function _getToken() internal view returns (address) { - return address(SOV); - } - - /** - * @notice Override default ApprovalReceiver._getSelectors function to - * register mintWithApproval selector on this contract. - * @return The array of registered selectors on this contract. - * */ - function _getSelectors() internal view returns (bytes4[] memory) { - bytes4[] memory selectors = new bytes4[](1); - selectors[0] = this.mintWithApproval.selector; - return selectors; - } + /* Storage */ + + string constant NAME = "Sovryn Vesting Reward Token"; + string constant SYMBOL = "SVR"; + uint8 constant DECIMALS = 18; + + /// @notice Constants used for computing the vesting dates. + uint256 constant FOUR_WEEKS = 4 weeks; + uint256 constant YEAR = 52 weeks; + /// @notice Amount of tokens divided by this constant will be transferred. + uint96 constant DIRECT_TRANSFER_PART = 14; + + /// @notice The SOV token contract. + IERC20_ public SOV; + /// @notice The staking contract. + IStaking public staking; + + /* Events */ + + event Mint(address indexed sender, uint256 amount); + event Burn(address indexed sender, uint256 amount); + + /* Functions */ + + /** + * @notice Create reward token RSOV. + * @param _SOV The SOV token address. + * @param _staking The staking contract address. + * */ + constructor(address _SOV, address _staking) public ERC20Detailed(NAME, SYMBOL, DECIMALS) { + require(_SOV != address(0), "SVR::SOV address invalid"); + require(_staking != address(0), "SVR::staking address invalid"); + + SOV = IERC20_(_SOV); + staking = IStaking(_staking); + } + + /** + * @notice Hold SOV tokens and mint the respective amount of SVR tokens. + * @param _amount The amount of tokens to be mint. + * */ + function mint(uint96 _amount) public { + _mintTo(msg.sender, _amount); + } + + /** + * @notice Hold SOV tokens and mint the respective amount of SVR tokens. + * @dev This function will be invoked from receiveApproval. + * @dev SOV.approveAndCall -> this.receiveApproval -> this.mintWithApproval + * @param _sender The sender of SOV.approveAndCall + * @param _amount The amount of tokens to be mint. + * */ + function mintWithApproval(address _sender, uint96 _amount) public onlyThisContract { + _mintTo(_sender, _amount); + } + + /** + * @notice The actual minting process, holding SOV and minting RSOV tokens. + * @param _sender The recipient of the minted tokens. + * @param _amount The amount of tokens to be minted. + * */ + function _mintTo(address _sender, uint96 _amount) internal { + require(_amount > 0, "SVR::mint: amount invalid"); + + /// @notice Holds SOV tokens. + bool success = SOV.transferFrom(_sender, address(this), _amount); + require(success); + + /// @notice Mints SVR tokens. + /// @dev uses openzeppelin/ERC20.sol internal _mint function + _mint(_sender, _amount); + + emit Mint(_sender, _amount); + } + + /** + * @notice burns SVR tokens and stakes the respective amount SOV tokens in the user's behalf + * @param _amount the amount of tokens to be burnt + */ + function burn(uint96 _amount) public { + require(_amount > 0, "SVR:: burn: amount invalid"); + + /// @notice Burns RSOV tokens. + _burn(msg.sender, _amount); + + /// @notice Transfer 1/14 of amount directly to the user. + /// If amount is too small it won't be transferred. + uint96 transferAmount = _amount / DIRECT_TRANSFER_PART; + if (transferAmount > 0) { + SOV.transfer(msg.sender, transferAmount); + _amount -= transferAmount; + } + + /// @notice Stakes SOV tokens in the user's behalf. + SOV.approve(address(staking), _amount); + + staking.stakesBySchedule(_amount, FOUR_WEEKS, YEAR, FOUR_WEEKS, msg.sender, msg.sender); + + emit Burn(msg.sender, _amount); + } + + /** + * @notice Override default ApprovalReceiver._getToken function to + * register SOV token on this contract. + * @return The address of SOV token. + * */ + function _getToken() internal view returns (address) { + return address(SOV); + } + + /** + * @notice Override default ApprovalReceiver._getSelectors function to + * register mintWithApproval selector on this contract. + * @return The array of registered selectors on this contract. + * */ + function _getSelectors() internal view returns (bytes4[] memory) { + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = this.mintWithApproval.selector; + return selectors; + } } diff --git a/contracts/governance/Vesting/TeamVesting.sol b/contracts/governance/Vesting/TeamVesting.sol index 1e8f98d17..11185eb29 100644 --- a/contracts/governance/Vesting/TeamVesting.sol +++ b/contracts/governance/Vesting/TeamVesting.sol @@ -20,36 +20,36 @@ import "../../proxy/Proxy.sol"; * use Proxy instead of UpgradableProxy. * */ contract TeamVesting is VestingStorage, Proxy { - /** - * @notice Setup the vesting schedule. - * @param _logic The address of logic contract. - * @param _SOV The SOV token address. - * @param _tokenOwner The owner of the tokens. - * @param _cliff The time interval to the first withdraw in seconds. - * @param _duration The total duration in seconds. - * */ - constructor( - address _logic, - address _SOV, - address _stakingAddress, - address _tokenOwner, - uint256 _cliff, - uint256 _duration, - address _feeSharingProxy - ) public { - require(_SOV != address(0), "SOV address invalid"); - require(_stakingAddress != address(0), "staking address invalid"); - require(_tokenOwner != address(0), "token owner address invalid"); - require(_duration >= _cliff, "duration must be bigger than or equal to the cliff"); - require(_feeSharingProxy != address(0), "feeSharingProxy address invalid"); + /** + * @notice Setup the vesting schedule. + * @param _logic The address of logic contract. + * @param _SOV The SOV token address. + * @param _tokenOwner The owner of the tokens. + * @param _cliff The time interval to the first withdraw in seconds. + * @param _duration The total duration in seconds. + * */ + constructor( + address _logic, + address _SOV, + address _stakingAddress, + address _tokenOwner, + uint256 _cliff, + uint256 _duration, + address _feeSharingProxy + ) public { + require(_SOV != address(0), "SOV address invalid"); + require(_stakingAddress != address(0), "staking address invalid"); + require(_tokenOwner != address(0), "token owner address invalid"); + require(_duration >= _cliff, "duration must be bigger than or equal to the cliff"); + require(_feeSharingProxy != address(0), "feeSharingProxy address invalid"); - _setImplementation(_logic); - SOV = IERC20(_SOV); - staking = Staking(_stakingAddress); - require(_duration <= staking.MAX_DURATION(), "duration may not exceed the max duration"); - tokenOwner = _tokenOwner; - cliff = _cliff; - duration = _duration; - feeSharingProxy = IFeeSharingProxy(_feeSharingProxy); - } + _setImplementation(_logic); + SOV = IERC20(_SOV); + staking = Staking(_stakingAddress); + require(_duration <= staking.MAX_DURATION(), "duration may not exceed the max duration"); + tokenOwner = _tokenOwner; + cliff = _cliff; + duration = _duration; + feeSharingProxy = IFeeSharingProxy(_feeSharingProxy); + } } diff --git a/contracts/governance/Vesting/TokenSender.sol b/contracts/governance/Vesting/TokenSender.sol index f3a5a32ac..6f5b149db 100644 --- a/contracts/governance/Vesting/TokenSender.sol +++ b/contracts/governance/Vesting/TokenSender.sol @@ -12,85 +12,88 @@ import "../../interfaces/IERC20.sol"; * */ contract TokenSender is Ownable { - /* Storage */ - - /// @notice The SOV token contract. - address public SOV; - - /// @dev user => flag whether user has admin role - mapping(address => bool) public admins; - - /* Events */ - - event SOVTransferred(address indexed receiver, uint256 amount); - event AdminAdded(address admin); - event AdminRemoved(address admin); - - /* Functions */ - - constructor(address _SOV) public { - require(_SOV != address(0), "SOV address invalid"); - - SOV = _SOV; - } - - /* Modifiers */ - - /** - * @dev Throws if called by any account other than the owner or admin. - * */ - modifier onlyAuthorized() { - require(isOwner() || admins[msg.sender], "unauthorized"); - _; - } - - /* Functions */ - - /** - * @notice Add account to ACL. - * @param _admin The addresses of the account to grant permissions. - * */ - function addAdmin(address _admin) public onlyOwner { - admins[_admin] = true; - emit AdminAdded(_admin); - } - - /** - * @notice Remove account from ACL. - * @param _admin The addresses of the account to revoke permissions. - * */ - function removeAdmin(address _admin) public onlyOwner { - admins[_admin] = false; - emit AdminRemoved(_admin); - } - - /** - * @notice Transfer given amounts of SOV to the given addresses. - * @param _receivers The addresses of the SOV receivers. - * @param _amounts The amounts to be transferred. - * */ - function transferSOVusingList(address[] memory _receivers, uint256[] memory _amounts) public onlyAuthorized { - require(_receivers.length == _amounts.length, "arrays mismatch"); - - for (uint256 i = 0; i < _receivers.length; i++) { - _transferSOV(_receivers[i], _amounts[i]); - } - } - - /** - * @notice Transfer SOV tokens to given address. - * @param _receiver The address of the SOV receiver. - * @param _amount The amount to be transferred. - * */ - function transferSOV(address _receiver, uint256 _amount) public onlyAuthorized { - _transferSOV(_receiver, _amount); - } - - function _transferSOV(address _receiver, uint256 _amount) internal { - require(_receiver != address(0), "receiver address invalid"); - require(_amount != 0, "amount invalid"); - - require(IERC20(SOV).transfer(_receiver, _amount), "transfer failed"); - emit SOVTransferred(_receiver, _amount); - } + /* Storage */ + + /// @notice The SOV token contract. + address public SOV; + + /// @dev user => flag whether user has admin role + mapping(address => bool) public admins; + + /* Events */ + + event SOVTransferred(address indexed receiver, uint256 amount); + event AdminAdded(address admin); + event AdminRemoved(address admin); + + /* Functions */ + + constructor(address _SOV) public { + require(_SOV != address(0), "SOV address invalid"); + + SOV = _SOV; + } + + /* Modifiers */ + + /** + * @dev Throws if called by any account other than the owner or admin. + * */ + modifier onlyAuthorized() { + require(isOwner() || admins[msg.sender], "unauthorized"); + _; + } + + /* Functions */ + + /** + * @notice Add account to ACL. + * @param _admin The addresses of the account to grant permissions. + * */ + function addAdmin(address _admin) public onlyOwner { + admins[_admin] = true; + emit AdminAdded(_admin); + } + + /** + * @notice Remove account from ACL. + * @param _admin The addresses of the account to revoke permissions. + * */ + function removeAdmin(address _admin) public onlyOwner { + admins[_admin] = false; + emit AdminRemoved(_admin); + } + + /** + * @notice Transfer given amounts of SOV to the given addresses. + * @param _receivers The addresses of the SOV receivers. + * @param _amounts The amounts to be transferred. + * */ + function transferSOVusingList(address[] memory _receivers, uint256[] memory _amounts) + public + onlyAuthorized + { + require(_receivers.length == _amounts.length, "arrays mismatch"); + + for (uint256 i = 0; i < _receivers.length; i++) { + _transferSOV(_receivers[i], _amounts[i]); + } + } + + /** + * @notice Transfer SOV tokens to given address. + * @param _receiver The address of the SOV receiver. + * @param _amount The amount to be transferred. + * */ + function transferSOV(address _receiver, uint256 _amount) public onlyAuthorized { + _transferSOV(_receiver, _amount); + } + + function _transferSOV(address _receiver, uint256 _amount) internal { + require(_receiver != address(0), "receiver address invalid"); + require(_amount != 0, "amount invalid"); + + require(IERC20(SOV).transfer(_receiver, _amount), "transfer failed"); + emit SOVTransferred(_receiver, _amount); + } } diff --git a/contracts/governance/Vesting/Vesting.sol b/contracts/governance/Vesting/Vesting.sol index cd3573d6a..3d930497a 100644 --- a/contracts/governance/Vesting/Vesting.sol +++ b/contracts/governance/Vesting/Vesting.sol @@ -11,29 +11,40 @@ import "./TeamVesting.sol"; * @dev TODO add tests for governanceWithdrawTokens. * */ contract Vesting is TeamVesting { - /** - * @notice Setup the vesting schedule. - * @param _logic The address of logic contract. - * @param _SOV The SOV token address. - * @param _tokenOwner The owner of the tokens. - * @param _cliff The time interval to the first withdraw in seconds. - * @param _duration The total duration in seconds. - * */ - constructor( - address _logic, - address _SOV, - address _stakingAddress, - address _tokenOwner, - uint256 _cliff, - uint256 _duration, - address _feeSharingProxy - ) public TeamVesting(_logic, _SOV, _stakingAddress, _tokenOwner, _cliff, _duration, _feeSharingProxy) {} + /** + * @notice Setup the vesting schedule. + * @param _logic The address of logic contract. + * @param _SOV The SOV token address. + * @param _tokenOwner The owner of the tokens. + * @param _cliff The time interval to the first withdraw in seconds. + * @param _duration The total duration in seconds. + * */ + constructor( + address _logic, + address _SOV, + address _stakingAddress, + address _tokenOwner, + uint256 _cliff, + uint256 _duration, + address _feeSharingProxy + ) + public + TeamVesting( + _logic, + _SOV, + _stakingAddress, + _tokenOwner, + _cliff, + _duration, + _feeSharingProxy + ) + {} - /** - * @dev We need to add this implementation to prevent proxy call VestingLogic.governanceWithdrawTokens - * @param receiver The receiver of the token withdrawal. - * */ - function governanceWithdrawTokens(address receiver) public { - revert("operation not supported"); - } + /** + * @dev We need to add this implementation to prevent proxy call VestingLogic.governanceWithdrawTokens + * @param receiver The receiver of the token withdrawal. + * */ + function governanceWithdrawTokens(address receiver) public { + revert("operation not supported"); + } } diff --git a/contracts/governance/Vesting/VestingCreator.sol b/contracts/governance/Vesting/VestingCreator.sol index eebb5204d..bbe98026b 100644 --- a/contracts/governance/Vesting/VestingCreator.sol +++ b/contracts/governance/Vesting/VestingCreator.sol @@ -7,278 +7,297 @@ import "./VestingLogic.sol"; import "../../openzeppelin/SafeMath.sol"; contract VestingCreator is AdminRole { - using SafeMath for uint256; + using SafeMath for uint256; - ///@notice Boolean to check both vesting creation and staking is completed for a record - bool vestingCreated; + ///@notice Boolean to check both vesting creation and staking is completed for a record + bool vestingCreated; - /// @notice 2 weeks in seconds. - uint256 public constant TWO_WEEKS = 2 weeks; + /// @notice 2 weeks in seconds. + uint256 public constant TWO_WEEKS = 2 weeks; - ///@notice the SOV token contract - IERC20 public SOV; + ///@notice the SOV token contract + IERC20 public SOV; - ///@notice the vesting registry contract - VestingRegistryLogic public vestingRegistryLogic; + ///@notice the vesting registry contract + VestingRegistryLogic public vestingRegistryLogic; - ///@notice Holds Vesting Data - struct VestingData { - uint256 amount; - uint256 cliff; - uint256 duration; - bool governanceControl; ///@dev true - tokens can be withdrawn by governance - address tokenOwner; - uint256 vestingCreationType; - } + ///@notice Holds Vesting Data + struct VestingData { + uint256 amount; + uint256 cliff; + uint256 duration; + bool governanceControl; ///@dev true - tokens can be withdrawn by governance + address tokenOwner; + uint256 vestingCreationType; + } - ///@notice list of vesting to be processed - VestingData[] public vestingDataList; + ///@notice list of vesting to be processed + VestingData[] public vestingDataList; - event SOVTransferred(address indexed receiver, uint256 amount); - event TokensStaked(address indexed vesting, address indexed tokenOwner, uint256 amount); - event VestingDataRemoved(address indexed caller, address indexed tokenOwner); - event DataCleared(address indexed caller); + event SOVTransferred(address indexed receiver, uint256 amount); + event TokensStaked(address indexed vesting, address indexed tokenOwner, uint256 amount); + event VestingDataRemoved(address indexed caller, address indexed tokenOwner); + event DataCleared(address indexed caller); - constructor(address _SOV, address _vestingRegistryProxy) public { - require(_SOV != address(0), "SOV address invalid"); - require(_vestingRegistryProxy != address(0), "Vesting registry address invalid"); + constructor(address _SOV, address _vestingRegistryProxy) public { + require(_SOV != address(0), "SOV address invalid"); + require(_vestingRegistryProxy != address(0), "Vesting registry address invalid"); - SOV = IERC20(_SOV); - vestingRegistryLogic = VestingRegistryLogic(_vestingRegistryProxy); - } + SOV = IERC20(_SOV); + vestingRegistryLogic = VestingRegistryLogic(_vestingRegistryProxy); + } - /** - * @notice transfers SOV tokens to given address - * @param _receiver the address of the SOV receiver - * @param _amount the amount to be transferred - */ - function transferSOV(address _receiver, uint256 _amount) external onlyOwner { - require(_amount != 0, "amount invalid"); - require(SOV.transfer(_receiver, _amount), "transfer failed"); - emit SOVTransferred(_receiver, _amount); - } + /** + * @notice transfers SOV tokens to given address + * @param _receiver the address of the SOV receiver + * @param _amount the amount to be transferred + */ + function transferSOV(address _receiver, uint256 _amount) external onlyOwner { + require(_amount != 0, "amount invalid"); + require(SOV.transfer(_receiver, _amount), "transfer failed"); + emit SOVTransferred(_receiver, _amount); + } - /** - * @notice adds vestings to be processed to the list - */ - function addVestings( - address[] calldata _tokenOwners, - uint256[] calldata _amounts, - uint256[] calldata _cliffs, - uint256[] calldata _durations, - bool[] calldata _governanceControls, - uint256[] calldata _vestingCreationTypes - ) external onlyAuthorized { - require( - _tokenOwners.length == _amounts.length && - _tokenOwners.length == _cliffs.length && - _tokenOwners.length == _durations.length && - _tokenOwners.length == _governanceControls.length, - "arrays mismatch" - ); + /** + * @notice adds vestings to be processed to the list + */ + function addVestings( + address[] calldata _tokenOwners, + uint256[] calldata _amounts, + uint256[] calldata _cliffs, + uint256[] calldata _durations, + bool[] calldata _governanceControls, + uint256[] calldata _vestingCreationTypes + ) external onlyAuthorized { + require( + _tokenOwners.length == _amounts.length && + _tokenOwners.length == _cliffs.length && + _tokenOwners.length == _durations.length && + _tokenOwners.length == _governanceControls.length, + "arrays mismatch" + ); - for (uint256 i = 0; i < _tokenOwners.length; i++) { - require(_durations[i] >= _cliffs[i], "duration must be bigger than or equal to the cliff"); - require(_amounts[i] > 0, "vesting amount cannot be 0"); - require(_tokenOwners[i] != address(0), "token owner cannot be 0 address"); - require(_cliffs[i].mod(TWO_WEEKS) == 0, "cliffs should have intervals of two weeks"); - require(_durations[i].mod(TWO_WEEKS) == 0, "durations should have intervals of two weeks"); - VestingData memory vestingData = - VestingData({ - amount: _amounts[i], - cliff: _cliffs[i], - duration: _durations[i], - governanceControl: _governanceControls[i], - tokenOwner: _tokenOwners[i], - vestingCreationType: _vestingCreationTypes[i] - }); - vestingDataList.push(vestingData); - } - } + for (uint256 i = 0; i < _tokenOwners.length; i++) { + require( + _durations[i] >= _cliffs[i], + "duration must be bigger than or equal to the cliff" + ); + require(_amounts[i] > 0, "vesting amount cannot be 0"); + require(_tokenOwners[i] != address(0), "token owner cannot be 0 address"); + require(_cliffs[i].mod(TWO_WEEKS) == 0, "cliffs should have intervals of two weeks"); + require( + _durations[i].mod(TWO_WEEKS) == 0, + "durations should have intervals of two weeks" + ); + VestingData memory vestingData = + VestingData({ + amount: _amounts[i], + cliff: _cliffs[i], + duration: _durations[i], + governanceControl: _governanceControls[i], + tokenOwner: _tokenOwners[i], + vestingCreationType: _vestingCreationTypes[i] + }); + vestingDataList.push(vestingData); + } + } - /** - * @notice Creates vesting contract and stakes tokens - * @dev Vesting and Staking are merged for calls that fits the gas limit - */ - function processNextVesting() external { - processVestingCreation(); - processStaking(); - } + /** + * @notice Creates vesting contract and stakes tokens + * @dev Vesting and Staking are merged for calls that fits the gas limit + */ + function processNextVesting() external { + processVestingCreation(); + processStaking(); + } - /** - * @notice Creates vesting contract without staking any tokens - * @dev Separating the Vesting and Staking to tackle Block Gas Limit - */ - function processVestingCreation() public { - require(!vestingCreated, "staking not done for the previous vesting"); - if (vestingDataList.length > 0) { - VestingData storage vestingData = vestingDataList[vestingDataList.length - 1]; - _createAndGetVesting(vestingData); - vestingCreated = true; - } - } + /** + * @notice Creates vesting contract without staking any tokens + * @dev Separating the Vesting and Staking to tackle Block Gas Limit + */ + function processVestingCreation() public { + require(!vestingCreated, "staking not done for the previous vesting"); + if (vestingDataList.length > 0) { + VestingData storage vestingData = vestingDataList[vestingDataList.length - 1]; + _createAndGetVesting(vestingData); + vestingCreated = true; + } + } - /** - * @notice Staking vested tokens - * @dev it can be the case when vesting creation and tokens staking can't be done in one transaction because of block gas limit - */ - function processStaking() public { - require(vestingCreated, "cannot stake without vesting creation"); - if (vestingDataList.length > 0) { - VestingData storage vestingData = vestingDataList[vestingDataList.length - 1]; - address vestingAddress = - _getVesting( - vestingData.tokenOwner, - vestingData.cliff, - vestingData.duration, - vestingData.governanceControl, - vestingData.vestingCreationType - ); - if (vestingAddress != address(0)) { - VestingLogic vesting = VestingLogic(vestingAddress); - require(SOV.approve(address(vesting), vestingData.amount), "Approve failed"); - vesting.stakeTokens(vestingData.amount); - emit TokensStaked(vestingAddress, vestingData.tokenOwner, vestingData.amount); - address tokenOwnerDetails = vestingData.tokenOwner; - vestingDataList.pop(); - emit VestingDataRemoved(msg.sender, tokenOwnerDetails); - } - } - vestingCreated = false; - } + /** + * @notice Staking vested tokens + * @dev it can be the case when vesting creation and tokens staking can't be done in one transaction because of block gas limit + */ + function processStaking() public { + require(vestingCreated, "cannot stake without vesting creation"); + if (vestingDataList.length > 0) { + VestingData storage vestingData = vestingDataList[vestingDataList.length - 1]; + address vestingAddress = + _getVesting( + vestingData.tokenOwner, + vestingData.cliff, + vestingData.duration, + vestingData.governanceControl, + vestingData.vestingCreationType + ); + if (vestingAddress != address(0)) { + VestingLogic vesting = VestingLogic(vestingAddress); + require(SOV.approve(address(vesting), vestingData.amount), "Approve failed"); + vesting.stakeTokens(vestingData.amount); + emit TokensStaked(vestingAddress, vestingData.tokenOwner, vestingData.amount); + address tokenOwnerDetails = vestingData.tokenOwner; + vestingDataList.pop(); + emit VestingDataRemoved(msg.sender, tokenOwnerDetails); + } + } + vestingCreated = false; + } - /** - * @notice removes next vesting data from the list - * @dev we process inverted list - * @dev we should be able to remove incorrect vesting data that can't be processed - */ - function removeNextVesting() external onlyAuthorized { - address tokenOwnerDetails; - if (vestingDataList.length > 0) { - VestingData storage vestingData = vestingDataList[vestingDataList.length - 1]; - tokenOwnerDetails = vestingData.tokenOwner; - vestingDataList.pop(); - emit VestingDataRemoved(msg.sender, tokenOwnerDetails); - } - } + /** + * @notice removes next vesting data from the list + * @dev we process inverted list + * @dev we should be able to remove incorrect vesting data that can't be processed + */ + function removeNextVesting() external onlyAuthorized { + address tokenOwnerDetails; + if (vestingDataList.length > 0) { + VestingData storage vestingData = vestingDataList[vestingDataList.length - 1]; + tokenOwnerDetails = vestingData.tokenOwner; + vestingDataList.pop(); + emit VestingDataRemoved(msg.sender, tokenOwnerDetails); + } + } - /** - * @notice removes all data about unprocessed vestings to be processed - */ - function clearVestingDataList() public onlyAuthorized { - delete vestingDataList; - emit DataCleared(msg.sender); - } + /** + * @notice removes all data about unprocessed vestings to be processed + */ + function clearVestingDataList() public onlyAuthorized { + delete vestingDataList; + emit DataCleared(msg.sender); + } - /** - * @notice returns address after vesting creation - */ - function getVestingAddress() external view returns (address) { - return - _getVesting( - vestingDataList[vestingDataList.length - 1].tokenOwner, - vestingDataList[vestingDataList.length - 1].cliff, - vestingDataList[vestingDataList.length - 1].duration, - vestingDataList[vestingDataList.length - 1].governanceControl, - vestingDataList[vestingDataList.length - 1].vestingCreationType - ); - } + /** + * @notice returns address after vesting creation + */ + function getVestingAddress() external view returns (address) { + return + _getVesting( + vestingDataList[vestingDataList.length - 1].tokenOwner, + vestingDataList[vestingDataList.length - 1].cliff, + vestingDataList[vestingDataList.length - 1].duration, + vestingDataList[vestingDataList.length - 1].governanceControl, + vestingDataList[vestingDataList.length - 1].vestingCreationType + ); + } - /** - * @notice returns period i.e. ((duration - cliff) / 4 WEEKS) - * @dev will be used for deciding if vesting and staking needs to be processed - * in a single transaction or separate transactions - */ - function getVestingPeriod() external view returns (uint256) { - uint256 duration = vestingDataList[vestingDataList.length - 1].duration; - uint256 cliff = vestingDataList[vestingDataList.length - 1].cliff; - uint256 fourWeeks = TWO_WEEKS.mul(2); - uint256 period = duration.sub(cliff).div(fourWeeks); - return period; - } + /** + * @notice returns period i.e. ((duration - cliff) / 4 WEEKS) + * @dev will be used for deciding if vesting and staking needs to be processed + * in a single transaction or separate transactions + */ + function getVestingPeriod() external view returns (uint256) { + uint256 duration = vestingDataList[vestingDataList.length - 1].duration; + uint256 cliff = vestingDataList[vestingDataList.length - 1].cliff; + uint256 fourWeeks = TWO_WEEKS.mul(2); + uint256 period = duration.sub(cliff).div(fourWeeks); + return period; + } - /** - * @notice returns count of vestings to be processed - */ - function getUnprocessedCount() external view returns (uint256) { - return vestingDataList.length; - } + /** + * @notice returns count of vestings to be processed + */ + function getUnprocessedCount() external view returns (uint256) { + return vestingDataList.length; + } - /** - * @notice returns total amount of vestings to be processed - */ - function getUnprocessedAmount() public view returns (uint256) { - uint256 amount = 0; - uint256 length = vestingDataList.length; - for (uint256 i = 0; i < length; i++) { - amount = amount.add(vestingDataList[i].amount); - } - return amount; - } + /** + * @notice returns total amount of vestings to be processed + */ + function getUnprocessedAmount() public view returns (uint256) { + uint256 amount = 0; + uint256 length = vestingDataList.length; + for (uint256 i = 0; i < length; i++) { + amount = amount.add(vestingDataList[i].amount); + } + return amount; + } - /** - * @notice checks if contract balance is enough to process all vestings - */ - function isEnoughBalance() public view returns (bool) { - return SOV.balanceOf(address(this)) >= getUnprocessedAmount(); - } + /** + * @notice checks if contract balance is enough to process all vestings + */ + function isEnoughBalance() public view returns (bool) { + return SOV.balanceOf(address(this)) >= getUnprocessedAmount(); + } - /** - * @notice returns missed balance to process all vestings - */ - function getMissingBalance() external view returns (uint256) { - if (isEnoughBalance()) { - return 0; - } - return getUnprocessedAmount() - SOV.balanceOf(address(this)); - } + /** + * @notice returns missed balance to process all vestings + */ + function getMissingBalance() external view returns (uint256) { + if (isEnoughBalance()) { + return 0; + } + return getUnprocessedAmount() - SOV.balanceOf(address(this)); + } - /** - * @notice creates TeamVesting or Vesting contract - * @dev new contract won't be created if account already has contract of the same type - */ - function _createAndGetVesting(VestingData memory vestingData) internal returns (address vesting) { - if (vestingData.governanceControl) { - vestingRegistryLogic.createTeamVesting( - vestingData.tokenOwner, - vestingData.amount, - vestingData.cliff, - vestingData.duration, - vestingData.vestingCreationType - ); - } else { - vestingRegistryLogic.createVestingAddr( - vestingData.tokenOwner, - vestingData.amount, - vestingData.cliff, - vestingData.duration, - vestingData.vestingCreationType - ); - } - return - _getVesting( - vestingData.tokenOwner, - vestingData.cliff, - vestingData.duration, - vestingData.governanceControl, - vestingData.vestingCreationType - ); - } + /** + * @notice creates TeamVesting or Vesting contract + * @dev new contract won't be created if account already has contract of the same type + */ + function _createAndGetVesting(VestingData memory vestingData) + internal + returns (address vesting) + { + if (vestingData.governanceControl) { + vestingRegistryLogic.createTeamVesting( + vestingData.tokenOwner, + vestingData.amount, + vestingData.cliff, + vestingData.duration, + vestingData.vestingCreationType + ); + } else { + vestingRegistryLogic.createVestingAddr( + vestingData.tokenOwner, + vestingData.amount, + vestingData.cliff, + vestingData.duration, + vestingData.vestingCreationType + ); + } + return + _getVesting( + vestingData.tokenOwner, + vestingData.cliff, + vestingData.duration, + vestingData.governanceControl, + vestingData.vestingCreationType + ); + } - /** - * @notice returns an address of TeamVesting or Vesting contract (depends on a governance control) - */ - function _getVesting( - address _tokenOwner, - uint256 _cliff, - uint256 _duration, - bool _governanceControl, - uint256 _vestingCreationType - ) internal view returns (address vestingAddress) { - if (_governanceControl) { - vestingAddress = vestingRegistryLogic.getTeamVesting(_tokenOwner, _cliff, _duration, _vestingCreationType); - } else { - vestingAddress = vestingRegistryLogic.getVestingAddr(_tokenOwner, _cliff, _duration, _vestingCreationType); - } - } + /** + * @notice returns an address of TeamVesting or Vesting contract (depends on a governance control) + */ + function _getVesting( + address _tokenOwner, + uint256 _cliff, + uint256 _duration, + bool _governanceControl, + uint256 _vestingCreationType + ) internal view returns (address vestingAddress) { + if (_governanceControl) { + vestingAddress = vestingRegistryLogic.getTeamVesting( + _tokenOwner, + _cliff, + _duration, + _vestingCreationType + ); + } else { + vestingAddress = vestingRegistryLogic.getVestingAddr( + _tokenOwner, + _cliff, + _duration, + _vestingCreationType + ); + } + } } diff --git a/contracts/governance/Vesting/VestingFactory.sol b/contracts/governance/Vesting/VestingFactory.sol index 18ba43781..6568421f9 100644 --- a/contracts/governance/Vesting/VestingFactory.sol +++ b/contracts/governance/Vesting/VestingFactory.sol @@ -12,68 +12,90 @@ import "./IVestingFactory.sol"; * of the same contract and keep track of them easier. * */ contract VestingFactory is IVestingFactory, Ownable { - address public vestingLogic; + address public vestingLogic; - constructor(address _vestingLogic) public { - require(_vestingLogic != address(0), "invalid vesting logic address"); - vestingLogic = _vestingLogic; - } + constructor(address _vestingLogic) public { + require(_vestingLogic != address(0), "invalid vesting logic address"); + vestingLogic = _vestingLogic; + } - /** - * @notice Deploys Vesting contract. - * @param _SOV the address of SOV token. - * @param _staking The address of staking contract. - * @param _tokenOwner The owner of the tokens. - * @param _cliff The time interval to the first withdraw in seconds. - * @param _duration The total duration in seconds. - * @param _feeSharing The address of fee sharing contract. - * @param _vestingOwner The address of an owner of vesting contract. - * @return The vesting contract address. - * */ - function deployVesting( - address _SOV, - address _staking, - address _tokenOwner, - uint256 _cliff, - uint256 _duration, - address _feeSharing, - address _vestingOwner - ) - external - onlyOwner /// @dev owner - VestingRegistry - returns (address) - { - address vesting = address(new Vesting(vestingLogic, _SOV, _staking, _tokenOwner, _cliff, _duration, _feeSharing)); - Ownable(vesting).transferOwnership(_vestingOwner); - return vesting; - } + /** + * @notice Deploys Vesting contract. + * @param _SOV the address of SOV token. + * @param _staking The address of staking contract. + * @param _tokenOwner The owner of the tokens. + * @param _cliff The time interval to the first withdraw in seconds. + * @param _duration The total duration in seconds. + * @param _feeSharing The address of fee sharing contract. + * @param _vestingOwner The address of an owner of vesting contract. + * @return The vesting contract address. + * */ + function deployVesting( + address _SOV, + address _staking, + address _tokenOwner, + uint256 _cliff, + uint256 _duration, + address _feeSharing, + address _vestingOwner + ) + external + onlyOwner /// @dev owner - VestingRegistry + returns (address) + { + address vesting = + address( + new Vesting( + vestingLogic, + _SOV, + _staking, + _tokenOwner, + _cliff, + _duration, + _feeSharing + ) + ); + Ownable(vesting).transferOwnership(_vestingOwner); + return vesting; + } - /** - * @notice Deploys Team Vesting contract. - * @param _SOV The address of SOV token. - * @param _staking The address of staking contract. - * @param _tokenOwner The owner of the tokens. - * @param _cliff The time interval to the first withdraw in seconds. - * @param _duration The total duration in seconds. - * @param _feeSharing The address of fee sharing contract. - * @param _vestingOwner The address of an owner of vesting contract. - * @return The vesting contract address. - * */ - function deployTeamVesting( - address _SOV, - address _staking, - address _tokenOwner, - uint256 _cliff, - uint256 _duration, - address _feeSharing, - address _vestingOwner - ) - external - onlyOwner //owner - VestingRegistry - returns (address) - { - address vesting = address(new TeamVesting(vestingLogic, _SOV, _staking, _tokenOwner, _cliff, _duration, _feeSharing)); - Ownable(vesting).transferOwnership(_vestingOwner); - return vesting; - } + /** + * @notice Deploys Team Vesting contract. + * @param _SOV The address of SOV token. + * @param _staking The address of staking contract. + * @param _tokenOwner The owner of the tokens. + * @param _cliff The time interval to the first withdraw in seconds. + * @param _duration The total duration in seconds. + * @param _feeSharing The address of fee sharing contract. + * @param _vestingOwner The address of an owner of vesting contract. + * @return The vesting contract address. + * */ + function deployTeamVesting( + address _SOV, + address _staking, + address _tokenOwner, + uint256 _cliff, + uint256 _duration, + address _feeSharing, + address _vestingOwner + ) + external + onlyOwner //owner - VestingRegistry + returns (address) + { + address vesting = + address( + new TeamVesting( + vestingLogic, + _SOV, + _staking, + _tokenOwner, + _cliff, + _duration, + _feeSharing + ) + ); + Ownable(vesting).transferOwnership(_vestingOwner); + return vesting; + } } diff --git a/contracts/governance/Vesting/VestingLogic.sol b/contracts/governance/Vesting/VestingLogic.sol index e47349a58..28f80443d 100644 --- a/contracts/governance/Vesting/VestingLogic.sol +++ b/contracts/governance/Vesting/VestingLogic.sol @@ -15,206 +15,211 @@ import "./VestingStorage.sol"; * @dev Deployed by a VestingFactory contract. * */ contract VestingLogic is IVesting, VestingStorage, ApprovalReceiver { - /* Events */ - - event TokensStaked(address indexed caller, uint256 amount); - event VotesDelegated(address indexed caller, address delegatee); - event TokensWithdrawn(address indexed caller, address receiver); - event DividendsCollected(address indexed caller, address loanPoolToken, address receiver, uint32 maxCheckpoints); - event MigratedToNewStakingContract(address indexed caller, address newStakingContract); - - /* Modifiers */ - - /** - * @dev Throws if called by any account other than the token owner or the contract owner. - */ - modifier onlyOwners() { - require(msg.sender == tokenOwner || isOwner(), "unauthorized"); - _; - } - - /** - * @dev Throws if called by any account other than the token owner. - */ - modifier onlyTokenOwner() { - require(msg.sender == tokenOwner, "unauthorized"); - _; - } - - /* Functions */ - - /** - * @notice Stakes tokens according to the vesting schedule. - * @param _amount The amount of tokens to stake. - * */ - function stakeTokens(uint256 _amount) public { - _stakeTokens(msg.sender, _amount); - } - - /** - * @notice Stakes tokens according to the vesting schedule. - * @dev This function will be invoked from receiveApproval. - * @dev SOV.approveAndCall -> this.receiveApproval -> this.stakeTokensWithApproval - * @param _sender The sender of SOV.approveAndCall - * @param _amount The amount of tokens to stake. - * */ - function stakeTokensWithApproval(address _sender, uint256 _amount) public onlyThisContract { - _stakeTokens(_sender, _amount); - } - - /** - * @notice Stakes tokens according to the vesting schedule. Low level function. - * @dev Once here the allowance of tokens is taken for granted. - * @param _sender The sender of tokens to stake. - * @param _amount The amount of tokens to stake. - * */ - function _stakeTokens(address _sender, uint256 _amount) internal { - /// @dev Maybe better to allow staking unil the cliff was reached. - if (startDate == 0) { - startDate = staking.timestampToLockDate(block.timestamp); - } - endDate = staking.timestampToLockDate(block.timestamp + duration); - - /// @dev Transfer the tokens to this contract. - bool success = SOV.transferFrom(_sender, address(this), _amount); - require(success); - - /// @dev Allow the staking contract to access them. - SOV.approve(address(staking), _amount); - - staking.stakesBySchedule(_amount, cliff, duration, FOUR_WEEKS, address(this), tokenOwner); - - emit TokensStaked(_sender, _amount); - } - - /** - * @notice Delegate votes from `msg.sender` which are locked until lockDate - * to `delegatee`. - * @param _delegatee The address to delegate votes to. - * */ - function delegate(address _delegatee) public onlyTokenOwner { - require(_delegatee != address(0), "delegatee address invalid"); - - /// @dev Withdraw for each unlocked position. - /// @dev Don't change FOUR_WEEKS to TWO_WEEKS, a lot of vestings already deployed with FOUR_WEEKS - /// workaround found, but it doesn't work with TWO_WEEKS - for (uint256 i = startDate + cliff; i <= endDate; i += FOUR_WEEKS) { - staking.delegate(_delegatee, i); - } - emit VotesDelegated(msg.sender, _delegatee); - } - - /** - * @notice Withdraws all tokens from the staking contract and - * forwards them to an address specified by the token owner. - * @param receiver The receiving address. - * @dev Can be called only by owner. - * */ - function governanceWithdrawTokens(address receiver) public { - require(msg.sender == address(staking), "unauthorized"); - - _withdrawTokens(receiver, true); - } - - /** - * @notice Withdraws unlocked tokens from the staking contract and - * forwards them to an address specified by the token owner. - * @param receiver The receiving address. - * */ - function withdrawTokens(address receiver) public onlyOwners { - _withdrawTokens(receiver, false); - } - - /** - * @notice Withdraws tokens from the staking contract and forwards them - * to an address specified by the token owner. Low level function. - * @dev Once here the caller permission is taken for granted. - * @param receiver The receiving address. - * @param isGovernance Whether all tokens (true) - * or just unlocked tokens (false). - * */ - function _withdrawTokens(address receiver, bool isGovernance) internal { - require(receiver != address(0), "receiver address invalid"); - - uint96 stake; - - /// @dev Usually we just need to iterate over the possible dates until now. - uint256 end; - - /// @dev In the unlikely case that all tokens have been unlocked early, - /// allow to withdraw all of them. - if (staking.allUnlocked() || isGovernance) { - end = endDate; - } else { - end = block.timestamp; - } - - /// @dev Withdraw for each unlocked position. - /// @dev Don't change FOUR_WEEKS to TWO_WEEKS, a lot of vestings already deployed with FOUR_WEEKS - /// workaround found, but it doesn't work with TWO_WEEKS - for (uint256 i = startDate + cliff; i <= end; i += FOUR_WEEKS) { - /// @dev Read amount to withdraw. - stake = staking.getPriorUserStakeByDate(address(this), i, block.number - 1); - - /// @dev Withdraw if > 0 - if (stake > 0) { - if (isGovernance) { - staking.governanceWithdraw(stake, i, receiver); - } else { - staking.withdraw(stake, i, receiver); - } - } - } - - emit TokensWithdrawn(msg.sender, receiver); - } - - /** - * @notice Collect dividends from fee sharing proxy. - * @param _loanPoolToken The loan pool token address. - * @param _maxCheckpoints Maximum number of checkpoints to be processed. - * @param _receiver The receiver of tokens or msg.sender - * */ - function collectDividends( - address _loanPoolToken, - uint32 _maxCheckpoints, - address _receiver - ) public onlyOwners { - require(_receiver != address(0), "receiver address invalid"); - - /// @dev Invokes the fee sharing proxy. - feeSharingProxy.withdraw(_loanPoolToken, _maxCheckpoints, _receiver); - - emit DividendsCollected(msg.sender, _loanPoolToken, _receiver, _maxCheckpoints); - } - - /** - * @notice Allows the owners to migrate the positions - * to a new staking contract. - * */ - function migrateToNewStakingContract() public onlyOwners { - staking.migrateToNewStakingContract(); - staking = Staking(staking.newStakingContract()); - emit MigratedToNewStakingContract(msg.sender, address(staking)); - } - - /** - * @notice Overrides default ApprovalReceiver._getToken function to - * register SOV token on this contract. - * @return The address of SOV token. - * */ - function _getToken() internal view returns (address) { - return address(SOV); - } - - /** - * @notice Overrides default ApprovalReceiver._getSelectors function to - * register stakeTokensWithApproval selector on this contract. - * @return The array of registered selectors on this contract. - * */ - function _getSelectors() internal view returns (bytes4[] memory) { - bytes4[] memory selectors = new bytes4[](1); - selectors[0] = this.stakeTokensWithApproval.selector; - return selectors; - } + /* Events */ + + event TokensStaked(address indexed caller, uint256 amount); + event VotesDelegated(address indexed caller, address delegatee); + event TokensWithdrawn(address indexed caller, address receiver); + event DividendsCollected( + address indexed caller, + address loanPoolToken, + address receiver, + uint32 maxCheckpoints + ); + event MigratedToNewStakingContract(address indexed caller, address newStakingContract); + + /* Modifiers */ + + /** + * @dev Throws if called by any account other than the token owner or the contract owner. + */ + modifier onlyOwners() { + require(msg.sender == tokenOwner || isOwner(), "unauthorized"); + _; + } + + /** + * @dev Throws if called by any account other than the token owner. + */ + modifier onlyTokenOwner() { + require(msg.sender == tokenOwner, "unauthorized"); + _; + } + + /* Functions */ + + /** + * @notice Stakes tokens according to the vesting schedule. + * @param _amount The amount of tokens to stake. + * */ + function stakeTokens(uint256 _amount) public { + _stakeTokens(msg.sender, _amount); + } + + /** + * @notice Stakes tokens according to the vesting schedule. + * @dev This function will be invoked from receiveApproval. + * @dev SOV.approveAndCall -> this.receiveApproval -> this.stakeTokensWithApproval + * @param _sender The sender of SOV.approveAndCall + * @param _amount The amount of tokens to stake. + * */ + function stakeTokensWithApproval(address _sender, uint256 _amount) public onlyThisContract { + _stakeTokens(_sender, _amount); + } + + /** + * @notice Stakes tokens according to the vesting schedule. Low level function. + * @dev Once here the allowance of tokens is taken for granted. + * @param _sender The sender of tokens to stake. + * @param _amount The amount of tokens to stake. + * */ + function _stakeTokens(address _sender, uint256 _amount) internal { + /// @dev Maybe better to allow staking unil the cliff was reached. + if (startDate == 0) { + startDate = staking.timestampToLockDate(block.timestamp); + } + endDate = staking.timestampToLockDate(block.timestamp + duration); + + /// @dev Transfer the tokens to this contract. + bool success = SOV.transferFrom(_sender, address(this), _amount); + require(success); + + /// @dev Allow the staking contract to access them. + SOV.approve(address(staking), _amount); + + staking.stakesBySchedule(_amount, cliff, duration, FOUR_WEEKS, address(this), tokenOwner); + + emit TokensStaked(_sender, _amount); + } + + /** + * @notice Delegate votes from `msg.sender` which are locked until lockDate + * to `delegatee`. + * @param _delegatee The address to delegate votes to. + * */ + function delegate(address _delegatee) public onlyTokenOwner { + require(_delegatee != address(0), "delegatee address invalid"); + + /// @dev Withdraw for each unlocked position. + /// @dev Don't change FOUR_WEEKS to TWO_WEEKS, a lot of vestings already deployed with FOUR_WEEKS + /// workaround found, but it doesn't work with TWO_WEEKS + for (uint256 i = startDate + cliff; i <= endDate; i += FOUR_WEEKS) { + staking.delegate(_delegatee, i); + } + emit VotesDelegated(msg.sender, _delegatee); + } + + /** + * @notice Withdraws all tokens from the staking contract and + * forwards them to an address specified by the token owner. + * @param receiver The receiving address. + * @dev Can be called only by owner. + * */ + function governanceWithdrawTokens(address receiver) public { + require(msg.sender == address(staking), "unauthorized"); + + _withdrawTokens(receiver, true); + } + + /** + * @notice Withdraws unlocked tokens from the staking contract and + * forwards them to an address specified by the token owner. + * @param receiver The receiving address. + * */ + function withdrawTokens(address receiver) public onlyOwners { + _withdrawTokens(receiver, false); + } + + /** + * @notice Withdraws tokens from the staking contract and forwards them + * to an address specified by the token owner. Low level function. + * @dev Once here the caller permission is taken for granted. + * @param receiver The receiving address. + * @param isGovernance Whether all tokens (true) + * or just unlocked tokens (false). + * */ + function _withdrawTokens(address receiver, bool isGovernance) internal { + require(receiver != address(0), "receiver address invalid"); + + uint96 stake; + + /// @dev Usually we just need to iterate over the possible dates until now. + uint256 end; + + /// @dev In the unlikely case that all tokens have been unlocked early, + /// allow to withdraw all of them. + if (staking.allUnlocked() || isGovernance) { + end = endDate; + } else { + end = block.timestamp; + } + + /// @dev Withdraw for each unlocked position. + /// @dev Don't change FOUR_WEEKS to TWO_WEEKS, a lot of vestings already deployed with FOUR_WEEKS + /// workaround found, but it doesn't work with TWO_WEEKS + for (uint256 i = startDate + cliff; i <= end; i += FOUR_WEEKS) { + /// @dev Read amount to withdraw. + stake = staking.getPriorUserStakeByDate(address(this), i, block.number - 1); + + /// @dev Withdraw if > 0 + if (stake > 0) { + if (isGovernance) { + staking.governanceWithdraw(stake, i, receiver); + } else { + staking.withdraw(stake, i, receiver); + } + } + } + + emit TokensWithdrawn(msg.sender, receiver); + } + + /** + * @notice Collect dividends from fee sharing proxy. + * @param _loanPoolToken The loan pool token address. + * @param _maxCheckpoints Maximum number of checkpoints to be processed. + * @param _receiver The receiver of tokens or msg.sender + * */ + function collectDividends( + address _loanPoolToken, + uint32 _maxCheckpoints, + address _receiver + ) public onlyOwners { + require(_receiver != address(0), "receiver address invalid"); + + /// @dev Invokes the fee sharing proxy. + feeSharingProxy.withdraw(_loanPoolToken, _maxCheckpoints, _receiver); + + emit DividendsCollected(msg.sender, _loanPoolToken, _receiver, _maxCheckpoints); + } + + /** + * @notice Allows the owners to migrate the positions + * to a new staking contract. + * */ + function migrateToNewStakingContract() public onlyOwners { + staking.migrateToNewStakingContract(); + staking = Staking(staking.newStakingContract()); + emit MigratedToNewStakingContract(msg.sender, address(staking)); + } + + /** + * @notice Overrides default ApprovalReceiver._getToken function to + * register SOV token on this contract. + * @return The address of SOV token. + * */ + function _getToken() internal view returns (address) { + return address(SOV); + } + + /** + * @notice Overrides default ApprovalReceiver._getSelectors function to + * register stakeTokensWithApproval selector on this contract. + * @return The array of registered selectors on this contract. + * */ + function _getSelectors() internal view returns (bytes4[] memory) { + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = this.stakeTokensWithApproval.selector; + return selectors; + } } diff --git a/contracts/governance/Vesting/VestingRegistry.sol b/contracts/governance/Vesting/VestingRegistry.sol index ef0fc2464..f9af0d1ee 100644 --- a/contracts/governance/Vesting/VestingRegistry.sol +++ b/contracts/governance/Vesting/VestingRegistry.sol @@ -28,449 +28,480 @@ import "../../openzeppelin/SafeMath.sol"; * This contract deals with the vesting and redemption of cSOV tokens. * */ contract VestingRegistry is Ownable { - using SafeMath for uint256; - - /* Storage */ - - /// @notice Constant used for computing the vesting dates. - uint256 public constant FOUR_WEEKS = 4 weeks; - - uint256 public constant CSOV_VESTING_CLIFF = FOUR_WEEKS; - uint256 public constant CSOV_VESTING_DURATION = 10 * FOUR_WEEKS; - - IVestingFactory public vestingFactory; - - /// @notice The SOV token contract. - address public SOV; - - /// @notice The cSOV token contracts. - address[] public CSOVtokens; - - uint256 public priceSats; - - /// @notice The staking contract address. - address public staking; - - /// @notice Fee sharing proxy. - address public feeSharingProxy; - - /// @notice The vesting owner (e.g. governance timelock address). - address public vestingOwner; - - /// @dev TODO: Add to the documentation: address can have only one vesting of each type. - /// @dev user => vesting type => vesting contract. - mapping(address => mapping(uint256 => address)) public vestingContracts; - - /** - * @dev Struct can be created to save storage slots, but it doesn't make - * sense. We don't have a lot of blacklisted accounts or account with - * locked amount. - * */ - - /// @dev user => flag whether user has already exchange cSOV or got a reimbursement. - mapping(address => bool) public processedList; - - /// @dev user => flag whether user shouldn't be able to exchange or reimburse. - mapping(address => bool) public blacklist; - - /// @dev user => amount of tokens should not be processed. - mapping(address => uint256) public lockedAmount; - - /// @dev user => flag whether user has admin role. - mapping(address => bool) public admins; - - enum VestingType { - TeamVesting, // MultisigVesting - Vesting // TokenHolderVesting - } - - /* Events */ - - event CSOVReImburse(address from, uint256 CSOVamount, uint256 reImburseAmount); - event CSOVTokensExchanged(address indexed caller, uint256 amount); - event SOVTransferred(address indexed receiver, uint256 amount); - event VestingCreated(address indexed tokenOwner, address vesting, uint256 cliff, uint256 duration, uint256 amount); - event TeamVestingCreated(address indexed tokenOwner, address vesting, uint256 cliff, uint256 duration, uint256 amount); - event TokensStaked(address indexed vesting, uint256 amount); - event AdminAdded(address admin); - event AdminRemoved(address admin); - - /* Functions */ - - /** - * @notice Contract deployment settings. - * @param _vestingFactory The address of vesting factory contract. - * @param _SOV The SOV token address. - * @param _CSOVtokens The array of cSOV tokens. - * @param _priceSats The price of cSOV tokens in satoshis. - * @param _staking The address of staking contract. - * @param _feeSharingProxy The address of fee sharing proxy contract. - * @param _vestingOwner The address of an owner of vesting contract. - * @dev On Sovryn the vesting owner is Exchequer Multisig. - * According to SIP-0007 The Exchequer Multisig is designated to hold - * certain funds in the form of rBTC and SOV, in order to allow for - * flexible deployment of such funds on: - * + facilitating rBTC redemptions for Genesis pre-sale participants. - * + deploying of SOV for the purposes of exchange listings, market - * making, and partnerships with third parties. - * */ - constructor( - address _vestingFactory, - address _SOV, - address[] memory _CSOVtokens, - uint256 _priceSats, - address _staking, - address _feeSharingProxy, - address _vestingOwner - ) public { - require(_SOV != address(0), "SOV address invalid"); - require(_staking != address(0), "staking address invalid"); - require(_feeSharingProxy != address(0), "feeSharingProxy address invalid"); - require(_vestingOwner != address(0), "vestingOwner address invalid"); - - _setVestingFactory(_vestingFactory); - _setCSOVtokens(_CSOVtokens); - - SOV = _SOV; - priceSats = _priceSats; - staking = _staking; - feeSharingProxy = _feeSharingProxy; - vestingOwner = _vestingOwner; - } - - //---ACL------------------------------------------------------------------ - - /** - * @dev Throws if called by any account other than the owner or admin. - * TODO: This ACL logic should be available on OpenZeppeling Ownable.sol - * or on our own overriding sovrynOwnable. This same logic is repeated - * on OriginInvestorsClaim.sol, TokenSender.sol and VestingRegistry2.sol - */ - modifier onlyAuthorized() { - require(isOwner() || admins[msg.sender], "unauthorized"); - _; - } - - /** - * @notice Add account to ACL. - * @param _admin The addresses of the account to grant permissions. - * */ - function addAdmin(address _admin) public onlyOwner { - admins[_admin] = true; - emit AdminAdded(_admin); - } - - /** - * @notice Remove account from ACL. - * @param _admin The addresses of the account to revoke permissions. - * */ - function removeAdmin(address _admin) public onlyOwner { - admins[_admin] = false; - emit AdminRemoved(_admin); - } - - //---PostCSOV-------------------------------------------------------------- - - modifier isNotProcessed() { - require(!processedList[msg.sender], "Address cannot be processed twice"); - _; - } - - modifier isNotBlacklisted() { - require(!blacklist[msg.sender], "Address blacklisted"); - _; - } - - /** - * @notice cSOV payout to sender with rBTC currency. - * 1.- Check holder cSOV balance by adding up every cSOV token balance. - * 2.- ReImburse rBTC if funds available. - * 3.- And store holder address in processedList. - */ - function reImburse() public isNotProcessed isNotBlacklisted { - uint256 CSOVAmountWei = 0; - for (uint256 i = 0; i < CSOVtokens.length; i++) { - address CSOV = CSOVtokens[i]; - uint256 balance = IERC20(CSOV).balanceOf(msg.sender); - CSOVAmountWei = CSOVAmountWei.add(balance); - } - - require(CSOVAmountWei > lockedAmount[msg.sender], "holder has no CSOV"); - CSOVAmountWei -= lockedAmount[msg.sender]; - processedList[msg.sender] = true; - - /** - * @dev Found and fixed the SIP-0007 bug on VestingRegistry::reImburse formula. - * More details at Documenting Code issues at point 11 in - * https://docs.google.com/document/d/10idTD1K6JvoBmtPKGuJ2Ub_mMh6qTLLlTP693GQKMyU/ - * Previous buggy code: uint256 reImburseAmount = (CSOVAmountWei.mul(priceSats)).div(10**10); - * */ - uint256 reImburseAmount = (CSOVAmountWei.mul(priceSats)).div(10**8); - require(address(this).balance >= reImburseAmount, "Not enough funds to reimburse"); - msg.sender.transfer(reImburseAmount); - - emit CSOVReImburse(msg.sender, CSOVAmountWei, reImburseAmount); - } - - /** - * @notice Get contract balance. - * @return The token balance of the contract. - * */ - function budget() external view returns (uint256) { - uint256 SCBudget = address(this).balance; - return SCBudget; - } - - /** - * @notice Deposit function to receiving value (rBTC). - * */ - function deposit() public payable {} - - /** - * @notice Send all contract balance to an account. - * @param to The account address to send the balance to. - * */ - function withdrawAll(address payable to) public onlyOwner { - to.transfer(address(this).balance); - } - - //-------------------------------------------------------------------------------------------------------------------------------------- - - /** - * @notice Sets vesting factory address. High level endpoint. - * @param _vestingFactory The address of vesting factory contract. - * - * @dev Splitting code on two functions: high level and low level - * is a pattern that makes easy to extend functionality in a readable way, - * without accidentally breaking the actual action being performed. - * For example, checks should be done on high level endpoint, while core - * functionality should be coded on the low level function. - * */ - function setVestingFactory(address _vestingFactory) public onlyOwner { - _setVestingFactory(_vestingFactory); - } - - /** - * @notice Sets vesting factory address. Low level core function. - * @param _vestingFactory The address of vesting factory contract. - * */ - function _setVestingFactory(address _vestingFactory) internal { - require(_vestingFactory != address(0), "vestingFactory address invalid"); - vestingFactory = IVestingFactory(_vestingFactory); - } - - /** - * @notice Sets cSOV tokens array. High level endpoint. - * @param _CSOVtokens The array of cSOV tokens. - * */ - function setCSOVtokens(address[] memory _CSOVtokens) public onlyOwner { - _setCSOVtokens(_CSOVtokens); - } - - /** - * @notice Sets cSOV tokens array by looping through input. Low level function. - * @param _CSOVtokens The array of cSOV tokens. - * */ - function _setCSOVtokens(address[] memory _CSOVtokens) internal { - for (uint256 i = 0; i < _CSOVtokens.length; i++) { - require(_CSOVtokens[i] != address(0), "CSOV address invalid"); - } - CSOVtokens = _CSOVtokens; - } - - /** - * @notice Set blacklist flag (true/false). - * @param _account The address to be blacklisted. - * @param _blacklisted The flag to add/remove to/from a blacklist. - * */ - function setBlacklistFlag(address _account, bool _blacklisted) public onlyOwner { - require(_account != address(0), "account address invalid"); - - blacklist[_account] = _blacklisted; - } - - /** - * @notice Set amount to be subtracted from user token balance. - * @param _account The address with locked amount. - * @param _amount The amount to be locked. - * */ - function setLockedAmount(address _account, uint256 _amount) public onlyOwner { - require(_account != address(0), "account address invalid"); - require(_amount != 0, "amount invalid"); - - lockedAmount[_account] = _amount; - } - - /** - * @notice Transfer SOV tokens to given address. - * - * @dev This is a wrapper for ERC-20 transfer function w/ - * additional checks and triggering an event. - * - * @param _receiver The address of the SOV receiver. - * @param _amount The amount to be transferred. - * */ - function transferSOV(address _receiver, uint256 _amount) public onlyOwner { - require(_receiver != address(0), "receiver address invalid"); - require(_amount != 0, "amount invalid"); - - IERC20(SOV).transfer(_receiver, _amount); - emit SOVTransferred(_receiver, _amount); - } - - /** - * @notice Exchange cSOV to SOV with 1:1 rate - */ - function exchangeAllCSOV() public isNotProcessed isNotBlacklisted { - processedList[msg.sender] = true; - - uint256 amount = 0; - for (uint256 i = 0; i < CSOVtokens.length; i++) { - address CSOV = CSOVtokens[i]; - uint256 balance = IERC20(CSOV).balanceOf(msg.sender); - amount += balance; - } - - require(amount > lockedAmount[msg.sender], "amount invalid"); - amount -= lockedAmount[msg.sender]; - - _createVestingForCSOV(amount); - } - - /** - * @notice cSOV tokens are moved and staked on Vesting contract. - * @param _amount The amount of tokens to be vested. - * */ - function _createVestingForCSOV(uint256 _amount) internal { - address vesting = _getOrCreateVesting(msg.sender, CSOV_VESTING_CLIFF, CSOV_VESTING_DURATION); - - IERC20(SOV).approve(vesting, _amount); - IVesting(vesting).stakeTokens(_amount); - - emit CSOVTokensExchanged(msg.sender, _amount); - } - - /** - * @notice Check a token address is among the cSOV token addresses. - * @param _CSOV The cSOV token address. - * */ - function _validateCSOV(address _CSOV) internal view { - bool isValid = false; - for (uint256 i = 0; i < CSOVtokens.length; i++) { - if (_CSOV == CSOVtokens[i]) { - isValid = true; - break; - } - } - require(isValid, "wrong CSOV address"); - } - - /** - * @notice Create Vesting contract. - * @param _tokenOwner The owner of the tokens. - * @param _amount The amount to be staked. - * @param _cliff The time interval to the first withdraw in seconds. - * @param _duration The total duration in seconds. - * */ - function createVesting( - address _tokenOwner, - uint256 _amount, - uint256 _cliff, - uint256 _duration - ) public onlyAuthorized { - address vesting = _getOrCreateVesting(_tokenOwner, _cliff, _duration); - emit VestingCreated(_tokenOwner, vesting, _cliff, _duration, _amount); - } - - /** - * @notice Create Team Vesting contract. - * @param _tokenOwner The owner of the tokens. - * @param _amount The amount to be staked. - * @param _cliff The time interval to the first withdraw in seconds. - * @param _duration The total duration in seconds. - * */ - function createTeamVesting( - address _tokenOwner, - uint256 _amount, - uint256 _cliff, - uint256 _duration - ) public onlyAuthorized { - address vesting = _getOrCreateTeamVesting(_tokenOwner, _cliff, _duration); - emit TeamVestingCreated(_tokenOwner, vesting, _cliff, _duration, _amount); - } - - /** - * @notice Stake tokens according to the vesting schedule. - * @param _vesting The address of Vesting contract. - * @param _amount The amount of tokens to stake. - * */ - function stakeTokens(address _vesting, uint256 _amount) public onlyAuthorized { - require(_vesting != address(0), "vesting address invalid"); - require(_amount > 0, "amount invalid"); - - IERC20(SOV).approve(_vesting, _amount); - IVesting(_vesting).stakeTokens(_amount); - emit TokensStaked(_vesting, _amount); - } - - /** - * @notice Query the vesting contract for an account. - * @param _tokenOwner The owner of the tokens. - * @return The vesting contract address for the given token owner. - * */ - function getVesting(address _tokenOwner) public view returns (address) { - return vestingContracts[_tokenOwner][uint256(VestingType.Vesting)]; - } - - /** - * @notice Query the team vesting contract for an account. - * @param _tokenOwner The owner of the tokens. - * @return The team vesting contract address for the given token owner. - * */ - function getTeamVesting(address _tokenOwner) public view returns (address) { - return vestingContracts[_tokenOwner][uint256(VestingType.TeamVesting)]; - } - - /** - * @notice If not exists, deploy a vesting contract through factory. - * @param _tokenOwner The owner of the tokens. - * @param _cliff The time interval to the first withdraw in seconds. - * @param _duration The total duration in seconds. - * @return The vesting contract address for the given token owner - * whether it existed previously or not. - * */ - function _getOrCreateVesting( - address _tokenOwner, - uint256 _cliff, - uint256 _duration - ) internal returns (address) { - uint256 type_ = uint256(VestingType.Vesting); - if (vestingContracts[_tokenOwner][type_] == address(0)) { - /// @dev TODO: Owner of OwnerVesting contracts - the same address as tokenOwner. - address vesting = vestingFactory.deployVesting(SOV, staking, _tokenOwner, _cliff, _duration, feeSharingProxy, _tokenOwner); - vestingContracts[_tokenOwner][type_] = vesting; - } - return vestingContracts[_tokenOwner][type_]; - } - - /** - * @notice If not exists, deploy a team vesting contract through factory. - * @param _tokenOwner The owner of the tokens. - * @param _cliff The time interval to the first withdraw in seconds. - * @param _duration The total duration in seconds. - * @return The team vesting contract address for the given token owner - * whether it existed previously or not. - * */ - function _getOrCreateTeamVesting( - address _tokenOwner, - uint256 _cliff, - uint256 _duration - ) internal returns (address) { - uint256 type_ = uint256(VestingType.TeamVesting); - if (vestingContracts[_tokenOwner][type_] == address(0)) { - address vesting = vestingFactory.deployTeamVesting(SOV, staking, _tokenOwner, _cliff, _duration, feeSharingProxy, vestingOwner); - vestingContracts[_tokenOwner][type_] = vesting; - } - return vestingContracts[_tokenOwner][type_]; - } + using SafeMath for uint256; + + /* Storage */ + + /// @notice Constant used for computing the vesting dates. + uint256 public constant FOUR_WEEKS = 4 weeks; + + uint256 public constant CSOV_VESTING_CLIFF = FOUR_WEEKS; + uint256 public constant CSOV_VESTING_DURATION = 10 * FOUR_WEEKS; + + IVestingFactory public vestingFactory; + + /// @notice The SOV token contract. + address public SOV; + + /// @notice The cSOV token contracts. + address[] public CSOVtokens; + + uint256 public priceSats; + + /// @notice The staking contract address. + address public staking; + + /// @notice Fee sharing proxy. + address public feeSharingProxy; + + /// @notice The vesting owner (e.g. governance timelock address). + address public vestingOwner; + + /// @dev TODO: Add to the documentation: address can have only one vesting of each type. + /// @dev user => vesting type => vesting contract. + mapping(address => mapping(uint256 => address)) public vestingContracts; + + /** + * @dev Struct can be created to save storage slots, but it doesn't make + * sense. We don't have a lot of blacklisted accounts or account with + * locked amount. + * */ + + /// @dev user => flag whether user has already exchange cSOV or got a reimbursement. + mapping(address => bool) public processedList; + + /// @dev user => flag whether user shouldn't be able to exchange or reimburse. + mapping(address => bool) public blacklist; + + /// @dev user => amount of tokens should not be processed. + mapping(address => uint256) public lockedAmount; + + /// @dev user => flag whether user has admin role. + mapping(address => bool) public admins; + + enum VestingType { + TeamVesting, // MultisigVesting + Vesting // TokenHolderVesting + } + + /* Events */ + + event CSOVReImburse(address from, uint256 CSOVamount, uint256 reImburseAmount); + event CSOVTokensExchanged(address indexed caller, uint256 amount); + event SOVTransferred(address indexed receiver, uint256 amount); + event VestingCreated( + address indexed tokenOwner, + address vesting, + uint256 cliff, + uint256 duration, + uint256 amount + ); + event TeamVestingCreated( + address indexed tokenOwner, + address vesting, + uint256 cliff, + uint256 duration, + uint256 amount + ); + event TokensStaked(address indexed vesting, uint256 amount); + event AdminAdded(address admin); + event AdminRemoved(address admin); + + /* Functions */ + + /** + * @notice Contract deployment settings. + * @param _vestingFactory The address of vesting factory contract. + * @param _SOV The SOV token address. + * @param _CSOVtokens The array of cSOV tokens. + * @param _priceSats The price of cSOV tokens in satoshis. + * @param _staking The address of staking contract. + * @param _feeSharingProxy The address of fee sharing proxy contract. + * @param _vestingOwner The address of an owner of vesting contract. + * @dev On Sovryn the vesting owner is Exchequer Multisig. + * According to SIP-0007 The Exchequer Multisig is designated to hold + * certain funds in the form of rBTC and SOV, in order to allow for + * flexible deployment of such funds on: + * + facilitating rBTC redemptions for Genesis pre-sale participants. + * + deploying of SOV for the purposes of exchange listings, market + * making, and partnerships with third parties. + * */ + constructor( + address _vestingFactory, + address _SOV, + address[] memory _CSOVtokens, + uint256 _priceSats, + address _staking, + address _feeSharingProxy, + address _vestingOwner + ) public { + require(_SOV != address(0), "SOV address invalid"); + require(_staking != address(0), "staking address invalid"); + require(_feeSharingProxy != address(0), "feeSharingProxy address invalid"); + require(_vestingOwner != address(0), "vestingOwner address invalid"); + + _setVestingFactory(_vestingFactory); + _setCSOVtokens(_CSOVtokens); + + SOV = _SOV; + priceSats = _priceSats; + staking = _staking; + feeSharingProxy = _feeSharingProxy; + vestingOwner = _vestingOwner; + } + + //---ACL------------------------------------------------------------------ + + /** + * @dev Throws if called by any account other than the owner or admin. + * TODO: This ACL logic should be available on OpenZeppeling Ownable.sol + * or on our own overriding sovrynOwnable. This same logic is repeated + * on OriginInvestorsClaim.sol, TokenSender.sol and VestingRegistry2.sol + */ + modifier onlyAuthorized() { + require(isOwner() || admins[msg.sender], "unauthorized"); + _; + } + + /** + * @notice Add account to ACL. + * @param _admin The addresses of the account to grant permissions. + * */ + function addAdmin(address _admin) public onlyOwner { + admins[_admin] = true; + emit AdminAdded(_admin); + } + + /** + * @notice Remove account from ACL. + * @param _admin The addresses of the account to revoke permissions. + * */ + function removeAdmin(address _admin) public onlyOwner { + admins[_admin] = false; + emit AdminRemoved(_admin); + } + + //---PostCSOV-------------------------------------------------------------- + + modifier isNotProcessed() { + require(!processedList[msg.sender], "Address cannot be processed twice"); + _; + } + + modifier isNotBlacklisted() { + require(!blacklist[msg.sender], "Address blacklisted"); + _; + } + + /** + * @notice cSOV payout to sender with rBTC currency. + * 1.- Check holder cSOV balance by adding up every cSOV token balance. + * 2.- ReImburse rBTC if funds available. + * 3.- And store holder address in processedList. + */ + function reImburse() public isNotProcessed isNotBlacklisted { + uint256 CSOVAmountWei = 0; + for (uint256 i = 0; i < CSOVtokens.length; i++) { + address CSOV = CSOVtokens[i]; + uint256 balance = IERC20(CSOV).balanceOf(msg.sender); + CSOVAmountWei = CSOVAmountWei.add(balance); + } + + require(CSOVAmountWei > lockedAmount[msg.sender], "holder has no CSOV"); + CSOVAmountWei -= lockedAmount[msg.sender]; + processedList[msg.sender] = true; + + /** + * @dev Found and fixed the SIP-0007 bug on VestingRegistry::reImburse formula. + * More details at Documenting Code issues at point 11 in + * https://docs.google.com/document/d/10idTD1K6JvoBmtPKGuJ2Ub_mMh6qTLLlTP693GQKMyU/ + * Previous buggy code: uint256 reImburseAmount = (CSOVAmountWei.mul(priceSats)).div(10**10); + * */ + uint256 reImburseAmount = (CSOVAmountWei.mul(priceSats)).div(10**8); + require(address(this).balance >= reImburseAmount, "Not enough funds to reimburse"); + msg.sender.transfer(reImburseAmount); + + emit CSOVReImburse(msg.sender, CSOVAmountWei, reImburseAmount); + } + + /** + * @notice Get contract balance. + * @return The token balance of the contract. + * */ + function budget() external view returns (uint256) { + uint256 SCBudget = address(this).balance; + return SCBudget; + } + + /** + * @notice Deposit function to receiving value (rBTC). + * */ + function deposit() public payable {} + + /** + * @notice Send all contract balance to an account. + * @param to The account address to send the balance to. + * */ + function withdrawAll(address payable to) public onlyOwner { + to.transfer(address(this).balance); + } + + //-------------------------------------------------------------------------------------------------------------------------------------- + + /** + * @notice Sets vesting factory address. High level endpoint. + * @param _vestingFactory The address of vesting factory contract. + * + * @dev Splitting code on two functions: high level and low level + * is a pattern that makes easy to extend functionality in a readable way, + * without accidentally breaking the actual action being performed. + * For example, checks should be done on high level endpoint, while core + * functionality should be coded on the low level function. + * */ + function setVestingFactory(address _vestingFactory) public onlyOwner { + _setVestingFactory(_vestingFactory); + } + + /** + * @notice Sets vesting factory address. Low level core function. + * @param _vestingFactory The address of vesting factory contract. + * */ + function _setVestingFactory(address _vestingFactory) internal { + require(_vestingFactory != address(0), "vestingFactory address invalid"); + vestingFactory = IVestingFactory(_vestingFactory); + } + + /** + * @notice Sets cSOV tokens array. High level endpoint. + * @param _CSOVtokens The array of cSOV tokens. + * */ + function setCSOVtokens(address[] memory _CSOVtokens) public onlyOwner { + _setCSOVtokens(_CSOVtokens); + } + + /** + * @notice Sets cSOV tokens array by looping through input. Low level function. + * @param _CSOVtokens The array of cSOV tokens. + * */ + function _setCSOVtokens(address[] memory _CSOVtokens) internal { + for (uint256 i = 0; i < _CSOVtokens.length; i++) { + require(_CSOVtokens[i] != address(0), "CSOV address invalid"); + } + CSOVtokens = _CSOVtokens; + } + + /** + * @notice Set blacklist flag (true/false). + * @param _account The address to be blacklisted. + * @param _blacklisted The flag to add/remove to/from a blacklist. + * */ + function setBlacklistFlag(address _account, bool _blacklisted) public onlyOwner { + require(_account != address(0), "account address invalid"); + + blacklist[_account] = _blacklisted; + } + + /** + * @notice Set amount to be subtracted from user token balance. + * @param _account The address with locked amount. + * @param _amount The amount to be locked. + * */ + function setLockedAmount(address _account, uint256 _amount) public onlyOwner { + require(_account != address(0), "account address invalid"); + require(_amount != 0, "amount invalid"); + + lockedAmount[_account] = _amount; + } + + /** + * @notice Transfer SOV tokens to given address. + * + * @dev This is a wrapper for ERC-20 transfer function w/ + * additional checks and triggering an event. + * + * @param _receiver The address of the SOV receiver. + * @param _amount The amount to be transferred. + * */ + function transferSOV(address _receiver, uint256 _amount) public onlyOwner { + require(_receiver != address(0), "receiver address invalid"); + require(_amount != 0, "amount invalid"); + + IERC20(SOV).transfer(_receiver, _amount); + emit SOVTransferred(_receiver, _amount); + } + + /** + * @notice Exchange cSOV to SOV with 1:1 rate + */ + function exchangeAllCSOV() public isNotProcessed isNotBlacklisted { + processedList[msg.sender] = true; + + uint256 amount = 0; + for (uint256 i = 0; i < CSOVtokens.length; i++) { + address CSOV = CSOVtokens[i]; + uint256 balance = IERC20(CSOV).balanceOf(msg.sender); + amount += balance; + } + + require(amount > lockedAmount[msg.sender], "amount invalid"); + amount -= lockedAmount[msg.sender]; + + _createVestingForCSOV(amount); + } + + /** + * @notice cSOV tokens are moved and staked on Vesting contract. + * @param _amount The amount of tokens to be vested. + * */ + function _createVestingForCSOV(uint256 _amount) internal { + address vesting = + _getOrCreateVesting(msg.sender, CSOV_VESTING_CLIFF, CSOV_VESTING_DURATION); + + IERC20(SOV).approve(vesting, _amount); + IVesting(vesting).stakeTokens(_amount); + + emit CSOVTokensExchanged(msg.sender, _amount); + } + + /** + * @notice Check a token address is among the cSOV token addresses. + * @param _CSOV The cSOV token address. + * */ + function _validateCSOV(address _CSOV) internal view { + bool isValid = false; + for (uint256 i = 0; i < CSOVtokens.length; i++) { + if (_CSOV == CSOVtokens[i]) { + isValid = true; + break; + } + } + require(isValid, "wrong CSOV address"); + } + + /** + * @notice Create Vesting contract. + * @param _tokenOwner The owner of the tokens. + * @param _amount The amount to be staked. + * @param _cliff The time interval to the first withdraw in seconds. + * @param _duration The total duration in seconds. + * */ + function createVesting( + address _tokenOwner, + uint256 _amount, + uint256 _cliff, + uint256 _duration + ) public onlyAuthorized { + address vesting = _getOrCreateVesting(_tokenOwner, _cliff, _duration); + emit VestingCreated(_tokenOwner, vesting, _cliff, _duration, _amount); + } + + /** + * @notice Create Team Vesting contract. + * @param _tokenOwner The owner of the tokens. + * @param _amount The amount to be staked. + * @param _cliff The time interval to the first withdraw in seconds. + * @param _duration The total duration in seconds. + * */ + function createTeamVesting( + address _tokenOwner, + uint256 _amount, + uint256 _cliff, + uint256 _duration + ) public onlyAuthorized { + address vesting = _getOrCreateTeamVesting(_tokenOwner, _cliff, _duration); + emit TeamVestingCreated(_tokenOwner, vesting, _cliff, _duration, _amount); + } + + /** + * @notice Stake tokens according to the vesting schedule. + * @param _vesting The address of Vesting contract. + * @param _amount The amount of tokens to stake. + * */ + function stakeTokens(address _vesting, uint256 _amount) public onlyAuthorized { + require(_vesting != address(0), "vesting address invalid"); + require(_amount > 0, "amount invalid"); + + IERC20(SOV).approve(_vesting, _amount); + IVesting(_vesting).stakeTokens(_amount); + emit TokensStaked(_vesting, _amount); + } + + /** + * @notice Query the vesting contract for an account. + * @param _tokenOwner The owner of the tokens. + * @return The vesting contract address for the given token owner. + * */ + function getVesting(address _tokenOwner) public view returns (address) { + return vestingContracts[_tokenOwner][uint256(VestingType.Vesting)]; + } + + /** + * @notice Query the team vesting contract for an account. + * @param _tokenOwner The owner of the tokens. + * @return The team vesting contract address for the given token owner. + * */ + function getTeamVesting(address _tokenOwner) public view returns (address) { + return vestingContracts[_tokenOwner][uint256(VestingType.TeamVesting)]; + } + + /** + * @notice If not exists, deploy a vesting contract through factory. + * @param _tokenOwner The owner of the tokens. + * @param _cliff The time interval to the first withdraw in seconds. + * @param _duration The total duration in seconds. + * @return The vesting contract address for the given token owner + * whether it existed previously or not. + * */ + function _getOrCreateVesting( + address _tokenOwner, + uint256 _cliff, + uint256 _duration + ) internal returns (address) { + uint256 type_ = uint256(VestingType.Vesting); + if (vestingContracts[_tokenOwner][type_] == address(0)) { + /// @dev TODO: Owner of OwnerVesting contracts - the same address as tokenOwner. + address vesting = + vestingFactory.deployVesting( + SOV, + staking, + _tokenOwner, + _cliff, + _duration, + feeSharingProxy, + _tokenOwner + ); + vestingContracts[_tokenOwner][type_] = vesting; + } + return vestingContracts[_tokenOwner][type_]; + } + + /** + * @notice If not exists, deploy a team vesting contract through factory. + * @param _tokenOwner The owner of the tokens. + * @param _cliff The time interval to the first withdraw in seconds. + * @param _duration The total duration in seconds. + * @return The team vesting contract address for the given token owner + * whether it existed previously or not. + * */ + function _getOrCreateTeamVesting( + address _tokenOwner, + uint256 _cliff, + uint256 _duration + ) internal returns (address) { + uint256 type_ = uint256(VestingType.TeamVesting); + if (vestingContracts[_tokenOwner][type_] == address(0)) { + address vesting = + vestingFactory.deployTeamVesting( + SOV, + staking, + _tokenOwner, + _cliff, + _duration, + feeSharingProxy, + vestingOwner + ); + vestingContracts[_tokenOwner][type_] = vesting; + } + return vestingContracts[_tokenOwner][type_]; + } } diff --git a/contracts/governance/Vesting/VestingRegistry2.sol b/contracts/governance/Vesting/VestingRegistry2.sol index 2172346b2..48157e220 100644 --- a/contracts/governance/Vesting/VestingRegistry2.sol +++ b/contracts/governance/Vesting/VestingRegistry2.sol @@ -14,393 +14,424 @@ import "../../openzeppelin/SafeMath.sol"; * @notice One time contract needed to distribute tokens to origin sales investors. * */ contract VestingRegistry2 is Ownable { - using SafeMath for uint256; - - /* Storage */ - - /// @notice Constant used for computing the vesting dates. - uint256 public constant FOUR_WEEKS = 4 weeks; - - uint256 public constant CSOV_VESTING_CLIFF = FOUR_WEEKS; - uint256 public constant CSOV_VESTING_DURATION = 10 * FOUR_WEEKS; - - IVestingFactory public vestingFactory; - - /// @notice The SOV token contract. - address public SOV; - - /// @notice The CSOV token contracts. - address[] public CSOVtokens; - - uint256 public priceSats; - - /// @notice The staking contract address. - address public staking; - - /// @notice Fee sharing proxy. - address public feeSharingProxy; - - /// @notice The vesting owner (e.g. governance timelock address). - address public vestingOwner; - - /// @dev TODO: Add to the documentation: address can have only one vesting of each type. - /// @dev user => vesting type => vesting contract - mapping(address => mapping(uint256 => address)) public vestingContracts; - - /** - * @dev Struct can be created to save storage slots, but it doesn't make - * sense. We don't have a lot of blacklisted accounts or account with - * locked amount. - * */ - - /// @dev user => flag whether user has already exchange cSOV or got a reimbursement. - mapping(address => bool) public processedList; - - /// @dev user => flag whether user shouldn't be able to exchange or reimburse. - mapping(address => bool) public blacklist; - - /// @dev user => amount of tokens should not be processed. - mapping(address => uint256) public lockedAmount; - - /// @dev user => flag whether user has admin role. - mapping(address => bool) public admins; - - enum VestingType { - TeamVesting, // MultisigVesting - Vesting // TokenHolderVesting - } - - /* Events */ - - event CSOVTokensExchanged(address indexed caller, uint256 amount); - event SOVTransferred(address indexed receiver, uint256 amount); - event VestingCreated(address indexed tokenOwner, address vesting, uint256 cliff, uint256 duration, uint256 amount); - event TeamVestingCreated(address indexed tokenOwner, address vesting, uint256 cliff, uint256 duration, uint256 amount); - event TokensStaked(address indexed vesting, uint256 amount); - event AdminAdded(address admin); - event AdminRemoved(address admin); - - /* Functions */ - - /** - * @notice Contract deployment settings. - * @param _vestingFactory The address of vesting factory contract. - * @param _SOV The SOV token address. - * @param _CSOVtokens The array of cSOV tokens. - * @param _priceSats The price of cSOV tokens in satoshis. - * @param _staking The address of staking contract. - * @param _feeSharingProxy The address of fee sharing proxy contract. - * @param _vestingOwner The address of an owner of vesting contract. - * @dev On Sovryn the vesting owner is Exchequer Multisig. - * According to SIP-0007 The Exchequer Multisig is designated to hold - * certain funds in the form of rBTC and SOV, in order to allow for - * flexible deployment of such funds on: - * + facilitating rBTC redemptions for Genesis pre-sale participants. - * + deploying of SOV for the purposes of exchange listings, market - * making, and partnerships with third parties. - * */ - constructor( - address _vestingFactory, - address _SOV, - address[] memory _CSOVtokens, - uint256 _priceSats, - address _staking, - address _feeSharingProxy, - address _vestingOwner - ) public { - require(_SOV != address(0), "SOV address invalid"); - require(_staking != address(0), "staking address invalid"); - require(_feeSharingProxy != address(0), "feeSharingProxy address invalid"); - require(_vestingOwner != address(0), "vestingOwner address invalid"); - - _setVestingFactory(_vestingFactory); - _setCSOVtokens(_CSOVtokens); - - SOV = _SOV; - priceSats = _priceSats; - staking = _staking; - feeSharingProxy = _feeSharingProxy; - vestingOwner = _vestingOwner; - } - - /** - * @dev Throws if called by any account other than the owner or admin. - */ - modifier onlyAuthorized() { - require(isOwner() || admins[msg.sender], "unauthorized"); - _; - } - - /** - * @notice Add account to ACL. - * @param _admin The addresses of the account to grant permissions. - * */ - function addAdmin(address _admin) public onlyOwner { - admins[_admin] = true; - emit AdminAdded(_admin); - } - - /** - * @notice Remove account from ACL. - * @param _admin The addresses of the account to revoke permissions. - * */ - function removeAdmin(address _admin) public onlyOwner { - admins[_admin] = false; - emit AdminRemoved(_admin); - } - - //---PostCSOV-------------------------------------------------------------- - - modifier isNotProcessed() { - require(!processedList[msg.sender], "Address cannot be processed twice"); - _; - } - - modifier isNotBlacklisted() { - require(!blacklist[msg.sender], "Address blacklisted"); - _; - } - - /** - * @notice Get contract balance. - * @return The token balance of the contract. - * */ - function budget() external view returns (uint256) { - uint256 SCBudget = address(this).balance; - return SCBudget; - } - - /** - * @notice Deposit function to receiving value (rBTC). - * */ - function deposit() public payable {} - - /** - * @notice Send all contract balance to an account. - * @param to The account address to send the balance to. - * */ - function withdrawAll(address payable to) public onlyOwner { - to.transfer(address(this).balance); - } - - //-------------------------------------------------------------------------------------------------------------------------------------- - - /** - * @notice Sets vesting factory address. High level endpoint. - * @param _vestingFactory The address of vesting factory contract. - * - * @dev Splitting code on two functions: high level and low level - * is a pattern that makes easy to extend functionality in a readable way, - * without accidentally breaking the actual action being performed. - * For example, checks should be done on high level endpoint, while core - * functionality should be coded on the low level function. - * */ - function setVestingFactory(address _vestingFactory) public onlyOwner { - _setVestingFactory(_vestingFactory); - } - - /** - * @notice Sets vesting factory address. Low level core function. - * @param _vestingFactory The address of vesting factory contract. - * */ - function _setVestingFactory(address _vestingFactory) internal { - require(_vestingFactory != address(0), "vestingFactory address invalid"); - vestingFactory = IVestingFactory(_vestingFactory); - } - - /** - * @notice Sets cSOV tokens array. High level endpoint. - * @param _CSOVtokens The array of cSOV tokens. - * */ - function setCSOVtokens(address[] memory _CSOVtokens) public onlyOwner { - _setCSOVtokens(_CSOVtokens); - } - - /** - * @notice Sets cSOV tokens array by looping through input. Low level function. - * @param _CSOVtokens The array of cSOV tokens. - * */ - function _setCSOVtokens(address[] memory _CSOVtokens) internal { - for (uint256 i = 0; i < _CSOVtokens.length; i++) { - require(_CSOVtokens[i] != address(0), "CSOV address invalid"); - } - CSOVtokens = _CSOVtokens; - } - - /** - * @notice Set blacklist flag (true/false). - * @param _account The address to be blacklisted. - * @param _blacklisted The flag to add/remove to/from a blacklist. - * */ - function setBlacklistFlag(address _account, bool _blacklisted) public onlyOwner { - require(_account != address(0), "account address invalid"); - - blacklist[_account] = _blacklisted; - } - - /** - * @notice Set amount to be subtracted from user token balance. - * @param _account The address with locked amount. - * @param _amount The amount to be locked. - * */ - function setLockedAmount(address _account, uint256 _amount) public onlyOwner { - require(_account != address(0), "account address invalid"); - require(_amount != 0, "amount invalid"); - - lockedAmount[_account] = _amount; - } - - /** - * @notice Transfer SOV tokens to given address. - * - * @dev This is a wrapper for ERC-20 transfer function w/ - * additional checks and triggering an event. - * - * @param _receiver The address of the SOV receiver. - * @param _amount The amount to be transferred. - * */ - function transferSOV(address _receiver, uint256 _amount) public onlyOwner { - require(_receiver != address(0), "receiver address invalid"); - require(_amount != 0, "amount invalid"); - - IERC20(SOV).transfer(_receiver, _amount); - emit SOVTransferred(_receiver, _amount); - } - - /** - * @notice cSOV tokens are moved and staked on Vesting contract. - * @param _amount The amount of tokens to be vested. - * */ - function _createVestingForCSOV(uint256 _amount) internal { - address vesting = _getOrCreateVesting(msg.sender, CSOV_VESTING_CLIFF, CSOV_VESTING_DURATION); - - IERC20(SOV).approve(vesting, _amount); - IVesting(vesting).stakeTokens(_amount); - - emit CSOVTokensExchanged(msg.sender, _amount); - } - - /** - * @notice Check a token address is among the cSOV token addresses. - * @param _CSOV The cSOV token address. - * */ - function _validateCSOV(address _CSOV) internal view { - bool isValid = false; - for (uint256 i = 0; i < CSOVtokens.length; i++) { - if (_CSOV == CSOVtokens[i]) { - isValid = true; - break; - } - } - require(isValid, "wrong CSOV address"); - } - - /** - * @notice Create Vesting contract. - * @param _tokenOwner The owner of the tokens. - * @param _amount The amount to be staked. - * @param _cliff The time interval to the first withdraw in seconds. - * @param _duration The total duration in seconds. - * */ - function createVesting( - address _tokenOwner, - uint256 _amount, - uint256 _cliff, - uint256 _duration - ) public onlyAuthorized { - address vesting = _getOrCreateVesting(_tokenOwner, _cliff, _duration); - emit VestingCreated(_tokenOwner, vesting, _cliff, _duration, _amount); - } - - /** - * @notice Create Team Vesting contract. - * @param _tokenOwner The owner of the tokens. - * @param _amount The amount to be staked. - * @param _cliff The time interval to the first withdraw in seconds. - * @param _duration The total duration in seconds. - * */ - function createTeamVesting( - address _tokenOwner, - uint256 _amount, - uint256 _cliff, - uint256 _duration - ) public onlyAuthorized { - address vesting = _getOrCreateTeamVesting(_tokenOwner, _cliff, _duration); - emit TeamVestingCreated(_tokenOwner, vesting, _cliff, _duration, _amount); - } - - /** - * @notice Stake tokens according to the vesting schedule - * @param _vesting the address of Vesting contract - * @param _amount the amount of tokens to stake - * */ - function stakeTokens(address _vesting, uint256 _amount) public onlyAuthorized { - require(_vesting != address(0), "vesting address invalid"); - require(_amount > 0, "amount invalid"); - - IERC20(SOV).approve(_vesting, _amount); - IVesting(_vesting).stakeTokens(_amount); - emit TokensStaked(_vesting, _amount); - } - - /** - * @notice Query the vesting contract for an account. - * @param _tokenOwner The owner of the tokens. - * @return The vesting contract address for the given token owner. - * */ - function getVesting(address _tokenOwner) public view returns (address) { - return vestingContracts[_tokenOwner][uint256(VestingType.Vesting)]; - } - - /** - * @notice Query the team vesting contract for an account. - * @param _tokenOwner The owner of the tokens. - * @return The team vesting contract address for the given token owner. - * */ - function getTeamVesting(address _tokenOwner) public view returns (address) { - return vestingContracts[_tokenOwner][uint256(VestingType.TeamVesting)]; - } - - /** - * @notice If not exists, deploy a vesting contract through factory. - * @param _tokenOwner The owner of the tokens. - * @param _cliff The time interval to the first withdraw in seconds. - * @param _duration The total duration in seconds. - * @return The vesting contract address for the given token owner - * whether it existed previously or not. - * */ - function _getOrCreateVesting( - address _tokenOwner, - uint256 _cliff, - uint256 _duration - ) internal returns (address) { - uint256 type_ = uint256(VestingType.Vesting); - if (vestingContracts[_tokenOwner][type_] == address(0)) { - //TODO Owner of OwnerVesting contracts - the same address as tokenOwner - address vesting = vestingFactory.deployVesting(SOV, staking, _tokenOwner, _cliff, _duration, feeSharingProxy, _tokenOwner); - vestingContracts[_tokenOwner][type_] = vesting; - } - return vestingContracts[_tokenOwner][type_]; - } - - /** - * @notice If not exists, deploy a team vesting contract through factory. - * @param _tokenOwner The owner of the tokens. - * @param _cliff The time interval to the first withdraw in seconds. - * @param _duration The total duration in seconds. - * @return The team vesting contract address for the given token owner - * whether it existed previously or not. - * */ - function _getOrCreateTeamVesting( - address _tokenOwner, - uint256 _cliff, - uint256 _duration - ) internal returns (address) { - uint256 type_ = uint256(VestingType.TeamVesting); - if (vestingContracts[_tokenOwner][type_] == address(0)) { - address vesting = vestingFactory.deployTeamVesting(SOV, staking, _tokenOwner, _cliff, _duration, feeSharingProxy, vestingOwner); - vestingContracts[_tokenOwner][type_] = vesting; - } - return vestingContracts[_tokenOwner][type_]; - } + using SafeMath for uint256; + + /* Storage */ + + /// @notice Constant used for computing the vesting dates. + uint256 public constant FOUR_WEEKS = 4 weeks; + + uint256 public constant CSOV_VESTING_CLIFF = FOUR_WEEKS; + uint256 public constant CSOV_VESTING_DURATION = 10 * FOUR_WEEKS; + + IVestingFactory public vestingFactory; + + /// @notice The SOV token contract. + address public SOV; + + /// @notice The CSOV token contracts. + address[] public CSOVtokens; + + uint256 public priceSats; + + /// @notice The staking contract address. + address public staking; + + /// @notice Fee sharing proxy. + address public feeSharingProxy; + + /// @notice The vesting owner (e.g. governance timelock address). + address public vestingOwner; + + /// @dev TODO: Add to the documentation: address can have only one vesting of each type. + /// @dev user => vesting type => vesting contract + mapping(address => mapping(uint256 => address)) public vestingContracts; + + /** + * @dev Struct can be created to save storage slots, but it doesn't make + * sense. We don't have a lot of blacklisted accounts or account with + * locked amount. + * */ + + /// @dev user => flag whether user has already exchange cSOV or got a reimbursement. + mapping(address => bool) public processedList; + + /// @dev user => flag whether user shouldn't be able to exchange or reimburse. + mapping(address => bool) public blacklist; + + /// @dev user => amount of tokens should not be processed. + mapping(address => uint256) public lockedAmount; + + /// @dev user => flag whether user has admin role. + mapping(address => bool) public admins; + + enum VestingType { + TeamVesting, // MultisigVesting + Vesting // TokenHolderVesting + } + + /* Events */ + + event CSOVTokensExchanged(address indexed caller, uint256 amount); + event SOVTransferred(address indexed receiver, uint256 amount); + event VestingCreated( + address indexed tokenOwner, + address vesting, + uint256 cliff, + uint256 duration, + uint256 amount + ); + event TeamVestingCreated( + address indexed tokenOwner, + address vesting, + uint256 cliff, + uint256 duration, + uint256 amount + ); + event TokensStaked(address indexed vesting, uint256 amount); + event AdminAdded(address admin); + event AdminRemoved(address admin); + + /* Functions */ + + /** + * @notice Contract deployment settings. + * @param _vestingFactory The address of vesting factory contract. + * @param _SOV The SOV token address. + * @param _CSOVtokens The array of cSOV tokens. + * @param _priceSats The price of cSOV tokens in satoshis. + * @param _staking The address of staking contract. + * @param _feeSharingProxy The address of fee sharing proxy contract. + * @param _vestingOwner The address of an owner of vesting contract. + * @dev On Sovryn the vesting owner is Exchequer Multisig. + * According to SIP-0007 The Exchequer Multisig is designated to hold + * certain funds in the form of rBTC and SOV, in order to allow for + * flexible deployment of such funds on: + * + facilitating rBTC redemptions for Genesis pre-sale participants. + * + deploying of SOV for the purposes of exchange listings, market + * making, and partnerships with third parties. + * */ + constructor( + address _vestingFactory, + address _SOV, + address[] memory _CSOVtokens, + uint256 _priceSats, + address _staking, + address _feeSharingProxy, + address _vestingOwner + ) public { + require(_SOV != address(0), "SOV address invalid"); + require(_staking != address(0), "staking address invalid"); + require(_feeSharingProxy != address(0), "feeSharingProxy address invalid"); + require(_vestingOwner != address(0), "vestingOwner address invalid"); + + _setVestingFactory(_vestingFactory); + _setCSOVtokens(_CSOVtokens); + + SOV = _SOV; + priceSats = _priceSats; + staking = _staking; + feeSharingProxy = _feeSharingProxy; + vestingOwner = _vestingOwner; + } + + /** + * @dev Throws if called by any account other than the owner or admin. + */ + modifier onlyAuthorized() { + require(isOwner() || admins[msg.sender], "unauthorized"); + _; + } + + /** + * @notice Add account to ACL. + * @param _admin The addresses of the account to grant permissions. + * */ + function addAdmin(address _admin) public onlyOwner { + admins[_admin] = true; + emit AdminAdded(_admin); + } + + /** + * @notice Remove account from ACL. + * @param _admin The addresses of the account to revoke permissions. + * */ + function removeAdmin(address _admin) public onlyOwner { + admins[_admin] = false; + emit AdminRemoved(_admin); + } + + //---PostCSOV-------------------------------------------------------------- + + modifier isNotProcessed() { + require(!processedList[msg.sender], "Address cannot be processed twice"); + _; + } + + modifier isNotBlacklisted() { + require(!blacklist[msg.sender], "Address blacklisted"); + _; + } + + /** + * @notice Get contract balance. + * @return The token balance of the contract. + * */ + function budget() external view returns (uint256) { + uint256 SCBudget = address(this).balance; + return SCBudget; + } + + /** + * @notice Deposit function to receiving value (rBTC). + * */ + function deposit() public payable {} + + /** + * @notice Send all contract balance to an account. + * @param to The account address to send the balance to. + * */ + function withdrawAll(address payable to) public onlyOwner { + to.transfer(address(this).balance); + } + + //-------------------------------------------------------------------------------------------------------------------------------------- + + /** + * @notice Sets vesting factory address. High level endpoint. + * @param _vestingFactory The address of vesting factory contract. + * + * @dev Splitting code on two functions: high level and low level + * is a pattern that makes easy to extend functionality in a readable way, + * without accidentally breaking the actual action being performed. + * For example, checks should be done on high level endpoint, while core + * functionality should be coded on the low level function. + * */ + function setVestingFactory(address _vestingFactory) public onlyOwner { + _setVestingFactory(_vestingFactory); + } + + /** + * @notice Sets vesting factory address. Low level core function. + * @param _vestingFactory The address of vesting factory contract. + * */ + function _setVestingFactory(address _vestingFactory) internal { + require(_vestingFactory != address(0), "vestingFactory address invalid"); + vestingFactory = IVestingFactory(_vestingFactory); + } + + /** + * @notice Sets cSOV tokens array. High level endpoint. + * @param _CSOVtokens The array of cSOV tokens. + * */ + function setCSOVtokens(address[] memory _CSOVtokens) public onlyOwner { + _setCSOVtokens(_CSOVtokens); + } + + /** + * @notice Sets cSOV tokens array by looping through input. Low level function. + * @param _CSOVtokens The array of cSOV tokens. + * */ + function _setCSOVtokens(address[] memory _CSOVtokens) internal { + for (uint256 i = 0; i < _CSOVtokens.length; i++) { + require(_CSOVtokens[i] != address(0), "CSOV address invalid"); + } + CSOVtokens = _CSOVtokens; + } + + /** + * @notice Set blacklist flag (true/false). + * @param _account The address to be blacklisted. + * @param _blacklisted The flag to add/remove to/from a blacklist. + * */ + function setBlacklistFlag(address _account, bool _blacklisted) public onlyOwner { + require(_account != address(0), "account address invalid"); + + blacklist[_account] = _blacklisted; + } + + /** + * @notice Set amount to be subtracted from user token balance. + * @param _account The address with locked amount. + * @param _amount The amount to be locked. + * */ + function setLockedAmount(address _account, uint256 _amount) public onlyOwner { + require(_account != address(0), "account address invalid"); + require(_amount != 0, "amount invalid"); + + lockedAmount[_account] = _amount; + } + + /** + * @notice Transfer SOV tokens to given address. + * + * @dev This is a wrapper for ERC-20 transfer function w/ + * additional checks and triggering an event. + * + * @param _receiver The address of the SOV receiver. + * @param _amount The amount to be transferred. + * */ + function transferSOV(address _receiver, uint256 _amount) public onlyOwner { + require(_receiver != address(0), "receiver address invalid"); + require(_amount != 0, "amount invalid"); + + IERC20(SOV).transfer(_receiver, _amount); + emit SOVTransferred(_receiver, _amount); + } + + /** + * @notice cSOV tokens are moved and staked on Vesting contract. + * @param _amount The amount of tokens to be vested. + * */ + function _createVestingForCSOV(uint256 _amount) internal { + address vesting = + _getOrCreateVesting(msg.sender, CSOV_VESTING_CLIFF, CSOV_VESTING_DURATION); + + IERC20(SOV).approve(vesting, _amount); + IVesting(vesting).stakeTokens(_amount); + + emit CSOVTokensExchanged(msg.sender, _amount); + } + + /** + * @notice Check a token address is among the cSOV token addresses. + * @param _CSOV The cSOV token address. + * */ + function _validateCSOV(address _CSOV) internal view { + bool isValid = false; + for (uint256 i = 0; i < CSOVtokens.length; i++) { + if (_CSOV == CSOVtokens[i]) { + isValid = true; + break; + } + } + require(isValid, "wrong CSOV address"); + } + + /** + * @notice Create Vesting contract. + * @param _tokenOwner The owner of the tokens. + * @param _amount The amount to be staked. + * @param _cliff The time interval to the first withdraw in seconds. + * @param _duration The total duration in seconds. + * */ + function createVesting( + address _tokenOwner, + uint256 _amount, + uint256 _cliff, + uint256 _duration + ) public onlyAuthorized { + address vesting = _getOrCreateVesting(_tokenOwner, _cliff, _duration); + emit VestingCreated(_tokenOwner, vesting, _cliff, _duration, _amount); + } + + /** + * @notice Create Team Vesting contract. + * @param _tokenOwner The owner of the tokens. + * @param _amount The amount to be staked. + * @param _cliff The time interval to the first withdraw in seconds. + * @param _duration The total duration in seconds. + * */ + function createTeamVesting( + address _tokenOwner, + uint256 _amount, + uint256 _cliff, + uint256 _duration + ) public onlyAuthorized { + address vesting = _getOrCreateTeamVesting(_tokenOwner, _cliff, _duration); + emit TeamVestingCreated(_tokenOwner, vesting, _cliff, _duration, _amount); + } + + /** + * @notice Stake tokens according to the vesting schedule + * @param _vesting the address of Vesting contract + * @param _amount the amount of tokens to stake + * */ + function stakeTokens(address _vesting, uint256 _amount) public onlyAuthorized { + require(_vesting != address(0), "vesting address invalid"); + require(_amount > 0, "amount invalid"); + + IERC20(SOV).approve(_vesting, _amount); + IVesting(_vesting).stakeTokens(_amount); + emit TokensStaked(_vesting, _amount); + } + + /** + * @notice Query the vesting contract for an account. + * @param _tokenOwner The owner of the tokens. + * @return The vesting contract address for the given token owner. + * */ + function getVesting(address _tokenOwner) public view returns (address) { + return vestingContracts[_tokenOwner][uint256(VestingType.Vesting)]; + } + + /** + * @notice Query the team vesting contract for an account. + * @param _tokenOwner The owner of the tokens. + * @return The team vesting contract address for the given token owner. + * */ + function getTeamVesting(address _tokenOwner) public view returns (address) { + return vestingContracts[_tokenOwner][uint256(VestingType.TeamVesting)]; + } + + /** + * @notice If not exists, deploy a vesting contract through factory. + * @param _tokenOwner The owner of the tokens. + * @param _cliff The time interval to the first withdraw in seconds. + * @param _duration The total duration in seconds. + * @return The vesting contract address for the given token owner + * whether it existed previously or not. + * */ + function _getOrCreateVesting( + address _tokenOwner, + uint256 _cliff, + uint256 _duration + ) internal returns (address) { + uint256 type_ = uint256(VestingType.Vesting); + if (vestingContracts[_tokenOwner][type_] == address(0)) { + //TODO Owner of OwnerVesting contracts - the same address as tokenOwner + address vesting = + vestingFactory.deployVesting( + SOV, + staking, + _tokenOwner, + _cliff, + _duration, + feeSharingProxy, + _tokenOwner + ); + vestingContracts[_tokenOwner][type_] = vesting; + } + return vestingContracts[_tokenOwner][type_]; + } + + /** + * @notice If not exists, deploy a team vesting contract through factory. + * @param _tokenOwner The owner of the tokens. + * @param _cliff The time interval to the first withdraw in seconds. + * @param _duration The total duration in seconds. + * @return The team vesting contract address for the given token owner + * whether it existed previously or not. + * */ + function _getOrCreateTeamVesting( + address _tokenOwner, + uint256 _cliff, + uint256 _duration + ) internal returns (address) { + uint256 type_ = uint256(VestingType.TeamVesting); + if (vestingContracts[_tokenOwner][type_] == address(0)) { + address vesting = + vestingFactory.deployTeamVesting( + SOV, + staking, + _tokenOwner, + _cliff, + _duration, + feeSharingProxy, + vestingOwner + ); + vestingContracts[_tokenOwner][type_] = vesting; + } + return vestingContracts[_tokenOwner][type_]; + } } diff --git a/contracts/governance/Vesting/VestingRegistry3.sol b/contracts/governance/Vesting/VestingRegistry3.sol index 32501a735..bd650bdbf 100644 --- a/contracts/governance/Vesting/VestingRegistry3.sol +++ b/contracts/governance/Vesting/VestingRegistry3.sol @@ -10,191 +10,221 @@ import "./ITeamVesting.sol"; import "../../openzeppelin/SafeMath.sol"; contract VestingRegistry3 is Ownable { - using SafeMath for uint256; - - IVestingFactory public vestingFactory; - - ///@notice the SOV token contract - address public SOV; - - ///@notice the staking contract address - address public staking; - //@notice fee sharing proxy - address public feeSharingProxy; - //@notice the vesting owner (e.g. governance timelock address) - address public vestingOwner; - - //TODO add to the documentation: address can have only one vesting of each type - //user => vesting type => vesting contract - mapping(address => mapping(uint256 => address)) public vestingContracts; - - //user => flag whether user has admin role - mapping(address => bool) public admins; - - enum VestingType { - TeamVesting, //MultisigVesting - Vesting //TokenHolderVesting - } - - event SOVTransferred(address indexed receiver, uint256 amount); - event VestingCreated(address indexed tokenOwner, address vesting, uint256 cliff, uint256 duration, uint256 amount); - event TeamVestingCreated(address indexed tokenOwner, address vesting, uint256 cliff, uint256 duration, uint256 amount); - event TokensStaked(address indexed vesting, uint256 amount); - event AdminAdded(address admin); - event AdminRemoved(address admin); - - constructor( - address _vestingFactory, - address _SOV, - address _staking, - address _feeSharingProxy, - address _vestingOwner - ) public { - require(_SOV != address(0), "SOV address invalid"); - require(_staking != address(0), "staking address invalid"); - require(_feeSharingProxy != address(0), "feeSharingProxy address invalid"); - require(_vestingOwner != address(0), "vestingOwner address invalid"); - - _setVestingFactory(_vestingFactory); - - SOV = _SOV; - staking = _staking; - feeSharingProxy = _feeSharingProxy; - vestingOwner = _vestingOwner; - } - - /** - * @dev Throws if called by any account other than the owner or admin. - */ - modifier onlyAuthorized() { - require(isOwner() || admins[msg.sender], "unauthorized"); - _; - } - - function addAdmin(address _admin) public onlyOwner { - admins[_admin] = true; - emit AdminAdded(_admin); - } - - function removeAdmin(address _admin) public onlyOwner { - admins[_admin] = false; - emit AdminRemoved(_admin); - } - - /** - * @notice sets vesting factory address - * @param _vestingFactory the address of vesting factory contract - */ - function setVestingFactory(address _vestingFactory) public onlyOwner { - _setVestingFactory(_vestingFactory); - } - - function _setVestingFactory(address _vestingFactory) internal { - require(_vestingFactory != address(0), "vestingFactory address invalid"); - vestingFactory = IVestingFactory(_vestingFactory); - } - - /** - * @notice transfers SOV tokens to given address - * @param _receiver the address of the SOV receiver - * @param _amount the amount to be transferred - */ - function transferSOV(address _receiver, uint256 _amount) public onlyOwner { - require(_receiver != address(0), "receiver address invalid"); - require(_amount != 0, "amount invalid"); - - IERC20(SOV).transfer(_receiver, _amount); - emit SOVTransferred(_receiver, _amount); - } - - /** - * @notice creates Vesting contract - * @param _tokenOwner the owner of the tokens - * @param _amount the amount to be staked - * @param _cliff the cliff in seconds - * @param _duration the total duration in seconds - */ - function createVesting( - address _tokenOwner, - uint256 _amount, - uint256 _cliff, - uint256 _duration - ) public onlyAuthorized { - address vesting = _getOrCreateVesting(_tokenOwner, _cliff, _duration); - emit VestingCreated(_tokenOwner, vesting, _cliff, _duration, _amount); - } - - /** - * @notice creates Team Vesting contract - * @param _tokenOwner the owner of the tokens - * @param _amount the amount to be staked - * @param _cliff the cliff in seconds - * @param _duration the total duration in seconds - */ - function createTeamVesting( - address _tokenOwner, - uint256 _amount, - uint256 _cliff, - uint256 _duration - ) public onlyAuthorized { - address vesting = _getOrCreateTeamVesting(_tokenOwner, _cliff, _duration); - emit TeamVestingCreated(_tokenOwner, vesting, _cliff, _duration, _amount); - } - - /** - * @notice stakes tokens according to the vesting schedule - * @param _vesting the address of Vesting contract - * @param _amount the amount of tokens to stake - */ - function stakeTokens(address _vesting, uint256 _amount) public onlyAuthorized { - require(_vesting != address(0), "vesting address invalid"); - require(_amount > 0, "amount invalid"); - - IERC20(SOV).approve(_vesting, _amount); - IVesting(_vesting).stakeTokens(_amount); - emit TokensStaked(_vesting, _amount); - } - - /** - * @notice returns vesting contract address for the given token owner - * @param _tokenOwner the owner of the tokens - */ - function getVesting(address _tokenOwner) public view returns (address) { - return vestingContracts[_tokenOwner][uint256(VestingType.Vesting)]; - } - - /** - * @notice returns team vesting contract address for the given token owner - * @param _tokenOwner the owner of the tokens - */ - function getTeamVesting(address _tokenOwner) public view returns (address) { - return vestingContracts[_tokenOwner][uint256(VestingType.TeamVesting)]; - } - - function _getOrCreateVesting( - address _tokenOwner, - uint256 _cliff, - uint256 _duration - ) internal returns (address) { - uint256 type_ = uint256(VestingType.Vesting); - if (vestingContracts[_tokenOwner][type_] == address(0)) { - //TODO Owner of OwnerVesting contracts - the same address as tokenOwner - address vesting = vestingFactory.deployVesting(SOV, staking, _tokenOwner, _cliff, _duration, feeSharingProxy, _tokenOwner); - vestingContracts[_tokenOwner][type_] = vesting; - } - return vestingContracts[_tokenOwner][type_]; - } - - function _getOrCreateTeamVesting( - address _tokenOwner, - uint256 _cliff, - uint256 _duration - ) internal returns (address) { - uint256 type_ = uint256(VestingType.TeamVesting); - if (vestingContracts[_tokenOwner][type_] == address(0)) { - address vesting = vestingFactory.deployTeamVesting(SOV, staking, _tokenOwner, _cliff, _duration, feeSharingProxy, vestingOwner); - vestingContracts[_tokenOwner][type_] = vesting; - } - return vestingContracts[_tokenOwner][type_]; - } + using SafeMath for uint256; + + IVestingFactory public vestingFactory; + + ///@notice the SOV token contract + address public SOV; + + ///@notice the staking contract address + address public staking; + //@notice fee sharing proxy + address public feeSharingProxy; + //@notice the vesting owner (e.g. governance timelock address) + address public vestingOwner; + + //TODO add to the documentation: address can have only one vesting of each type + //user => vesting type => vesting contract + mapping(address => mapping(uint256 => address)) public vestingContracts; + + //user => flag whether user has admin role + mapping(address => bool) public admins; + + enum VestingType { + TeamVesting, //MultisigVesting + Vesting //TokenHolderVesting + } + + event SOVTransferred(address indexed receiver, uint256 amount); + event VestingCreated( + address indexed tokenOwner, + address vesting, + uint256 cliff, + uint256 duration, + uint256 amount + ); + event TeamVestingCreated( + address indexed tokenOwner, + address vesting, + uint256 cliff, + uint256 duration, + uint256 amount + ); + event TokensStaked(address indexed vesting, uint256 amount); + event AdminAdded(address admin); + event AdminRemoved(address admin); + + constructor( + address _vestingFactory, + address _SOV, + address _staking, + address _feeSharingProxy, + address _vestingOwner + ) public { + require(_SOV != address(0), "SOV address invalid"); + require(_staking != address(0), "staking address invalid"); + require(_feeSharingProxy != address(0), "feeSharingProxy address invalid"); + require(_vestingOwner != address(0), "vestingOwner address invalid"); + + _setVestingFactory(_vestingFactory); + + SOV = _SOV; + staking = _staking; + feeSharingProxy = _feeSharingProxy; + vestingOwner = _vestingOwner; + } + + /** + * @dev Throws if called by any account other than the owner or admin. + */ + modifier onlyAuthorized() { + require(isOwner() || admins[msg.sender], "unauthorized"); + _; + } + + function addAdmin(address _admin) public onlyOwner { + admins[_admin] = true; + emit AdminAdded(_admin); + } + + function removeAdmin(address _admin) public onlyOwner { + admins[_admin] = false; + emit AdminRemoved(_admin); + } + + /** + * @notice sets vesting factory address + * @param _vestingFactory the address of vesting factory contract + */ + function setVestingFactory(address _vestingFactory) public onlyOwner { + _setVestingFactory(_vestingFactory); + } + + function _setVestingFactory(address _vestingFactory) internal { + require(_vestingFactory != address(0), "vestingFactory address invalid"); + vestingFactory = IVestingFactory(_vestingFactory); + } + + /** + * @notice transfers SOV tokens to given address + * @param _receiver the address of the SOV receiver + * @param _amount the amount to be transferred + */ + function transferSOV(address _receiver, uint256 _amount) public onlyOwner { + require(_receiver != address(0), "receiver address invalid"); + require(_amount != 0, "amount invalid"); + + IERC20(SOV).transfer(_receiver, _amount); + emit SOVTransferred(_receiver, _amount); + } + + /** + * @notice creates Vesting contract + * @param _tokenOwner the owner of the tokens + * @param _amount the amount to be staked + * @param _cliff the cliff in seconds + * @param _duration the total duration in seconds + */ + function createVesting( + address _tokenOwner, + uint256 _amount, + uint256 _cliff, + uint256 _duration + ) public onlyAuthorized { + address vesting = _getOrCreateVesting(_tokenOwner, _cliff, _duration); + emit VestingCreated(_tokenOwner, vesting, _cliff, _duration, _amount); + } + + /** + * @notice creates Team Vesting contract + * @param _tokenOwner the owner of the tokens + * @param _amount the amount to be staked + * @param _cliff the cliff in seconds + * @param _duration the total duration in seconds + */ + function createTeamVesting( + address _tokenOwner, + uint256 _amount, + uint256 _cliff, + uint256 _duration + ) public onlyAuthorized { + address vesting = _getOrCreateTeamVesting(_tokenOwner, _cliff, _duration); + emit TeamVestingCreated(_tokenOwner, vesting, _cliff, _duration, _amount); + } + + /** + * @notice stakes tokens according to the vesting schedule + * @param _vesting the address of Vesting contract + * @param _amount the amount of tokens to stake + */ + function stakeTokens(address _vesting, uint256 _amount) public onlyAuthorized { + require(_vesting != address(0), "vesting address invalid"); + require(_amount > 0, "amount invalid"); + + IERC20(SOV).approve(_vesting, _amount); + IVesting(_vesting).stakeTokens(_amount); + emit TokensStaked(_vesting, _amount); + } + + /** + * @notice returns vesting contract address for the given token owner + * @param _tokenOwner the owner of the tokens + */ + function getVesting(address _tokenOwner) public view returns (address) { + return vestingContracts[_tokenOwner][uint256(VestingType.Vesting)]; + } + + /** + * @notice returns team vesting contract address for the given token owner + * @param _tokenOwner the owner of the tokens + */ + function getTeamVesting(address _tokenOwner) public view returns (address) { + return vestingContracts[_tokenOwner][uint256(VestingType.TeamVesting)]; + } + + function _getOrCreateVesting( + address _tokenOwner, + uint256 _cliff, + uint256 _duration + ) internal returns (address) { + uint256 type_ = uint256(VestingType.Vesting); + if (vestingContracts[_tokenOwner][type_] == address(0)) { + //TODO Owner of OwnerVesting contracts - the same address as tokenOwner + address vesting = + vestingFactory.deployVesting( + SOV, + staking, + _tokenOwner, + _cliff, + _duration, + feeSharingProxy, + _tokenOwner + ); + vestingContracts[_tokenOwner][type_] = vesting; + } + return vestingContracts[_tokenOwner][type_]; + } + + function _getOrCreateTeamVesting( + address _tokenOwner, + uint256 _cliff, + uint256 _duration + ) internal returns (address) { + uint256 type_ = uint256(VestingType.TeamVesting); + if (vestingContracts[_tokenOwner][type_] == address(0)) { + address vesting = + vestingFactory.deployTeamVesting( + SOV, + staking, + _tokenOwner, + _cliff, + _duration, + feeSharingProxy, + vestingOwner + ); + vestingContracts[_tokenOwner][type_] = vesting; + } + return vestingContracts[_tokenOwner][type_]; + } } diff --git a/contracts/governance/Vesting/VestingRegistryLogic.sol b/contracts/governance/Vesting/VestingRegistryLogic.sol index 722f7f6a1..dca87e39c 100644 --- a/contracts/governance/Vesting/VestingRegistryLogic.sol +++ b/contracts/governance/Vesting/VestingRegistryLogic.sol @@ -8,323 +8,420 @@ import "./ITeamVesting.sol"; import "./VestingRegistryStorage.sol"; contract VestingRegistryLogic is VestingRegistryStorage { - event SOVTransferred(address indexed receiver, uint256 amount); - event VestingCreated( - address indexed tokenOwner, - address vesting, - uint256 cliff, - uint256 duration, - uint256 amount, - uint256 vestingCreationType - ); - event TeamVestingCreated( - address indexed tokenOwner, - address vesting, - uint256 cliff, - uint256 duration, - uint256 amount, - uint256 vestingCreationType - ); - event TokensStaked(address indexed vesting, uint256 amount); + event SOVTransferred(address indexed receiver, uint256 amount); + event VestingCreated( + address indexed tokenOwner, + address vesting, + uint256 cliff, + uint256 duration, + uint256 amount, + uint256 vestingCreationType + ); + event TeamVestingCreated( + address indexed tokenOwner, + address vesting, + uint256 cliff, + uint256 duration, + uint256 amount, + uint256 vestingCreationType + ); + event TokensStaked(address indexed vesting, uint256 amount); - /** - * @notice Replace constructor with initialize function for Upgradable Contracts - * This function will be called only once by the owner - * */ - function initialize( - address _vestingFactory, - address _SOV, - address _staking, - address _feeSharingProxy, - address _vestingOwner, - address _lockedSOV, - address[] calldata _vestingRegistries - ) external onlyOwner initializer { - require(_SOV != address(0), "SOV address invalid"); - require(_staking != address(0), "staking address invalid"); - require(_feeSharingProxy != address(0), "feeSharingProxy address invalid"); - require(_vestingOwner != address(0), "vestingOwner address invalid"); - require(_lockedSOV != address(0), "LockedSOV address invalid"); + /** + * @notice Replace constructor with initialize function for Upgradable Contracts + * This function will be called only once by the owner + * */ + function initialize( + address _vestingFactory, + address _SOV, + address _staking, + address _feeSharingProxy, + address _vestingOwner, + address _lockedSOV, + address[] calldata _vestingRegistries + ) external onlyOwner initializer { + require(_SOV != address(0), "SOV address invalid"); + require(_staking != address(0), "staking address invalid"); + require(_feeSharingProxy != address(0), "feeSharingProxy address invalid"); + require(_vestingOwner != address(0), "vestingOwner address invalid"); + require(_lockedSOV != address(0), "LockedSOV address invalid"); - _setVestingFactory(_vestingFactory); - SOV = _SOV; - staking = _staking; - feeSharingProxy = _feeSharingProxy; - vestingOwner = _vestingOwner; - lockedSOV = LockedSOV(_lockedSOV); - for (uint256 i = 0; i < _vestingRegistries.length; i++) { - require(_vestingRegistries[i] != address(0), "Vesting registry address invalid"); - vestingRegistries.push(IVestingRegistry(_vestingRegistries[i])); - } - } + _setVestingFactory(_vestingFactory); + SOV = _SOV; + staking = _staking; + feeSharingProxy = _feeSharingProxy; + vestingOwner = _vestingOwner; + lockedSOV = LockedSOV(_lockedSOV); + for (uint256 i = 0; i < _vestingRegistries.length; i++) { + require(_vestingRegistries[i] != address(0), "Vesting registry address invalid"); + vestingRegistries.push(IVestingRegistry(_vestingRegistries[i])); + } + } - /** - * @notice sets vesting factory address - * @param _vestingFactory the address of vesting factory contract - */ - function setVestingFactory(address _vestingFactory) external onlyOwner { - _setVestingFactory(_vestingFactory); - } + /** + * @notice sets vesting factory address + * @param _vestingFactory the address of vesting factory contract + */ + function setVestingFactory(address _vestingFactory) external onlyOwner { + _setVestingFactory(_vestingFactory); + } - /** - * @notice Internal function that sets vesting factory address - * @param _vestingFactory the address of vesting factory contract - */ - function _setVestingFactory(address _vestingFactory) internal { - require(_vestingFactory != address(0), "vestingFactory address invalid"); - vestingFactory = IVestingFactory(_vestingFactory); - } + /** + * @notice Internal function that sets vesting factory address + * @param _vestingFactory the address of vesting factory contract + */ + function _setVestingFactory(address _vestingFactory) internal { + require(_vestingFactory != address(0), "vestingFactory address invalid"); + vestingFactory = IVestingFactory(_vestingFactory); + } - /** - * @notice transfers SOV tokens to given address - * @param _receiver the address of the SOV receiver - * @param _amount the amount to be transferred - */ - function transferSOV(address _receiver, uint256 _amount) external onlyOwner { - require(_receiver != address(0), "receiver address invalid"); - require(_amount != 0, "amount invalid"); - require(IERC20(SOV).transfer(_receiver, _amount), "transfer failed"); - emit SOVTransferred(_receiver, _amount); - } + /** + * @notice transfers SOV tokens to given address + * @param _receiver the address of the SOV receiver + * @param _amount the amount to be transferred + */ + function transferSOV(address _receiver, uint256 _amount) external onlyOwner { + require(_receiver != address(0), "receiver address invalid"); + require(_amount != 0, "amount invalid"); + require(IERC20(SOV).transfer(_receiver, _amount), "transfer failed"); + emit SOVTransferred(_receiver, _amount); + } - /** - * @notice adds vestings that were deployed in previous vesting registries - * @dev migration of data from previous vesting registy contracts - */ - function addDeployedVestings(address[] calldata _tokenOwners, uint256[] calldata _vestingCreationTypes) external onlyAuthorized { - for (uint256 i = 0; i < _tokenOwners.length; i++) { - require(_tokenOwners[i] != address(0), "token owner cannot be 0 address"); - require(_vestingCreationTypes[i] > 0, "vesting creation type must be greater than 0"); - _addDeployedVestings(_tokenOwners[i], _vestingCreationTypes[i]); - } - } + /** + * @notice adds vestings that were deployed in previous vesting registries + * @dev migration of data from previous vesting registy contracts + */ + function addDeployedVestings( + address[] calldata _tokenOwners, + uint256[] calldata _vestingCreationTypes + ) external onlyAuthorized { + for (uint256 i = 0; i < _tokenOwners.length; i++) { + require(_tokenOwners[i] != address(0), "token owner cannot be 0 address"); + require(_vestingCreationTypes[i] > 0, "vesting creation type must be greater than 0"); + _addDeployedVestings(_tokenOwners[i], _vestingCreationTypes[i]); + } + } - /** - * @notice adds four year vestings to vesting registry logic - * @param _tokenOwners array of token owners - * @param _vestingAddresses array of vesting addresses - */ - function addFourYearVestings(address[] calldata _tokenOwners, address[] calldata _vestingAddresses) external onlyAuthorized { - require(_tokenOwners.length == _vestingAddresses.length, "arrays mismatch"); - uint256 vestingCreationType = 4; - uint256 cliff = 4 weeks; - uint256 duration = 156 weeks; - for (uint256 i = 0; i < _tokenOwners.length; i++) { - require(_tokenOwners[i] != address(0), "token owner cannot be 0 address"); - require(_vestingAddresses[i] != address(0), "vesting cannot be 0 address"); - uint256 uid = - uint256(keccak256(abi.encodePacked(_tokenOwners[i], uint256(VestingType.Vesting), cliff, duration, vestingCreationType))); - vestings[uid] = Vesting(uint256(VestingType.Vesting), vestingCreationType, _vestingAddresses[i]); - vestingsOf[_tokenOwners[i]].push(uid); - isVesting[_vestingAddresses[i]] = true; - } - } + /** + * @notice adds four year vestings to vesting registry logic + * @param _tokenOwners array of token owners + * @param _vestingAddresses array of vesting addresses + */ + function addFourYearVestings( + address[] calldata _tokenOwners, + address[] calldata _vestingAddresses + ) external onlyAuthorized { + require(_tokenOwners.length == _vestingAddresses.length, "arrays mismatch"); + uint256 vestingCreationType = 4; + uint256 cliff = 4 weeks; + uint256 duration = 156 weeks; + for (uint256 i = 0; i < _tokenOwners.length; i++) { + require(_tokenOwners[i] != address(0), "token owner cannot be 0 address"); + require(_vestingAddresses[i] != address(0), "vesting cannot be 0 address"); + uint256 uid = + uint256( + keccak256( + abi.encodePacked( + _tokenOwners[i], + uint256(VestingType.Vesting), + cliff, + duration, + vestingCreationType + ) + ) + ); + vestings[uid] = Vesting( + uint256(VestingType.Vesting), + vestingCreationType, + _vestingAddresses[i] + ); + vestingsOf[_tokenOwners[i]].push(uid); + isVesting[_vestingAddresses[i]] = true; + } + } - /** - * @notice creates Vesting contract - * @param _tokenOwner the owner of the tokens - * @param _amount the amount to be staked - * @param _cliff the cliff in seconds - * @param _duration the total duration in seconds - * @dev Calls a public createVestingAddr function with vestingCreationType. This is to accomodate the existing logic for LockedSOV - * @dev vestingCreationType 0 = LockedSOV - */ - function createVesting( - address _tokenOwner, - uint256 _amount, - uint256 _cliff, - uint256 _duration - ) external onlyAuthorized { - createVestingAddr(_tokenOwner, _amount, _cliff, _duration, 3); - } + /** + * @notice creates Vesting contract + * @param _tokenOwner the owner of the tokens + * @param _amount the amount to be staked + * @param _cliff the cliff in seconds + * @param _duration the total duration in seconds + * @dev Calls a public createVestingAddr function with vestingCreationType. This is to accomodate the existing logic for LockedSOV + * @dev vestingCreationType 0 = LockedSOV + */ + function createVesting( + address _tokenOwner, + uint256 _amount, + uint256 _cliff, + uint256 _duration + ) external onlyAuthorized { + createVestingAddr(_tokenOwner, _amount, _cliff, _duration, 3); + } - /** - * @notice creates Vesting contract - * @param _tokenOwner the owner of the tokens - * @param _amount the amount to be staked - * @param _cliff the cliff in seconds - * @param _duration the total duration in seconds - * @param _vestingCreationType the type of vesting created(e.g. Origin, Bug Bounty etc.) - */ - function createVestingAddr( - address _tokenOwner, - uint256 _amount, - uint256 _cliff, - uint256 _duration, - uint256 _vestingCreationType - ) public onlyAuthorized { - address vesting = _getOrCreateVesting(_tokenOwner, _cliff, _duration, uint256(VestingType.Vesting), _vestingCreationType); - emit VestingCreated(_tokenOwner, vesting, _cliff, _duration, _amount, _vestingCreationType); - } + /** + * @notice creates Vesting contract + * @param _tokenOwner the owner of the tokens + * @param _amount the amount to be staked + * @param _cliff the cliff in seconds + * @param _duration the total duration in seconds + * @param _vestingCreationType the type of vesting created(e.g. Origin, Bug Bounty etc.) + */ + function createVestingAddr( + address _tokenOwner, + uint256 _amount, + uint256 _cliff, + uint256 _duration, + uint256 _vestingCreationType + ) public onlyAuthorized { + address vesting = + _getOrCreateVesting( + _tokenOwner, + _cliff, + _duration, + uint256(VestingType.Vesting), + _vestingCreationType + ); + emit VestingCreated( + _tokenOwner, + vesting, + _cliff, + _duration, + _amount, + _vestingCreationType + ); + } - /** - * @notice creates Team Vesting contract - * @param _tokenOwner the owner of the tokens - * @param _amount the amount to be staked - * @param _cliff the cliff in seconds - * @param _duration the total duration in seconds - * @param _vestingCreationType the type of vesting created(e.g. Origin, Bug Bounty etc.) - */ - function createTeamVesting( - address _tokenOwner, - uint256 _amount, - uint256 _cliff, - uint256 _duration, - uint256 _vestingCreationType - ) external onlyAuthorized { - address vesting = _getOrCreateVesting(_tokenOwner, _cliff, _duration, uint256(VestingType.TeamVesting), _vestingCreationType); - emit TeamVestingCreated(_tokenOwner, vesting, _cliff, _duration, _amount, _vestingCreationType); - } + /** + * @notice creates Team Vesting contract + * @param _tokenOwner the owner of the tokens + * @param _amount the amount to be staked + * @param _cliff the cliff in seconds + * @param _duration the total duration in seconds + * @param _vestingCreationType the type of vesting created(e.g. Origin, Bug Bounty etc.) + */ + function createTeamVesting( + address _tokenOwner, + uint256 _amount, + uint256 _cliff, + uint256 _duration, + uint256 _vestingCreationType + ) external onlyAuthorized { + address vesting = + _getOrCreateVesting( + _tokenOwner, + _cliff, + _duration, + uint256(VestingType.TeamVesting), + _vestingCreationType + ); + emit TeamVestingCreated( + _tokenOwner, + vesting, + _cliff, + _duration, + _amount, + _vestingCreationType + ); + } - /** - * @notice stakes tokens according to the vesting schedule - * @param _vesting the address of Vesting contract - * @param _amount the amount of tokens to stake - */ - function stakeTokens(address _vesting, uint256 _amount) external onlyAuthorized { - require(_vesting != address(0), "vesting address invalid"); - require(_amount > 0, "amount invalid"); + /** + * @notice stakes tokens according to the vesting schedule + * @param _vesting the address of Vesting contract + * @param _amount the amount of tokens to stake + */ + function stakeTokens(address _vesting, uint256 _amount) external onlyAuthorized { + require(_vesting != address(0), "vesting address invalid"); + require(_amount > 0, "amount invalid"); - IERC20(SOV).approve(_vesting, _amount); - IVesting(_vesting).stakeTokens(_amount); - emit TokensStaked(_vesting, _amount); - } + IERC20(SOV).approve(_vesting, _amount); + IVesting(_vesting).stakeTokens(_amount); + emit TokensStaked(_vesting, _amount); + } - /** - * @notice returns vesting contract address for the given token owner - * @param _tokenOwner the owner of the tokens - * @dev Calls a public getVestingAddr function with cliff and duration. This is to accomodate the existing logic for LockedSOV - * @dev We need to use LockedSOV.changeRegistryCliffAndDuration function very judiciously - * @dev vestingCreationType 0 - LockedSOV - */ - function getVesting(address _tokenOwner) public view returns (address) { - return getVestingAddr(_tokenOwner, lockedSOV.cliff(), lockedSOV.duration(), 3); - } + /** + * @notice returns vesting contract address for the given token owner + * @param _tokenOwner the owner of the tokens + * @dev Calls a public getVestingAddr function with cliff and duration. This is to accomodate the existing logic for LockedSOV + * @dev We need to use LockedSOV.changeRegistryCliffAndDuration function very judiciously + * @dev vestingCreationType 0 - LockedSOV + */ + function getVesting(address _tokenOwner) public view returns (address) { + return getVestingAddr(_tokenOwner, lockedSOV.cliff(), lockedSOV.duration(), 3); + } - /** - * @notice public function that returns vesting contract address for the given token owner, cliff, duration - * @dev Important: Please use this instead of getVesting function - */ - function getVestingAddr( - address _tokenOwner, - uint256 _cliff, - uint256 _duration, - uint256 _vestingCreationType - ) public view returns (address) { - uint256 type_ = uint256(VestingType.Vesting); - uint256 uid = uint256(keccak256(abi.encodePacked(_tokenOwner, type_, _cliff, _duration, _vestingCreationType))); - return vestings[uid].vestingAddress; - } + /** + * @notice public function that returns vesting contract address for the given token owner, cliff, duration + * @dev Important: Please use this instead of getVesting function + */ + function getVestingAddr( + address _tokenOwner, + uint256 _cliff, + uint256 _duration, + uint256 _vestingCreationType + ) public view returns (address) { + uint256 type_ = uint256(VestingType.Vesting); + uint256 uid = + uint256( + keccak256( + abi.encodePacked(_tokenOwner, type_, _cliff, _duration, _vestingCreationType) + ) + ); + return vestings[uid].vestingAddress; + } - /** - * @notice returns team vesting contract address for the given token owner, cliff, duration - */ - function getTeamVesting( - address _tokenOwner, - uint256 _cliff, - uint256 _duration, - uint256 _vestingCreationType - ) public view returns (address) { - uint256 type_ = uint256(VestingType.TeamVesting); - uint256 uid = uint256(keccak256(abi.encodePacked(_tokenOwner, type_, _cliff, _duration, _vestingCreationType))); - return vestings[uid].vestingAddress; - } + /** + * @notice returns team vesting contract address for the given token owner, cliff, duration + */ + function getTeamVesting( + address _tokenOwner, + uint256 _cliff, + uint256 _duration, + uint256 _vestingCreationType + ) public view returns (address) { + uint256 type_ = uint256(VestingType.TeamVesting); + uint256 uid = + uint256( + keccak256( + abi.encodePacked(_tokenOwner, type_, _cliff, _duration, _vestingCreationType) + ) + ); + return vestings[uid].vestingAddress; + } - /** - * @notice Internal function to deploy Vesting/Team Vesting contract - * @param _tokenOwner the owner of the tokens - * @param _cliff the cliff in seconds - * @param _duration the total duration in seconds - * @param _type the type of vesting - * @param _vestingCreationType the type of vesting created(e.g. Origin, Bug Bounty etc.) - */ - function _getOrCreateVesting( - address _tokenOwner, - uint256 _cliff, - uint256 _duration, - uint256 _type, - uint256 _vestingCreationType - ) internal returns (address) { - address vesting; - uint256 uid = uint256(keccak256(abi.encodePacked(_tokenOwner, _type, _cliff, _duration, _vestingCreationType))); - if (vestings[uid].vestingAddress == address(0)) { - if (_type == 1) { - vesting = vestingFactory.deployVesting(SOV, staking, _tokenOwner, _cliff, _duration, feeSharingProxy, _tokenOwner); - } else { - vesting = vestingFactory.deployTeamVesting(SOV, staking, _tokenOwner, _cliff, _duration, feeSharingProxy, vestingOwner); - } - vestings[uid] = Vesting(_type, _vestingCreationType, vesting); - vestingsOf[_tokenOwner].push(uid); - isVesting[vesting] = true; - } - return vestings[uid].vestingAddress; - } + /** + * @notice Internal function to deploy Vesting/Team Vesting contract + * @param _tokenOwner the owner of the tokens + * @param _cliff the cliff in seconds + * @param _duration the total duration in seconds + * @param _type the type of vesting + * @param _vestingCreationType the type of vesting created(e.g. Origin, Bug Bounty etc.) + */ + function _getOrCreateVesting( + address _tokenOwner, + uint256 _cliff, + uint256 _duration, + uint256 _type, + uint256 _vestingCreationType + ) internal returns (address) { + address vesting; + uint256 uid = + uint256( + keccak256( + abi.encodePacked(_tokenOwner, _type, _cliff, _duration, _vestingCreationType) + ) + ); + if (vestings[uid].vestingAddress == address(0)) { + if (_type == 1) { + vesting = vestingFactory.deployVesting( + SOV, + staking, + _tokenOwner, + _cliff, + _duration, + feeSharingProxy, + _tokenOwner + ); + } else { + vesting = vestingFactory.deployTeamVesting( + SOV, + staking, + _tokenOwner, + _cliff, + _duration, + feeSharingProxy, + vestingOwner + ); + } + vestings[uid] = Vesting(_type, _vestingCreationType, vesting); + vestingsOf[_tokenOwner].push(uid); + isVesting[vesting] = true; + } + return vestings[uid].vestingAddress; + } - /** - * @notice stores the addresses of Vesting contracts from all three previous versions of Vesting Registry - */ - function _addDeployedVestings(address _tokenOwner, uint256 _vestingCreationType) internal { - uint256 uid; - uint256 i = _vestingCreationType - 1; + /** + * @notice stores the addresses of Vesting contracts from all three previous versions of Vesting Registry + */ + function _addDeployedVestings(address _tokenOwner, uint256 _vestingCreationType) internal { + uint256 uid; + uint256 i = _vestingCreationType - 1; - address vestingAddress = vestingRegistries[i].getVesting(_tokenOwner); - if (vestingAddress != address(0)) { - VestingLogic vesting = VestingLogic(vestingAddress); - uid = uint256( - keccak256( - abi.encodePacked(_tokenOwner, uint256(VestingType.Vesting), vesting.cliff(), vesting.duration(), _vestingCreationType) - ) - ); - vestings[uid] = Vesting(uint256(VestingType.Vesting), _vestingCreationType, vestingAddress); - vestingsOf[_tokenOwner].push(uid); - isVesting[vestingAddress] = true; - } + address vestingAddress = vestingRegistries[i].getVesting(_tokenOwner); + if (vestingAddress != address(0)) { + VestingLogic vesting = VestingLogic(vestingAddress); + uid = uint256( + keccak256( + abi.encodePacked( + _tokenOwner, + uint256(VestingType.Vesting), + vesting.cliff(), + vesting.duration(), + _vestingCreationType + ) + ) + ); + vestings[uid] = Vesting( + uint256(VestingType.Vesting), + _vestingCreationType, + vestingAddress + ); + vestingsOf[_tokenOwner].push(uid); + isVesting[vestingAddress] = true; + } - address teamVestingAddress = vestingRegistries[i].getTeamVesting(_tokenOwner); - if (teamVestingAddress != address(0)) { - VestingLogic vesting = VestingLogic(teamVestingAddress); - uid = uint256( - keccak256( - abi.encodePacked( - _tokenOwner, - uint256(VestingType.TeamVesting), - vesting.cliff(), - vesting.duration(), - _vestingCreationType - ) - ) - ); - vestings[uid] = Vesting(uint256(VestingType.TeamVesting), _vestingCreationType, teamVestingAddress); - vestingsOf[_tokenOwner].push(uid); - isVesting[teamVestingAddress] = true; - } - } + address teamVestingAddress = vestingRegistries[i].getTeamVesting(_tokenOwner); + if (teamVestingAddress != address(0)) { + VestingLogic vesting = VestingLogic(teamVestingAddress); + uid = uint256( + keccak256( + abi.encodePacked( + _tokenOwner, + uint256(VestingType.TeamVesting), + vesting.cliff(), + vesting.duration(), + _vestingCreationType + ) + ) + ); + vestings[uid] = Vesting( + uint256(VestingType.TeamVesting), + _vestingCreationType, + teamVestingAddress + ); + vestingsOf[_tokenOwner].push(uid); + isVesting[teamVestingAddress] = true; + } + } - /** - * @notice returns all vesting details for the given token owner - */ - function getVestingsOf(address _tokenOwner) external view returns (Vesting[] memory) { - uint256[] memory vestingIds = vestingsOf[_tokenOwner]; - uint256 length = vestingIds.length; - Vesting[] memory _vestings = new Vesting[](vestingIds.length); - for (uint256 i = 0; i < length; i++) { - _vestings[i] = vestings[vestingIds[i]]; - } - return _vestings; - } + /** + * @notice returns all vesting details for the given token owner + */ + function getVestingsOf(address _tokenOwner) external view returns (Vesting[] memory) { + uint256[] memory vestingIds = vestingsOf[_tokenOwner]; + uint256 length = vestingIds.length; + Vesting[] memory _vestings = new Vesting[](vestingIds.length); + for (uint256 i = 0; i < length; i++) { + _vestings[i] = vestings[vestingIds[i]]; + } + return _vestings; + } - /** - * @notice returns cliff and duration for Vesting & TeamVesting contracts - */ - function getVestingDetails(address _vestingAddress) external view returns (uint256 cliff, uint256 duration) { - VestingLogic vesting = VestingLogic(_vestingAddress); - return (vesting.cliff(), vesting.duration()); - } + /** + * @notice returns cliff and duration for Vesting & TeamVesting contracts + */ + function getVestingDetails(address _vestingAddress) + external + view + returns (uint256 cliff, uint256 duration) + { + VestingLogic vesting = VestingLogic(_vestingAddress); + return (vesting.cliff(), vesting.duration()); + } - /** - * @notice returns if the address is a vesting address - */ - function isVestingAdress(address _vestingAddress) external view returns (bool isVestingAddr) { - return isVesting[_vestingAddress]; - } + /** + * @notice returns if the address is a vesting address + */ + function isVestingAdress(address _vestingAddress) external view returns (bool isVestingAddr) { + return isVesting[_vestingAddress]; + } } diff --git a/contracts/governance/Vesting/VestingRegistryStorage.sol b/contracts/governance/Vesting/VestingRegistryStorage.sol index e694bb7e5..f3b383f84 100644 --- a/contracts/governance/Vesting/VestingRegistryStorage.sol +++ b/contracts/governance/Vesting/VestingRegistryStorage.sol @@ -17,48 +17,48 @@ import "./IVestingRegistry.sol"; * */ contract VestingRegistryStorage is Initializable, AdminRole { - ///@notice the vesting factory contract - IVestingFactory public vestingFactory; + ///@notice the vesting factory contract + IVestingFactory public vestingFactory; - ///@notice the Locked SOV contract - LockedSOV public lockedSOV; + ///@notice the Locked SOV contract + LockedSOV public lockedSOV; - ///@notice the list of vesting registries - IVestingRegistry[] public vestingRegistries; + ///@notice the list of vesting registries + IVestingRegistry[] public vestingRegistries; - ///@notice the SOV token contract - address public SOV; + ///@notice the SOV token contract + address public SOV; - ///@notice the staking contract address - address public staking; + ///@notice the staking contract address + address public staking; - ///@notice fee sharing proxy - address public feeSharingProxy; + ///@notice fee sharing proxy + address public feeSharingProxy; - ///@notice the vesting owner (e.g. governance timelock address) - address public vestingOwner; + ///@notice the vesting owner (e.g. governance timelock address) + address public vestingOwner; - enum VestingType { - TeamVesting, //MultisigVesting - Vesting //TokenHolderVesting - } + enum VestingType { + TeamVesting, //MultisigVesting + Vesting //TokenHolderVesting + } - ///@notice Vesting details - struct Vesting { - uint256 vestingType; - uint256 vestingCreationType; - address vestingAddress; - } + ///@notice Vesting details + struct Vesting { + uint256 vestingType; + uint256 vestingCreationType; + address vestingAddress; + } - ///@notice A record of vesting details for a unique id - ///@dev vestings[uid] returns vesting data - mapping(uint256 => Vesting) public vestings; + ///@notice A record of vesting details for a unique id + ///@dev vestings[uid] returns vesting data + mapping(uint256 => Vesting) public vestings; - ///@notice A record of all unique ids for a particular token owner - ///@dev vestingsOf[tokenOwner] returns array of unique ids - mapping(address => uint256[]) public vestingsOf; + ///@notice A record of all unique ids for a particular token owner + ///@dev vestingsOf[tokenOwner] returns array of unique ids + mapping(address => uint256[]) public vestingsOf; - ///@notice A record of all vesting addresses - ///@dev isVesting[address] returns if the address is a vesting address - mapping(address => bool) public isVesting; + ///@notice A record of all vesting addresses + ///@dev isVesting[address] returns if the address is a vesting address + mapping(address => bool) public isVesting; } diff --git a/contracts/governance/Vesting/VestingStorage.sol b/contracts/governance/Vesting/VestingStorage.sol index 83218faa7..65b340ece 100644 --- a/contracts/governance/Vesting/VestingStorage.sol +++ b/contracts/governance/Vesting/VestingStorage.sol @@ -14,30 +14,30 @@ import "../IFeeSharingProxy.sol"; * @dev Use Ownable as a parent to align storage structure for Logic and Proxy contracts. * */ contract VestingStorage is Ownable { - /// @notice The SOV token contract. - IERC20 public SOV; + /// @notice The SOV token contract. + IERC20 public SOV; - /// @notice The staking contract address. - Staking public staking; + /// @notice The staking contract address. + Staking public staking; - /// @notice The owner of the vested tokens. - address public tokenOwner; + /// @notice The owner of the vested tokens. + address public tokenOwner; - /// @notice Fee sharing Proxy. - IFeeSharingProxy public feeSharingProxy; + /// @notice Fee sharing Proxy. + IFeeSharingProxy public feeSharingProxy; - /// @notice The cliff. After this time period the tokens begin to unlock. - uint256 public cliff; + /// @notice The cliff. After this time period the tokens begin to unlock. + uint256 public cliff; - /// @notice The duration. After this period all tokens will have been unlocked. - uint256 public duration; + /// @notice The duration. After this period all tokens will have been unlocked. + uint256 public duration; - /// @notice The start date of the vesting. - uint256 public startDate; + /// @notice The start date of the vesting. + uint256 public startDate; - /// @notice The end date of the vesting. - uint256 public endDate; + /// @notice The end date of the vesting. + uint256 public endDate; - /// @notice Constant used for computing the vesting dates. - uint256 constant FOUR_WEEKS = 4 weeks; + /// @notice Constant used for computing the vesting dates. + uint256 constant FOUR_WEEKS = 4 weeks; } diff --git a/contracts/governance/Vesting/fouryear/FourYearVesting.sol b/contracts/governance/Vesting/fouryear/FourYearVesting.sol index 0dfe1a3a1..8f6efce13 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVesting.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVesting.sol @@ -20,49 +20,49 @@ import "../../../openzeppelin/Address.sol"; * catastrophic. * */ contract FourYearVesting is FourYearVestingStorage, UpgradableProxy { - /** - * @notice Setup the vesting schedule. - * @param _logic The address of logic contract. - * @param _SOV The SOV token address. - * @param _tokenOwner The owner of the tokens. - * @param _feeSharingProxy Fee sharing proxy address. - * */ - constructor( - address _logic, - address _SOV, - address _stakingAddress, - address _tokenOwner, - address _feeSharingProxy - ) public { - require(Address.isContract(_logic), "_logic not a contract"); - require(_SOV != address(0), "SOV address invalid"); - require(Address.isContract(_SOV), "_SOV not a contract"); - require(_stakingAddress != address(0), "staking address invalid"); - require(Address.isContract(_stakingAddress), "_stakingAddress not a contract"); - require(_tokenOwner != address(0), "token owner address invalid"); - require(_feeSharingProxy != address(0), "feeSharingProxy address invalid"); - require(Address.isContract(_feeSharingProxy), "_feeSharingProxy not a contract"); + /** + * @notice Setup the vesting schedule. + * @param _logic The address of logic contract. + * @param _SOV The SOV token address. + * @param _tokenOwner The owner of the tokens. + * @param _feeSharingProxy Fee sharing proxy address. + * */ + constructor( + address _logic, + address _SOV, + address _stakingAddress, + address _tokenOwner, + address _feeSharingProxy + ) public { + require(Address.isContract(_logic), "_logic not a contract"); + require(_SOV != address(0), "SOV address invalid"); + require(Address.isContract(_SOV), "_SOV not a contract"); + require(_stakingAddress != address(0), "staking address invalid"); + require(Address.isContract(_stakingAddress), "_stakingAddress not a contract"); + require(_tokenOwner != address(0), "token owner address invalid"); + require(_feeSharingProxy != address(0), "feeSharingProxy address invalid"); + require(Address.isContract(_feeSharingProxy), "_feeSharingProxy not a contract"); - _setImplementation(_logic); - SOV = IERC20(_SOV); - staking = Staking(_stakingAddress); - tokenOwner = _tokenOwner; - feeSharingProxy = IFeeSharingProxy(_feeSharingProxy); - maxInterval = 18 * FOUR_WEEKS; - } + _setImplementation(_logic); + SOV = IERC20(_SOV); + staking = Staking(_stakingAddress); + tokenOwner = _tokenOwner; + feeSharingProxy = IFeeSharingProxy(_feeSharingProxy); + maxInterval = 18 * FOUR_WEEKS; + } - /** - * @notice Set address of the implementation - vesting owner. - * @dev Overriding setImplementation function of UpgradableProxy. The logic can only be - * modified when both token owner and veting owner approve. Since - * setImplementation can only be called by vesting owner, we also need to check - * if the new logic is already approved by the token owner. - * @param _implementation Address of the implementation. Must match with what is set by token owner. - * */ - function setImplementation(address _implementation) public onlyProxyOwner { - require(Address.isContract(_implementation), "_implementation not a contract"); - require(newImplementation == _implementation, "address mismatch"); - _setImplementation(_implementation); - newImplementation = address(0); - } + /** + * @notice Set address of the implementation - vesting owner. + * @dev Overriding setImplementation function of UpgradableProxy. The logic can only be + * modified when both token owner and veting owner approve. Since + * setImplementation can only be called by vesting owner, we also need to check + * if the new logic is already approved by the token owner. + * @param _implementation Address of the implementation. Must match with what is set by token owner. + * */ + function setImplementation(address _implementation) public onlyProxyOwner { + require(Address.isContract(_implementation), "_implementation not a contract"); + require(newImplementation == _implementation, "address mismatch"); + _setImplementation(_implementation); + newImplementation = address(0); + } } diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol b/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol index 86be0eea5..d2e60a08d 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol @@ -10,31 +10,40 @@ import "./IFourYearVestingFactory.sol"; * of the same contract and keep track of them easier. * */ contract FourYearVestingFactory is IFourYearVestingFactory, Ownable { - /// @dev Added an event to keep track of the vesting contract created for a token owner - event FourYearVestingCreated(address indexed tokenOwner, address indexed vestingAddress); + /// @dev Added an event to keep track of the vesting contract created for a token owner + event FourYearVestingCreated(address indexed tokenOwner, address indexed vestingAddress); - /** - * @notice Deploys four year vesting contract. - * @param _SOV the address of SOV token. - * @param _staking The address of staking contract. - * @param _tokenOwner The owner of the tokens. - * @param _feeSharing The address of fee sharing contract. - * @param _vestingOwnerMultisig The address of an owner of vesting contract. - * @dev _vestingOwnerMultisig should ALWAYS be multisig. - * @param _fourYearVestingLogic The implementation contract. - * @return The four year vesting contract address. - * */ - function deployFourYearVesting( - address _SOV, - address _staking, - address _tokenOwner, - address _feeSharing, - address _vestingOwnerMultisig, - address _fourYearVestingLogic - ) external onlyOwner returns (address) { - address fourYearVesting = address(new FourYearVesting(_fourYearVestingLogic, _SOV, _staking, _tokenOwner, _feeSharing)); - Ownable(fourYearVesting).transferOwnership(_vestingOwnerMultisig); - emit FourYearVestingCreated(_tokenOwner, fourYearVesting); - return fourYearVesting; - } + /** + * @notice Deploys four year vesting contract. + * @param _SOV the address of SOV token. + * @param _staking The address of staking contract. + * @param _tokenOwner The owner of the tokens. + * @param _feeSharing The address of fee sharing contract. + * @param _vestingOwnerMultisig The address of an owner of vesting contract. + * @dev _vestingOwnerMultisig should ALWAYS be multisig. + * @param _fourYearVestingLogic The implementation contract. + * @return The four year vesting contract address. + * */ + function deployFourYearVesting( + address _SOV, + address _staking, + address _tokenOwner, + address _feeSharing, + address _vestingOwnerMultisig, + address _fourYearVestingLogic + ) external onlyOwner returns (address) { + address fourYearVesting = + address( + new FourYearVesting( + _fourYearVestingLogic, + _SOV, + _staking, + _tokenOwner, + _feeSharing + ) + ); + Ownable(fourYearVesting).transferOwnership(_vestingOwnerMultisig); + emit FourYearVestingCreated(_tokenOwner, fourYearVesting); + return fourYearVesting; + } } diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol index 37a288db8..61e1fce29 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol @@ -12,327 +12,349 @@ import "../../../openzeppelin/SafeMath.sol"; * @dev Deployed by FourYearVestingFactory contract. * */ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, ApprovalReceiver { - using SafeMath for uint256; - - /* Events */ - event TokensStaked(address indexed caller, uint256 amount); - event VotesDelegated(address indexed caller, address delegatee); - event TokensWithdrawn(address indexed caller, address receiver); - event DividendsCollected(address indexed caller, address loanPoolToken, address receiver, uint32 maxCheckpoints); - event MigratedToNewStakingContract(address indexed caller, address newStakingContract); - event TokenOwnerChanged(address indexed newOwner, address indexed oldOwner); - - /* Modifiers */ - /** - * @dev Throws if called by any account other than the token owner or the contract owner. - */ - modifier onlyOwners() { - require(msg.sender == tokenOwner || isOwner(), "unauthorized"); - _; - } - - /** - * @dev Throws if called by any account other than the token owner. - */ - modifier onlyTokenOwner() { - require(msg.sender == tokenOwner, "unauthorized"); - _; - } - - /* Functions */ - - /** - * @notice Sets the max interval. - * @param _interval Max interval for which tokens scheduled shall be staked. - * */ - function setMaxInterval(uint256 _interval) external onlyOwner { - require(_interval.mod(FOUR_WEEKS) == 0, "invalid interval"); - maxInterval = _interval; - } - - /** - * @notice Stakes tokens according to the vesting schedule. - * @param _amount The amount of tokens to stake. - * @param _restartStakeSchedule The time from which staking schedule restarts. - * The issue is that we can only stake tokens for a max duration. Thus, we need to restart - * from the lastSchedule. - * @return lastSchedule The max duration for which tokens were staked. - * @return remainingAmount The amount outstanding - to be staked. - * */ - function stakeTokens(uint256 _amount, uint256 _restartStakeSchedule) external returns (uint256 lastSchedule, uint256 remainingAmount) { - (lastSchedule, remainingAmount) = _stakeTokens(msg.sender, _amount, _restartStakeSchedule); - } - - /** - * @notice Stakes tokens according to the vesting schedule. - * @dev This function will be invoked from receiveApproval. - * @dev SOV.approveAndCall -> this.receiveApproval -> this.stakeTokensWithApproval - * @param _sender The sender of SOV.approveAndCall - * @param _amount The amount of tokens to stake. - * @param _restartStakeSchedule The time from which staking schedule restarts. - * The issue is that we can only stake tokens for a max duration. Thus, we need to restart - * from the lastSchedule. - * @return lastSchedule The max duration for which tokens were staked. - * @return remainingAmount The amount outstanding - to be staked. - * */ - function stakeTokensWithApproval( - address _sender, - uint256 _amount, - uint256 _restartStakeSchedule - ) external onlyThisContract returns (uint256 lastSchedule, uint256 remainingAmount) { - (lastSchedule, remainingAmount) = _stakeTokens(_sender, _amount, _restartStakeSchedule); - } - - /** - * @notice Delegate votes from `msg.sender` which are locked until lockDate - * to `delegatee`. - * @param _delegatee The address to delegate votes to. - * */ - function delegate(address _delegatee) external onlyTokenOwner { - require(_delegatee != address(0), "delegatee address invalid"); - uint256 stakingEndDate = endDate; - /// @dev Withdraw for each unlocked position. - /// @dev Don't change FOUR_WEEKS to TWO_WEEKS, a lot of vestings already deployed with FOUR_WEEKS - /// workaround found, but it doesn't work with TWO_WEEKS - for (uint256 i = startDate.add(CLIFF); i <= stakingEndDate; i += FOUR_WEEKS) { - staking.delegate(_delegatee, i); - } - emit VotesDelegated(msg.sender, _delegatee); - } - - /** - * @notice Withdraws unlocked tokens from the staking contract and - * forwards them to an address specified by the token owner. - * @param receiver The receiving address. - * */ - function withdrawTokens(address receiver) external onlyTokenOwner { - _withdrawTokens(receiver, false); - } - - /** - * @notice Collect dividends from fee sharing proxy. - * @param _loanPoolToken The loan pool token address. - * @param _maxCheckpoints Maximum number of checkpoints to be processed. - * @param _receiver The receiver of tokens or msg.sender - * */ - function collectDividends( - address _loanPoolToken, - uint32 _maxCheckpoints, - address _receiver - ) external onlyTokenOwner { - require(_receiver != address(0), "receiver address invalid"); - - /// @dev Invokes the fee sharing proxy. - feeSharingProxy.withdraw(_loanPoolToken, _maxCheckpoints, _receiver); - - emit DividendsCollected(msg.sender, _loanPoolToken, _receiver, _maxCheckpoints); - } - - /** - * @notice Change token owner - only vesting owner is allowed to change. - * @dev Modifies token owner. This must be followed by approval - * from token owner. - * @param _newTokenOwner Address of new token owner. - * */ - function changeTokenOwner(address _newTokenOwner) public onlyOwner { - require(_newTokenOwner != address(0), "invalid new token owner address"); - require(_newTokenOwner != tokenOwner, "same owner not allowed"); - newTokenOwner = _newTokenOwner; - } - - /** - * @notice Approve token owner change - only token Owner. - * @dev Token owner can only be modified - * when both vesting owner and token owner have approved. This - * function ascertains the approval of token owner. - * */ - function approveOwnershipTransfer() public onlyTokenOwner { - require(newTokenOwner != address(0), "invalid address"); - tokenOwner = newTokenOwner; - newTokenOwner = address(0); - emit TokenOwnerChanged(tokenOwner, msg.sender); - } - - /** - * @notice Set address of the implementation - only Token Owner. - * @dev This function sets the new implementation address. - * It must also be approved by the Vesting owner. - * @param _newImplementation Address of the new implementation. - * */ - function setImpl(address _newImplementation) public onlyTokenOwner { - require(_newImplementation != address(0), "invalid new implementation address"); - newImplementation = _newImplementation; - } - - /** - * @notice Allows the owners to migrate the positions - * to a new staking contract. - * */ - function migrateToNewStakingContract() external onlyOwners { - staking.migrateToNewStakingContract(); - staking = Staking(staking.newStakingContract()); - emit MigratedToNewStakingContract(msg.sender, address(staking)); - } - - /** - * @notice Extends first year stakes for four year vesting contracts. - * @dev Tokens are vested for 4 years. Since the max staking - * period is 3 years and the tokens are unlocked only after the first year is - * passed, hence, we extend the duration of staking for all unlocked tokens for the first - * year by 3 years. - * */ - function extendStaking() external { - uint256 oneYear = startDate.add(52 weeks); - uint256[] memory dates; - uint96[] memory stakes; - (dates, stakes) = staking.getStakes(address(this)); - - for (uint256 i = 0; i < dates.length; i++) { - if ((dates[i] < block.timestamp) && (dates[i] <= oneYear) && (stakes[i] > 0)) { - staking.extendStakingDuration(dates[i], dates[i].add(156 weeks)); - endDate = dates[i].add(156 weeks); - } else { - break; - } - } - } - - /** - * @notice Stakes tokens according to the vesting schedule. Low level function. - * @dev Once here the allowance of tokens is taken for granted. - * @param _sender The sender of tokens to stake. - * @param _amount The amount of tokens to stake. - * @param _restartStakeSchedule The time from which staking schedule restarts. - * The issue is that we can only stake tokens for a max duration. Thus, we need to restart - * from the lastSchedule. - * @return lastSchedule The max duration for which tokens were staked. - * @return remainingAmount The amount outstanding - to be staked. - * */ - function _stakeTokens( - address _sender, - uint256 _amount, - uint256 _restartStakeSchedule - ) internal returns (uint256 lastSchedule, uint256 remainingAmount) { - // Creating a new staking schedule for the same vesting contract is disallowed unlike normal vesting - require((startDate == 0) || (startDate > 0 && remainingStakeAmount > 0 && _restartStakeSchedule > 0), "create new vesting address"); - uint256 restartDate; - uint256 relativeAmount; - // Calling the _stakeTokens function first time for the vesting contract - // Runs for maxInterval only (consider maxInterval = 18 * 4 = 72 weeks) - if (startDate == 0 && _restartStakeSchedule == 0) { - startDate = staking.timestampToLockDate(block.timestamp); // Set only once - durationLeft = DURATION; // We do not touch duration and cliff as they are used throughout - cliffAdded = CLIFF; // Hence, durationLeft and cliffAdded is created - } - // Calling the _stakeTokens second/third time - we start from the end of previous interval - // and the remaining amount(amount left after tokens are staked in the previous interval) - if (_restartStakeSchedule > 0) { - require(_restartStakeSchedule == lastStakingSchedule && _amount == remainingStakeAmount, "invalid params"); - restartDate = _restartStakeSchedule; - } else { - restartDate = startDate; - } - // Runs only once when the _stakeTokens is called for the first time - if (endDate == 0) { - endDate = staking.timestampToLockDate(block.timestamp.add(DURATION)); - } - uint256 addedMaxInterval = restartDate.add(maxInterval); // run for maxInterval - if (addedMaxInterval < endDate) { - // Runs for max interval - lastStakingSchedule = addedMaxInterval; - relativeAmount = (_amount.mul(maxInterval)).div(durationLeft); // (_amount * 18) / 39 - durationLeft = durationLeft.sub(maxInterval); // durationLeft - 18 periods(72 weeks) - remainingStakeAmount = _amount.sub(relativeAmount); // Amount left to be staked in subsequent intervals - } else { - // Normal run - lastStakingSchedule = endDate; // if staking intervals left < 18 periods(72 weeks) - remainingStakeAmount = 0; - durationLeft = 0; - relativeAmount = _amount; // Stake all amount left - } - - /// @dev Transfer the tokens to this contract. - bool success = SOV.transferFrom(_sender, address(this), relativeAmount); - require(success, "transfer failed"); - - /// @dev Allow the staking contract to access them. - SOV.approve(address(staking), relativeAmount); - - staking.stakesBySchedule(relativeAmount, cliffAdded, DURATION.sub(durationLeft), FOUR_WEEKS, address(this), tokenOwner); - if (durationLeft == 0) { - // All tokens staked - cliffAdded = 0; - } else { - cliffAdded = cliffAdded.add(maxInterval); // Add cliff to the end of previous maxInterval - } - - emit TokensStaked(_sender, relativeAmount); - return (lastStakingSchedule, remainingStakeAmount); - } - - /** - * @notice Withdraws tokens from the staking contract and forwards them - * to an address specified by the token owner. Low level function. - * @dev Once here the caller permission is taken for granted. - * @param receiver The receiving address. - * @param isGovernance Whether all tokens (true) - * or just unlocked tokens (false). - * */ - function _withdrawTokens(address receiver, bool isGovernance) internal { - require(receiver != address(0), "receiver address invalid"); - - uint96 stake; - - /// @dev Usually we just need to iterate over the possible dates until now. - uint256 end; - - /// @dev In the unlikely case that all tokens have been unlocked early, - /// allow to withdraw all of them. - if (staking.allUnlocked() || isGovernance) { - end = endDate; - } else { - end = block.timestamp; - } - - /// @dev Withdraw for each unlocked position. - /// @dev Don't change FOUR_WEEKS to TWO_WEEKS, a lot of vestings already deployed with FOUR_WEEKS - /// workaround found, but it doesn't work with TWO_WEEKS - /// @dev For four year vesting, withdrawal of stakes for the first year is not allowed. These - /// stakes are extended for three years. - for (uint256 i = startDate.add(52 weeks); i <= end; i += FOUR_WEEKS) { - /// @dev Read amount to withdraw. - stake = staking.getPriorUserStakeByDate(address(this), i, block.number.sub(1)); - - /// @dev Withdraw if > 0 - if (stake > 0) { - if (isGovernance) { - staking.governanceWithdraw(stake, i, receiver); - } else { - staking.withdraw(stake, i, receiver); - } - } - } - - emit TokensWithdrawn(msg.sender, receiver); - } - - /** - * @notice Overrides default ApprovalReceiver._getToken function to - * register SOV token on this contract. - * @return The address of SOV token. - * */ - function _getToken() internal view returns (address) { - return address(SOV); - } - - /** - * @notice Overrides default ApprovalReceiver._getSelectors function to - * register stakeTokensWithApproval selector on this contract. - * @return The array of registered selectors on this contract. - * */ - function _getSelectors() internal view returns (bytes4[] memory) { - bytes4[] memory selectors = new bytes4[](1); - selectors[0] = this.stakeTokensWithApproval.selector; - return selectors; - } + using SafeMath for uint256; + + /* Events */ + event TokensStaked(address indexed caller, uint256 amount); + event VotesDelegated(address indexed caller, address delegatee); + event TokensWithdrawn(address indexed caller, address receiver); + event DividendsCollected( + address indexed caller, + address loanPoolToken, + address receiver, + uint32 maxCheckpoints + ); + event MigratedToNewStakingContract(address indexed caller, address newStakingContract); + event TokenOwnerChanged(address indexed newOwner, address indexed oldOwner); + + /* Modifiers */ + /** + * @dev Throws if called by any account other than the token owner or the contract owner. + */ + modifier onlyOwners() { + require(msg.sender == tokenOwner || isOwner(), "unauthorized"); + _; + } + + /** + * @dev Throws if called by any account other than the token owner. + */ + modifier onlyTokenOwner() { + require(msg.sender == tokenOwner, "unauthorized"); + _; + } + + /* Functions */ + + /** + * @notice Sets the max interval. + * @param _interval Max interval for which tokens scheduled shall be staked. + * */ + function setMaxInterval(uint256 _interval) external onlyOwner { + require(_interval.mod(FOUR_WEEKS) == 0, "invalid interval"); + maxInterval = _interval; + } + + /** + * @notice Stakes tokens according to the vesting schedule. + * @param _amount The amount of tokens to stake. + * @param _restartStakeSchedule The time from which staking schedule restarts. + * The issue is that we can only stake tokens for a max duration. Thus, we need to restart + * from the lastSchedule. + * @return lastSchedule The max duration for which tokens were staked. + * @return remainingAmount The amount outstanding - to be staked. + * */ + function stakeTokens(uint256 _amount, uint256 _restartStakeSchedule) + external + returns (uint256 lastSchedule, uint256 remainingAmount) + { + (lastSchedule, remainingAmount) = _stakeTokens(msg.sender, _amount, _restartStakeSchedule); + } + + /** + * @notice Stakes tokens according to the vesting schedule. + * @dev This function will be invoked from receiveApproval. + * @dev SOV.approveAndCall -> this.receiveApproval -> this.stakeTokensWithApproval + * @param _sender The sender of SOV.approveAndCall + * @param _amount The amount of tokens to stake. + * @param _restartStakeSchedule The time from which staking schedule restarts. + * The issue is that we can only stake tokens for a max duration. Thus, we need to restart + * from the lastSchedule. + * @return lastSchedule The max duration for which tokens were staked. + * @return remainingAmount The amount outstanding - to be staked. + * */ + function stakeTokensWithApproval( + address _sender, + uint256 _amount, + uint256 _restartStakeSchedule + ) external onlyThisContract returns (uint256 lastSchedule, uint256 remainingAmount) { + (lastSchedule, remainingAmount) = _stakeTokens(_sender, _amount, _restartStakeSchedule); + } + + /** + * @notice Delegate votes from `msg.sender` which are locked until lockDate + * to `delegatee`. + * @param _delegatee The address to delegate votes to. + * */ + function delegate(address _delegatee) external onlyTokenOwner { + require(_delegatee != address(0), "delegatee address invalid"); + uint256 stakingEndDate = endDate; + /// @dev Withdraw for each unlocked position. + /// @dev Don't change FOUR_WEEKS to TWO_WEEKS, a lot of vestings already deployed with FOUR_WEEKS + /// workaround found, but it doesn't work with TWO_WEEKS + for (uint256 i = startDate.add(CLIFF); i <= stakingEndDate; i += FOUR_WEEKS) { + staking.delegate(_delegatee, i); + } + emit VotesDelegated(msg.sender, _delegatee); + } + + /** + * @notice Withdraws unlocked tokens from the staking contract and + * forwards them to an address specified by the token owner. + * @param receiver The receiving address. + * */ + function withdrawTokens(address receiver) external onlyTokenOwner { + _withdrawTokens(receiver, false); + } + + /** + * @notice Collect dividends from fee sharing proxy. + * @param _loanPoolToken The loan pool token address. + * @param _maxCheckpoints Maximum number of checkpoints to be processed. + * @param _receiver The receiver of tokens or msg.sender + * */ + function collectDividends( + address _loanPoolToken, + uint32 _maxCheckpoints, + address _receiver + ) external onlyTokenOwner { + require(_receiver != address(0), "receiver address invalid"); + + /// @dev Invokes the fee sharing proxy. + feeSharingProxy.withdraw(_loanPoolToken, _maxCheckpoints, _receiver); + + emit DividendsCollected(msg.sender, _loanPoolToken, _receiver, _maxCheckpoints); + } + + /** + * @notice Change token owner - only vesting owner is allowed to change. + * @dev Modifies token owner. This must be followed by approval + * from token owner. + * @param _newTokenOwner Address of new token owner. + * */ + function changeTokenOwner(address _newTokenOwner) public onlyOwner { + require(_newTokenOwner != address(0), "invalid new token owner address"); + require(_newTokenOwner != tokenOwner, "same owner not allowed"); + newTokenOwner = _newTokenOwner; + } + + /** + * @notice Approve token owner change - only token Owner. + * @dev Token owner can only be modified + * when both vesting owner and token owner have approved. This + * function ascertains the approval of token owner. + * */ + function approveOwnershipTransfer() public onlyTokenOwner { + require(newTokenOwner != address(0), "invalid address"); + tokenOwner = newTokenOwner; + newTokenOwner = address(0); + emit TokenOwnerChanged(tokenOwner, msg.sender); + } + + /** + * @notice Set address of the implementation - only Token Owner. + * @dev This function sets the new implementation address. + * It must also be approved by the Vesting owner. + * @param _newImplementation Address of the new implementation. + * */ + function setImpl(address _newImplementation) public onlyTokenOwner { + require(_newImplementation != address(0), "invalid new implementation address"); + newImplementation = _newImplementation; + } + + /** + * @notice Allows the owners to migrate the positions + * to a new staking contract. + * */ + function migrateToNewStakingContract() external onlyOwners { + staking.migrateToNewStakingContract(); + staking = Staking(staking.newStakingContract()); + emit MigratedToNewStakingContract(msg.sender, address(staking)); + } + + /** + * @notice Extends first year stakes for four year vesting contracts. + * @dev Tokens are vested for 4 years. Since the max staking + * period is 3 years and the tokens are unlocked only after the first year is + * passed, hence, we extend the duration of staking for all unlocked tokens for the first + * year by 3 years. + * */ + function extendStaking() external { + uint256 oneYear = startDate.add(52 weeks); + uint256[] memory dates; + uint96[] memory stakes; + (dates, stakes) = staking.getStakes(address(this)); + + for (uint256 i = 0; i < dates.length; i++) { + if ((dates[i] < block.timestamp) && (dates[i] <= oneYear) && (stakes[i] > 0)) { + staking.extendStakingDuration(dates[i], dates[i].add(156 weeks)); + endDate = dates[i].add(156 weeks); + } else { + break; + } + } + } + + /** + * @notice Stakes tokens according to the vesting schedule. Low level function. + * @dev Once here the allowance of tokens is taken for granted. + * @param _sender The sender of tokens to stake. + * @param _amount The amount of tokens to stake. + * @param _restartStakeSchedule The time from which staking schedule restarts. + * The issue is that we can only stake tokens for a max duration. Thus, we need to restart + * from the lastSchedule. + * @return lastSchedule The max duration for which tokens were staked. + * @return remainingAmount The amount outstanding - to be staked. + * */ + function _stakeTokens( + address _sender, + uint256 _amount, + uint256 _restartStakeSchedule + ) internal returns (uint256 lastSchedule, uint256 remainingAmount) { + // Creating a new staking schedule for the same vesting contract is disallowed unlike normal vesting + require( + (startDate == 0) || + (startDate > 0 && remainingStakeAmount > 0 && _restartStakeSchedule > 0), + "create new vesting address" + ); + uint256 restartDate; + uint256 relativeAmount; + // Calling the _stakeTokens function first time for the vesting contract + // Runs for maxInterval only (consider maxInterval = 18 * 4 = 72 weeks) + if (startDate == 0 && _restartStakeSchedule == 0) { + startDate = staking.timestampToLockDate(block.timestamp); // Set only once + durationLeft = DURATION; // We do not touch duration and cliff as they are used throughout + cliffAdded = CLIFF; // Hence, durationLeft and cliffAdded is created + } + // Calling the _stakeTokens second/third time - we start from the end of previous interval + // and the remaining amount(amount left after tokens are staked in the previous interval) + if (_restartStakeSchedule > 0) { + require( + _restartStakeSchedule == lastStakingSchedule && _amount == remainingStakeAmount, + "invalid params" + ); + restartDate = _restartStakeSchedule; + } else { + restartDate = startDate; + } + // Runs only once when the _stakeTokens is called for the first time + if (endDate == 0) { + endDate = staking.timestampToLockDate(block.timestamp.add(DURATION)); + } + uint256 addedMaxInterval = restartDate.add(maxInterval); // run for maxInterval + if (addedMaxInterval < endDate) { + // Runs for max interval + lastStakingSchedule = addedMaxInterval; + relativeAmount = (_amount.mul(maxInterval)).div(durationLeft); // (_amount * 18) / 39 + durationLeft = durationLeft.sub(maxInterval); // durationLeft - 18 periods(72 weeks) + remainingStakeAmount = _amount.sub(relativeAmount); // Amount left to be staked in subsequent intervals + } else { + // Normal run + lastStakingSchedule = endDate; // if staking intervals left < 18 periods(72 weeks) + remainingStakeAmount = 0; + durationLeft = 0; + relativeAmount = _amount; // Stake all amount left + } + + /// @dev Transfer the tokens to this contract. + bool success = SOV.transferFrom(_sender, address(this), relativeAmount); + require(success, "transfer failed"); + + /// @dev Allow the staking contract to access them. + SOV.approve(address(staking), relativeAmount); + + staking.stakesBySchedule( + relativeAmount, + cliffAdded, + DURATION.sub(durationLeft), + FOUR_WEEKS, + address(this), + tokenOwner + ); + if (durationLeft == 0) { + // All tokens staked + cliffAdded = 0; + } else { + cliffAdded = cliffAdded.add(maxInterval); // Add cliff to the end of previous maxInterval + } + + emit TokensStaked(_sender, relativeAmount); + return (lastStakingSchedule, remainingStakeAmount); + } + + /** + * @notice Withdraws tokens from the staking contract and forwards them + * to an address specified by the token owner. Low level function. + * @dev Once here the caller permission is taken for granted. + * @param receiver The receiving address. + * @param isGovernance Whether all tokens (true) + * or just unlocked tokens (false). + * */ + function _withdrawTokens(address receiver, bool isGovernance) internal { + require(receiver != address(0), "receiver address invalid"); + + uint96 stake; + + /// @dev Usually we just need to iterate over the possible dates until now. + uint256 end; + + /// @dev In the unlikely case that all tokens have been unlocked early, + /// allow to withdraw all of them. + if (staking.allUnlocked() || isGovernance) { + end = endDate; + } else { + end = block.timestamp; + } + + /// @dev Withdraw for each unlocked position. + /// @dev Don't change FOUR_WEEKS to TWO_WEEKS, a lot of vestings already deployed with FOUR_WEEKS + /// workaround found, but it doesn't work with TWO_WEEKS + /// @dev For four year vesting, withdrawal of stakes for the first year is not allowed. These + /// stakes are extended for three years. + for (uint256 i = startDate.add(52 weeks); i <= end; i += FOUR_WEEKS) { + /// @dev Read amount to withdraw. + stake = staking.getPriorUserStakeByDate(address(this), i, block.number.sub(1)); + + /// @dev Withdraw if > 0 + if (stake > 0) { + if (isGovernance) { + staking.governanceWithdraw(stake, i, receiver); + } else { + staking.withdraw(stake, i, receiver); + } + } + } + + emit TokensWithdrawn(msg.sender, receiver); + } + + /** + * @notice Overrides default ApprovalReceiver._getToken function to + * register SOV token on this contract. + * @return The address of SOV token. + * */ + function _getToken() internal view returns (address) { + return address(SOV); + } + + /** + * @notice Overrides default ApprovalReceiver._getSelectors function to + * register stakeTokensWithApproval selector on this contract. + * @return The array of registered selectors on this contract. + * */ + function _getSelectors() internal view returns (bytes4[] memory) { + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = this.stakeTokensWithApproval.selector; + return selectors; + } } diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol b/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol index 421e7a8a2..8daa632d7 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol @@ -14,54 +14,54 @@ import "../../IFeeSharingProxy.sol"; * @dev Use Ownable as a parent to align storage structure for Logic and Proxy contracts. * */ contract FourYearVestingStorage is Ownable { - /// @notice The SOV token contract. - IERC20 public SOV; + /// @notice The SOV token contract. + IERC20 public SOV; - /// @notice The staking contract address. - Staking public staking; + /// @notice The staking contract address. + Staking public staking; - /// @notice The owner of the vested tokens. - address public tokenOwner; + /// @notice The owner of the vested tokens. + address public tokenOwner; - /// @notice Fee sharing Proxy. - IFeeSharingProxy public feeSharingProxy; + /// @notice Fee sharing Proxy. + IFeeSharingProxy public feeSharingProxy; - /// @notice The cliff. After this time period the tokens begin to unlock. - uint256 public constant CLIFF = 4 weeks; + /// @notice The cliff. After this time period the tokens begin to unlock. + uint256 public constant CLIFF = 4 weeks; - /// @notice The duration. After this period all tokens will have been unlocked. - uint256 public constant DURATION = 156 weeks; + /// @notice The duration. After this period all tokens will have been unlocked. + uint256 public constant DURATION = 156 weeks; - /// @notice The start date of the vesting. - uint256 public startDate; + /// @notice The start date of the vesting. + uint256 public startDate; - /// @notice The end date of the vesting. - uint256 public endDate; + /// @notice The end date of the vesting. + uint256 public endDate; - /// @notice Constant used for computing the vesting dates. - uint256 public constant FOUR_WEEKS = 4 weeks; + /// @notice Constant used for computing the vesting dates. + uint256 public constant FOUR_WEEKS = 4 weeks; - /// @notice Maximum interval to stake tokens at one go - uint256 public maxInterval; + /// @notice Maximum interval to stake tokens at one go + uint256 public maxInterval; - /// @notice End of previous staking schedule. - uint256 public lastStakingSchedule; + /// @notice End of previous staking schedule. + uint256 public lastStakingSchedule; - /// @notice Amount of shares left to be staked. - uint256 public remainingStakeAmount; + /// @notice Amount of shares left to be staked. + uint256 public remainingStakeAmount; - /// @notice Durations left. - uint256 public durationLeft; + /// @notice Durations left. + uint256 public durationLeft; - /// @notice Cliffs added. - uint256 public cliffAdded; + /// @notice Cliffs added. + uint256 public cliffAdded; - /// @notice Address of new token owner. - address public newTokenOwner; + /// @notice Address of new token owner. + address public newTokenOwner; - /// @notice Address of new implementation. - address public newImplementation; + /// @notice Address of new implementation. + address public newImplementation; - /// @dev Please add new state variables below this line. Mark them internal and - /// add a getter function while upgrading the contracts. + /// @dev Please add new state variables below this line. Mark them internal and + /// add a getter function while upgrading the contracts. } diff --git a/contracts/governance/Vesting/fouryear/IFourYearVesting.sol b/contracts/governance/Vesting/fouryear/IFourYearVesting.sol index a95aaf056..2d34fe61e 100644 --- a/contracts/governance/Vesting/fouryear/IFourYearVesting.sol +++ b/contracts/governance/Vesting/fouryear/IFourYearVesting.sol @@ -8,7 +8,9 @@ pragma solidity ^0.5.17; * at a vesting instance. */ interface IFourYearVesting { - function endDate() external returns (uint256); + function endDate() external returns (uint256); - function stakeTokens(uint256 _amount, uint256 _restartStakeSchedule) external returns (uint256 lastSchedule, uint256 remainingAmount); + function stakeTokens(uint256 _amount, uint256 _restartStakeSchedule) + external + returns (uint256 lastSchedule, uint256 remainingAmount); } diff --git a/contracts/governance/Vesting/fouryear/IFourYearVestingFactory.sol b/contracts/governance/Vesting/fouryear/IFourYearVestingFactory.sol index c4486230c..a645960c3 100644 --- a/contracts/governance/Vesting/fouryear/IFourYearVestingFactory.sol +++ b/contracts/governance/Vesting/fouryear/IFourYearVestingFactory.sol @@ -8,12 +8,12 @@ pragma solidity ^0.5.17; * and use an instance of FourYearVestingFactory. */ interface IFourYearVestingFactory { - function deployFourYearVesting( - address _SOV, - address _staking, - address _tokenOwner, - address _feeSharing, - address _vestingOwnerMultisig, - address _fourYearVestingLogic - ) external returns (address); + function deployFourYearVesting( + address _SOV, + address _staking, + address _tokenOwner, + address _feeSharing, + address _vestingOwnerMultisig, + address _fourYearVestingLogic + ) external returns (address); } diff --git a/contracts/interfaces/IChai.sol b/contracts/interfaces/IChai.sol index de7beb736..3d0ff354d 100644 --- a/contracts/interfaces/IChai.sol +++ b/contracts/interfaces/IChai.sol @@ -8,23 +8,23 @@ pragma solidity >=0.5.0 <0.6.0; import "./IERC20.sol"; interface IPot { - function dsr() external view returns (uint256); + function dsr() external view returns (uint256); - function chi() external view returns (uint256); + function chi() external view returns (uint256); - function rho() external view returns (uint256); + function rho() external view returns (uint256); } contract IChai is IERC20 { - function move( - address src, - address dst, - uint256 wad - ) external returns (bool); + function move( + address src, + address dst, + uint256 wad + ) external returns (bool); - function join(address dst, uint256 wad) external; + function join(address dst, uint256 wad) external; - function draw(address src, uint256 wad) external; + function draw(address src, uint256 wad) external; - function exit(address src, uint256 wad) external; + function exit(address src, uint256 wad) external; } diff --git a/contracts/interfaces/IConverterAMM.sol b/contracts/interfaces/IConverterAMM.sol index abed3e09f..35a63c158 100644 --- a/contracts/interfaces/IConverterAMM.sol +++ b/contracts/interfaces/IConverterAMM.sol @@ -1,5 +1,5 @@ pragma solidity >=0.5.0 <0.6.0; interface IConverterAMM { - function withdrawFees(address receiver) external returns (uint256); + function withdrawFees(address receiver) external returns (uint256); } diff --git a/contracts/interfaces/IERC20.sol b/contracts/interfaces/IERC20.sol index 268d59355..9329c36ec 100644 --- a/contracts/interfaces/IERC20.sol +++ b/contracts/interfaces/IERC20.sol @@ -6,26 +6,26 @@ pragma solidity >=0.5.0 <0.6.0; contract IERC20 { - string public name; - uint8 public decimals; - string public symbol; + string public name; + uint8 public decimals; + string public symbol; - function totalSupply() public view returns (uint256); + function totalSupply() public view returns (uint256); - function balanceOf(address _who) public view returns (uint256); + function balanceOf(address _who) public view returns (uint256); - function allowance(address _owner, address _spender) public view returns (uint256); + function allowance(address _owner, address _spender) public view returns (uint256); - function approve(address _spender, uint256 _value) public returns (bool); + function approve(address _spender, uint256 _value) public returns (bool); - function transfer(address _to, uint256 _value) public returns (bool); + function transfer(address _to, uint256 _value) public returns (bool); - function transferFrom( - address _from, - address _to, - uint256 _value - ) public returns (bool); + function transferFrom( + address _from, + address _to, + uint256 _value + ) public returns (bool); - event Transfer(address indexed from, address indexed to, uint256 value); - event Approval(address indexed owner, address indexed spender, uint256 value); + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); } diff --git a/contracts/interfaces/ILoanPool.sol b/contracts/interfaces/ILoanPool.sol index 72f88d68e..80fbb6e36 100644 --- a/contracts/interfaces/ILoanPool.sol +++ b/contracts/interfaces/ILoanPool.sol @@ -6,9 +6,9 @@ pragma solidity >=0.5.0 <0.6.0; interface ILoanPool { - function tokenPrice() external view returns (uint256 price); + function tokenPrice() external view returns (uint256 price); - function borrowInterestRate() external view returns (uint256); + function borrowInterestRate() external view returns (uint256); - function totalAssetSupply() external view returns (uint256); + function totalAssetSupply() external view returns (uint256); } diff --git a/contracts/interfaces/ILoanTokenLogicProxy.sol b/contracts/interfaces/ILoanTokenLogicProxy.sol index 24089ba4d..903aaed32 100644 --- a/contracts/interfaces/ILoanTokenLogicProxy.sol +++ b/contracts/interfaces/ILoanTokenLogicProxy.sol @@ -1,7 +1,7 @@ pragma solidity 0.5.17; interface ILoanTokenLogicProxy { - function beaconAddress() external view returns (address); + function beaconAddress() external view returns (address); - function setBeaconAddress(address _newBeaconAddress) external; + function setBeaconAddress(address _newBeaconAddress) external; } diff --git a/contracts/interfaces/ILoanTokenModules.sol b/contracts/interfaces/ILoanTokenModules.sol index fce8549ca..6859f23a4 100644 --- a/contracts/interfaces/ILoanTokenModules.sol +++ b/contracts/interfaces/ILoanTokenModules.sol @@ -2,262 +2,278 @@ pragma solidity 0.5.17; pragma experimental ABIEncoderV2; interface ILoanTokenModules { - /** EVENT */ - /// topic: 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef - event Transfer(address indexed from, address indexed to, uint256 value); - - /// topic: 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925 - event Approval(address indexed owner, address indexed spender, uint256 value); - - /// topic: 0x628e75c63c1873bcd3885f7aee9f58ee36f60dc789b2a6b3a978c4189bc548ba - event AllowanceUpdate(address indexed owner, address indexed spender, uint256 valueBefore, uint256 valueAfter); - - /// topic: 0xb4c03061fb5b7fed76389d5af8f2e0ddb09f8c70d1333abbb62582835e10accb - event Mint(address indexed minter, uint256 tokenAmount, uint256 assetAmount, uint256 price); - - /// topic: 0x743033787f4738ff4d6a7225ce2bd0977ee5f86b91a902a58f5e4d0b297b4644 - event Burn(address indexed burner, uint256 tokenAmount, uint256 assetAmount, uint256 price); - - /// topic: 0xc688ff9bd4a1c369dd44c5cf64efa9db6652fb6b280aa765cd43f17d256b816e - event FlashBorrow(address borrower, address target, address loanToken, uint256 loanAmount); - - /// topic: 0x9bbd2de400810774339120e2f8a2b517ed748595e944529bba8ebabf314d0591 - event SetTransactionLimits(address[] addresses, uint256[] limits); - - event WithdrawRBTCTo(address indexed to, uint256 amount); - - event ToggledFunctionPaused(string functionId, bool prevFlag, bool newFlag); - - /** INTERFACE */ - - /** START LOAN TOKEN SETTINGS LOWER ADMIN */ - struct LoanParams { - /// @dev ID of loan params object. - bytes32 id; - /// @dev If false, this object has been disabled by the owner and can't - /// be used for future loans. - bool active; - /// @dev Owner of this object. - address owner; - /// @dev The token being loaned. - address loanToken; - /// @dev The required collateral token. - address collateralToken; - /// @dev The minimum allowed initial margin. - uint256 minInitialMargin; - /// @dev An unhealthy loan when current margin is at or below this value. - uint256 maintenanceMargin; - /// @dev The maximum term for new loans (0 means there's no max term). - uint256 maxLoanTerm; - } - - function setAdmin(address _admin) external; - - function setPauser(address _pauser) external; - - function setupLoanParams(LoanParams[] calldata loanParamsList, bool areTorqueLoans) external; - - function disableLoanParams(address[] calldata collateralTokens, bool[] calldata isTorqueLoans) external; - - function setDemandCurve( - uint256 _baseRate, - uint256 _rateMultiplier, - uint256 _lowUtilBaseRate, - uint256 _lowUtilRateMultiplier, - uint256 _targetLevel, - uint256 _kinkLevel, - uint256 _maxScaleRate - ) external; - - function toggleFunctionPause( - string calldata funcId, /// example: "mint(uint256,uint256)" - bool isPaused - ) external; - - function setTransactionLimits(address[] calldata addresses, uint256[] calldata limits) external; - - function changeLoanTokenNameAndSymbol(string calldata _name, string calldata _symbol) external; - - /** END LOAN TOKEN SETTINGS LOWER ADMIN */ - - /** START LOAN TOKEN LOGIC STANDARD */ - function marginTrade( - bytes32 loanId, /// 0 if new loan - uint256 leverageAmount, /// Expected in x * 10**18 where x is the actual leverage (2, 3, 4, or 5). - uint256 loanTokenSent, - uint256 collateralTokenSent, - address collateralTokenAddress, - address trader, - uint256 minReturn, // minimum position size in the collateral tokens - bytes calldata loanDataBytes /// Arbitrary order data. - ) - external - payable - returns ( - uint256, - uint256 /// Returns new principal and new collateral added to trade. - ); - - function marginTradeAffiliate( - bytes32 loanId, // 0 if new loan - uint256 leverageAmount, // expected in x * 10**18 where x is the actual leverage (2, 3, 4, or 5) - uint256 loanTokenSent, - uint256 collateralTokenSent, - address collateralTokenAddress, - address trader, - uint256 minReturn, /// Minimum position size in the collateral tokens. - address affiliateReferrer, /// The user was brought by the affiliate (referrer). - bytes calldata loanDataBytes /// Arbitrary order data. - ) - external - payable - returns ( - uint256, - uint256 /// Returns new principal and new collateral added to trade. - ); - - function borrowInterestRate() external view returns (uint256); - - function mint(address receiver, uint256 depositAmount) external returns (uint256 mintAmount); - - function burn(address receiver, uint256 burnAmount) external returns (uint256 loanAmountPaid); - - function checkPause(string calldata funcId) external view returns (bool isPaused); - - function nextBorrowInterestRate(uint256 borrowAmount) external view returns (uint256); - - function totalAssetBorrow() external view returns (uint256); - - function totalAssetSupply() external view returns (uint256); - - function borrow( - bytes32 loanId, /// 0 if new loan. - uint256 withdrawAmount, - uint256 initialLoanDuration, /// Duration in seconds. - uint256 collateralTokenSent, /// If 0, loanId must be provided; any rBTC sent must equal this value. - address collateralTokenAddress, /// If address(0), this means rBTC and rBTC must be sent with the call or loanId must be provided. - address borrower, - address receiver, - bytes calldata /// loanDataBytes: arbitrary order data (for future use). - ) - external - payable - returns ( - uint256, - uint256 /// Returns new principal and new collateral added to loan. - ); - - function transfer(address _to, uint256 _value) external returns (bool); - - function transferFrom( - address _from, - address _to, - uint256 _value - ) external returns (bool); - - function setLiquidityMiningAddress(address LMAddress) external; - - function getLiquidityMiningAddress() external view returns (address); - - function getEstimatedMarginDetails( - uint256 leverageAmount, - uint256 loanTokenSent, - uint256 collateralTokenSent, - address collateralTokenAddress // address(0) means ETH - ) - external - view - returns ( - uint256 principal, - uint256 collateral, - uint256 interestRate - ); - - function getDepositAmountForBorrow( - uint256 borrowAmount, - uint256 initialLoanDuration, /// Duration in seconds. - address collateralTokenAddress /// address(0) means rBTC - ) external view returns (uint256 depositAmount); - - function getBorrowAmountForDeposit( - uint256 depositAmount, - uint256 initialLoanDuration, /// Duration in seconds. - address collateralTokenAddress /// address(0) means rBTC - ) external view returns (uint256 borrowAmount); - - function checkPriceDivergence( - uint256 leverageAmount, - uint256 loanTokenSent, - uint256 collateralTokenSent, - address collateralTokenAddress, - uint256 minReturn - ) external view; + /** EVENT */ + /// topic: 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef + event Transfer(address indexed from, address indexed to, uint256 value); + + /// topic: 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925 + event Approval(address indexed owner, address indexed spender, uint256 value); + + /// topic: 0x628e75c63c1873bcd3885f7aee9f58ee36f60dc789b2a6b3a978c4189bc548ba + event AllowanceUpdate( + address indexed owner, + address indexed spender, + uint256 valueBefore, + uint256 valueAfter + ); + + /// topic: 0xb4c03061fb5b7fed76389d5af8f2e0ddb09f8c70d1333abbb62582835e10accb + event Mint(address indexed minter, uint256 tokenAmount, uint256 assetAmount, uint256 price); + + /// topic: 0x743033787f4738ff4d6a7225ce2bd0977ee5f86b91a902a58f5e4d0b297b4644 + event Burn(address indexed burner, uint256 tokenAmount, uint256 assetAmount, uint256 price); + + /// topic: 0xc688ff9bd4a1c369dd44c5cf64efa9db6652fb6b280aa765cd43f17d256b816e + event FlashBorrow(address borrower, address target, address loanToken, uint256 loanAmount); + + /// topic: 0x9bbd2de400810774339120e2f8a2b517ed748595e944529bba8ebabf314d0591 + event SetTransactionLimits(address[] addresses, uint256[] limits); + + event WithdrawRBTCTo(address indexed to, uint256 amount); + + event ToggledFunctionPaused(string functionId, bool prevFlag, bool newFlag); + + /** INTERFACE */ + + /** START LOAN TOKEN SETTINGS LOWER ADMIN */ + struct LoanParams { + /// @dev ID of loan params object. + bytes32 id; + /// @dev If false, this object has been disabled by the owner and can't + /// be used for future loans. + bool active; + /// @dev Owner of this object. + address owner; + /// @dev The token being loaned. + address loanToken; + /// @dev The required collateral token. + address collateralToken; + /// @dev The minimum allowed initial margin. + uint256 minInitialMargin; + /// @dev An unhealthy loan when current margin is at or below this value. + uint256 maintenanceMargin; + /// @dev The maximum term for new loans (0 means there's no max term). + uint256 maxLoanTerm; + } + + function setAdmin(address _admin) external; + + function setPauser(address _pauser) external; + + function setupLoanParams(LoanParams[] calldata loanParamsList, bool areTorqueLoans) external; + + function disableLoanParams(address[] calldata collateralTokens, bool[] calldata isTorqueLoans) + external; + + function setDemandCurve( + uint256 _baseRate, + uint256 _rateMultiplier, + uint256 _lowUtilBaseRate, + uint256 _lowUtilRateMultiplier, + uint256 _targetLevel, + uint256 _kinkLevel, + uint256 _maxScaleRate + ) external; + + function toggleFunctionPause( + string calldata funcId, /// example: "mint(uint256,uint256)" + bool isPaused + ) external; + + function setTransactionLimits(address[] calldata addresses, uint256[] calldata limits) + external; + + function changeLoanTokenNameAndSymbol(string calldata _name, string calldata _symbol) external; + + /** END LOAN TOKEN SETTINGS LOWER ADMIN */ + + /** START LOAN TOKEN LOGIC STANDARD */ + function marginTrade( + bytes32 loanId, /// 0 if new loan + uint256 leverageAmount, /// Expected in x * 10**18 where x is the actual leverage (2, 3, 4, or 5). + uint256 loanTokenSent, + uint256 collateralTokenSent, + address collateralTokenAddress, + address trader, + uint256 minReturn, // minimum position size in the collateral tokens + bytes calldata loanDataBytes /// Arbitrary order data. + ) + external + payable + returns ( + uint256, + uint256 /// Returns new principal and new collateral added to trade. + ); + + function marginTradeAffiliate( + bytes32 loanId, // 0 if new loan + uint256 leverageAmount, // expected in x * 10**18 where x is the actual leverage (2, 3, 4, or 5) + uint256 loanTokenSent, + uint256 collateralTokenSent, + address collateralTokenAddress, + address trader, + uint256 minReturn, /// Minimum position size in the collateral tokens. + address affiliateReferrer, /// The user was brought by the affiliate (referrer). + bytes calldata loanDataBytes /// Arbitrary order data. + ) + external + payable + returns ( + uint256, + uint256 /// Returns new principal and new collateral added to trade. + ); + + function borrowInterestRate() external view returns (uint256); + + function mint(address receiver, uint256 depositAmount) external returns (uint256 mintAmount); + + function burn(address receiver, uint256 burnAmount) external returns (uint256 loanAmountPaid); + + function checkPause(string calldata funcId) external view returns (bool isPaused); + + function nextBorrowInterestRate(uint256 borrowAmount) external view returns (uint256); + + function totalAssetBorrow() external view returns (uint256); + + function totalAssetSupply() external view returns (uint256); + + function borrow( + bytes32 loanId, /// 0 if new loan. + uint256 withdrawAmount, + uint256 initialLoanDuration, /// Duration in seconds. + uint256 collateralTokenSent, /// If 0, loanId must be provided; any rBTC sent must equal this value. + address collateralTokenAddress, /// If address(0), this means rBTC and rBTC must be sent with the call or loanId must be provided. + address borrower, + address receiver, + bytes calldata /// loanDataBytes: arbitrary order data (for future use). + ) + external + payable + returns ( + uint256, + uint256 /// Returns new principal and new collateral added to loan. + ); + + function transfer(address _to, uint256 _value) external returns (bool); + + function transferFrom( + address _from, + address _to, + uint256 _value + ) external returns (bool); + + function setLiquidityMiningAddress(address LMAddress) external; + + function getLiquidityMiningAddress() external view returns (address); + + function getEstimatedMarginDetails( + uint256 leverageAmount, + uint256 loanTokenSent, + uint256 collateralTokenSent, + address collateralTokenAddress // address(0) means ETH + ) + external + view + returns ( + uint256 principal, + uint256 collateral, + uint256 interestRate + ); + + function getDepositAmountForBorrow( + uint256 borrowAmount, + uint256 initialLoanDuration, /// Duration in seconds. + address collateralTokenAddress /// address(0) means rBTC + ) external view returns (uint256 depositAmount); + + function getBorrowAmountForDeposit( + uint256 depositAmount, + uint256 initialLoanDuration, /// Duration in seconds. + address collateralTokenAddress /// address(0) means rBTC + ) external view returns (uint256 borrowAmount); + + function checkPriceDivergence( + uint256 leverageAmount, + uint256 loanTokenSent, + uint256 collateralTokenSent, + address collateralTokenAddress, + uint256 minReturn + ) external view; + + function getMaxEscrowAmount(uint256 leverageAmount) + external + view + returns (uint256 maxEscrowAmount); + + function checkpointPrice(address _user) external view returns (uint256 price); + + function assetBalanceOf(address _owner) external view returns (uint256); + + function profitOf(address user) external view returns (int256); - function getMaxEscrowAmount(uint256 leverageAmount) external view returns (uint256 maxEscrowAmount); + function tokenPrice() external view returns (uint256 price); - function checkpointPrice(address _user) external view returns (uint256 price); + function avgBorrowInterestRate() external view returns (uint256); - function assetBalanceOf(address _owner) external view returns (uint256); + function supplyInterestRate() external view returns (uint256); - function profitOf(address user) external view returns (int256); + function nextSupplyInterestRate(uint256 supplyAmount) external view returns (uint256); - function tokenPrice() external view returns (uint256 price); + function totalSupplyInterestRate(uint256 assetSupply) external view returns (uint256); - function avgBorrowInterestRate() external view returns (uint256); + function loanTokenAddress() external view returns (address); - function supplyInterestRate() external view returns (uint256); + function getMarginBorrowAmountAndRate(uint256 leverageAmount, uint256 depositAmount) + external + view + returns (uint256, uint256); - function nextSupplyInterestRate(uint256 supplyAmount) external view returns (uint256); + function withdrawRBTCTo(address payable _receiverAddress, uint256 _amount) external; - function totalSupplyInterestRate(uint256 assetSupply) external view returns (uint256); + /** START LOAN TOKEN BASE */ + function initialPrice() external view returns (uint256); - function loanTokenAddress() external view returns (address); + /** START LOAN TOKEN LOGIC LM */ + function mint( + address receiver, + uint256 depositAmount, + bool useLM + ) external returns (uint256 minted); + + function burn( + address receiver, + uint256 burnAmount, + bool useLM + ) external returns (uint256 redeemed); - function getMarginBorrowAmountAndRate(uint256 leverageAmount, uint256 depositAmount) external view returns (uint256, uint256); + /** START LOAN TOKEN LOGIC WRBTC */ + function mintWithBTC(address receiver, bool useLM) + external + payable + returns (uint256 mintAmount); - function withdrawRBTCTo(address payable _receiverAddress, uint256 _amount) external; + function burnToBTC( + address receiver, + uint256 burnAmount, + bool useLM + ) external returns (uint256 loanAmountPaid); - /** START LOAN TOKEN BASE */ - function initialPrice() external view returns (uint256); + /** START LOAN TOKEN LOGIC STORAGE */ + function pauser() external view returns (address); - /** START LOAN TOKEN LOGIC LM */ - function mint( - address receiver, - uint256 depositAmount, - bool useLM - ) external returns (uint256 minted); + function liquidityMiningAddress() external view returns (address); - function burn( - address receiver, - uint256 burnAmount, - bool useLM - ) external returns (uint256 redeemed); + function name() external view returns (string memory); - /** START LOAN TOKEN LOGIC WRBTC */ - function mintWithBTC(address receiver, bool useLM) external payable returns (uint256 mintAmount); + function symbol() external view returns (string memory); - function burnToBTC( - address receiver, - uint256 burnAmount, - bool useLM - ) external returns (uint256 loanAmountPaid); + /** START ADVANCED TOKEN */ + function approve(address _spender, uint256 _value) external returns (bool); - /** START LOAN TOKEN LOGIC STORAGE */ - function pauser() external view returns (address); + /** START ADVANCED TOKEN STORAGE */ + function allowance(address _owner, address _spender) external view returns (uint256); - function liquidityMiningAddress() external view returns (address); + function balanceOf(address _owner) external view returns (uint256); - function name() external view returns (string memory); - - function symbol() external view returns (string memory); - - /** START ADVANCED TOKEN */ - function approve(address _spender, uint256 _value) external returns (bool); - - /** START ADVANCED TOKEN STORAGE */ - function allowance(address _owner, address _spender) external view returns (uint256); - - function balanceOf(address _owner) external view returns (uint256); - - function totalSupply() external view returns (uint256); + function totalSupply() external view returns (uint256); } diff --git a/contracts/interfaces/ISovryn.sol b/contracts/interfaces/ISovryn.sol index 9b2439249..e51a9ada6 100644 --- a/contracts/interfaces/ISovryn.sol +++ b/contracts/interfaces/ISovryn.sol @@ -19,443 +19,469 @@ import "../events/SwapsEvents.sol"; import "../events/AffiliatesEvents.sol"; contract ISovryn is - State, - ProtocolSettingsEvents, - LoanSettingsEvents, - LoanOpeningsEvents, - LoanMaintenanceEvents, - LoanClosingsEvents, - SwapsEvents, - AffiliatesEvents, - FeesEvents + State, + ProtocolSettingsEvents, + LoanSettingsEvents, + LoanOpeningsEvents, + LoanMaintenanceEvents, + LoanClosingsEvents, + SwapsEvents, + AffiliatesEvents, + FeesEvents { - /// Triggered whenever interest is paid to lender. - event PayInterestTransfer(address indexed interestToken, address indexed lender, uint256 effectiveInterest); + /// Triggered whenever interest is paid to lender. + event PayInterestTransfer( + address indexed interestToken, + address indexed lender, + uint256 effectiveInterest + ); - ////// Protocol ////// + ////// Protocol ////// - function replaceContract(address target) external; + function replaceContract(address target) external; - function setTargets(string[] calldata sigsArr, address[] calldata targetsArr) external; + function setTargets(string[] calldata sigsArr, address[] calldata targetsArr) external; - function getTarget(string calldata sig) external view returns (address); + function getTarget(string calldata sig) external view returns (address); - ////// Protocol Settings ////// + ////// Protocol Settings ////// - function setSovrynProtocolAddress(address newProtocolAddress) external; + function setSovrynProtocolAddress(address newProtocolAddress) external; - function setSOVTokenAddress(address newSovTokenAddress) external; + function setSOVTokenAddress(address newSovTokenAddress) external; - function setLockedSOVAddress(address newSOVLockedAddress) external; + function setLockedSOVAddress(address newSOVLockedAddress) external; - function setMinReferralsToPayoutAffiliates(uint256 newMinReferrals) external; + function setMinReferralsToPayoutAffiliates(uint256 newMinReferrals) external; - function setPriceFeedContract(address newContract) external; + function setPriceFeedContract(address newContract) external; - function setSwapsImplContract(address newContract) external; + function setSwapsImplContract(address newContract) external; - function setLoanPool(address[] calldata pools, address[] calldata assets) external; + function setLoanPool(address[] calldata pools, address[] calldata assets) external; - function setSupportedTokens(address[] calldata addrs, bool[] calldata toggles) external; + function setSupportedTokens(address[] calldata addrs, bool[] calldata toggles) external; - function setLendingFeePercent(uint256 newValue) external; + function setLendingFeePercent(uint256 newValue) external; - function setTradingFeePercent(uint256 newValue) external; + function setTradingFeePercent(uint256 newValue) external; - function setBorrowingFeePercent(uint256 newValue) external; + function setBorrowingFeePercent(uint256 newValue) external; - function setSwapExternalFeePercent(uint256 newValue) external; + function setSwapExternalFeePercent(uint256 newValue) external; - function setAffiliateFeePercent(uint256 newValue) external; + function setAffiliateFeePercent(uint256 newValue) external; - function setAffiliateTradingTokenFeePercent(uint256 newValue) external; + function setAffiliateTradingTokenFeePercent(uint256 newValue) external; - function setLiquidationIncentivePercent(uint256 newAmount) external; + function setLiquidationIncentivePercent(uint256 newAmount) external; - function setMaxDisagreement(uint256 newAmount) external; + function setMaxDisagreement(uint256 newAmount) external; - function setSourceBuffer(uint256 newAmount) external; + function setSourceBuffer(uint256 newAmount) external; - function setMaxSwapSize(uint256 newAmount) external; + function setMaxSwapSize(uint256 newAmount) external; - function setFeesController(address newController) external; + function setFeesController(address newController) external; - function withdrawLendingFees( - address token, - address receiver, - uint256 amount - ) external returns (bool); + function withdrawLendingFees( + address token, + address receiver, + uint256 amount + ) external returns (bool); - function withdrawTradingFees( - address token, - address receiver, - uint256 amount - ) external returns (bool); + function withdrawTradingFees( + address token, + address receiver, + uint256 amount + ) external returns (bool); - function withdrawBorrowingFees( - address token, - address receiver, - uint256 amount - ) external returns (bool); + function withdrawBorrowingFees( + address token, + address receiver, + uint256 amount + ) external returns (bool); - function withdrawProtocolToken(address receiver, uint256 amount) external returns (address, bool); + function withdrawProtocolToken(address receiver, uint256 amount) + external + returns (address, bool); - function depositProtocolToken(uint256 amount) external; + function depositProtocolToken(uint256 amount) external; - function getLoanPoolsList(uint256 start, uint256 count) external; + function getLoanPoolsList(uint256 start, uint256 count) external; - function isLoanPool(address loanPool) external view returns (bool); + function isLoanPool(address loanPool) external view returns (bool); - function setWrbtcToken(address wrbtcTokenAddress) external; + function setWrbtcToken(address wrbtcTokenAddress) external; - function setSovrynSwapContractRegistryAddress(address registryAddress) external; + function setSovrynSwapContractRegistryAddress(address registryAddress) external; - function setProtocolTokenAddress(address _protocolTokenAddress) external; + function setProtocolTokenAddress(address _protocolTokenAddress) external; - function setRolloverBaseReward(uint256 transactionCost) external; + function setRolloverBaseReward(uint256 transactionCost) external; - function setRebatePercent(uint256 rebatePercent) external; + function setRebatePercent(uint256 rebatePercent) external; - function setSpecialRebates( - address sourceToken, - address destToken, - uint256 specialRebatesPercent - ) external; + function setSpecialRebates( + address sourceToken, + address destToken, + uint256 specialRebatesPercent + ) external; - function getSpecialRebates(address sourceToken, address destToken) external view returns (uint256 specialRebatesPercent); + function getSpecialRebates(address sourceToken, address destToken) + external + view + returns (uint256 specialRebatesPercent); - function togglePaused(bool paused) external; + function togglePaused(bool paused) external; - function isProtocolPaused() external view returns (bool); + function isProtocolPaused() external view returns (bool); - ////// Loan Settings ////// + ////// Loan Settings ////// - function setupLoanParams(LoanParams[] calldata loanParamsList) external returns (bytes32[] memory loanParamsIdList); + function setupLoanParams(LoanParams[] calldata loanParamsList) + external + returns (bytes32[] memory loanParamsIdList); - // Deactivates LoanParams for future loans. Active loans using it are unaffected. - function disableLoanParams(bytes32[] calldata loanParamsIdList) external; + // Deactivates LoanParams for future loans. Active loans using it are unaffected. + function disableLoanParams(bytes32[] calldata loanParamsIdList) external; - function getLoanParams(bytes32[] calldata loanParamsIdList) external view returns (LoanParams[] memory loanParamsList); + function getLoanParams(bytes32[] calldata loanParamsIdList) + external + view + returns (LoanParams[] memory loanParamsList); - function getLoanParamsList( - address owner, - uint256 start, - uint256 count - ) external view returns (bytes32[] memory loanParamsList); + function getLoanParamsList( + address owner, + uint256 start, + uint256 count + ) external view returns (bytes32[] memory loanParamsList); + + function getTotalPrincipal(address lender, address loanToken) external view returns (uint256); + + function minInitialMargin(bytes32 loanParamsId) external view returns (uint256); + + ////// Loan Openings ////// + + function borrowOrTradeFromPool( + bytes32 loanParamsId, + bytes32 loanId, // if 0, start a new loan + bool isTorqueLoan, + uint256 initialMargin, + address[4] calldata sentAddresses, + // lender: must match loan if loanId provided + // borrower: must match loan if loanId provided + // receiver: receiver of funds (address(0) assumes borrower address) + // manager: delegated manager of loan unless address(0) + uint256[5] calldata sentValues, + // newRate: new loan interest rate + // newPrincipal: new loan size (borrowAmount + any borrowed interest) + // torqueInterest: new amount of interest to escrow for Torque loan (determines initial loan length) + // loanTokenReceived: total loanToken deposit (amount not sent to borrower in the case of Torque loans) + // collateralTokenReceived: total collateralToken deposit + bytes calldata loanDataBytes + ) external payable returns (uint256); + + function setDelegatedManager( + bytes32 loanId, + address delegated, + bool toggle + ) external; + + function getEstimatedMarginExposure( + address loanToken, + address collateralToken, + uint256 loanTokenSent, + uint256 collateralTokenSent, + uint256 interestRate, + uint256 newPrincipal + ) external view returns (uint256); + + function getRequiredCollateral( + address loanToken, + address collateralToken, + uint256 newPrincipal, + uint256 marginAmount, + bool isTorqueLoan + ) external view returns (uint256 collateralAmountRequired); + + function getBorrowAmount( + address loanToken, + address collateralToken, + uint256 collateralTokenAmount, + uint256 marginAmount, + bool isTorqueLoan + ) external view returns (uint256 borrowAmount); + + ////// Loan Closings ////// + + function liquidate( + bytes32 loanId, + address receiver, + uint256 closeAmount // denominated in loanToken + ) + external + payable + returns ( + uint256 loanCloseAmount, + uint256 seizedAmount, + address seizedToken + ); + + function rollover(bytes32 loanId, bytes calldata loanDataBytes) external; + + function closeWithDeposit( + bytes32 loanId, + address receiver, + uint256 depositAmount // denominated in loanToken + ) + external + payable + returns ( + uint256 loanCloseAmount, + uint256 withdrawAmount, + address withdrawToken + ); + + function closeWithSwap( + bytes32 loanId, + address receiver, + uint256 swapAmount, // denominated in collateralToken + bool returnTokenIsCollateral, // true: withdraws collateralToken, false: withdraws loanToken + bytes calldata loanDataBytes + ) + external + returns ( + uint256 loanCloseAmount, + uint256 withdrawAmount, + address withdrawToken + ); + + ////// Loan Maintenance ////// + + function depositCollateral( + bytes32 loanId, + uint256 depositAmount // must match msg.value if ether is sent + ) external payable; + + function withdrawCollateral( + bytes32 loanId, + address receiver, + uint256 withdrawAmount + ) external returns (uint256 actualWithdrawAmount); + + function extendLoanByInterest( + bytes32 loanId, + address payer, + uint256 depositAmount, + bool useCollateral, + bytes calldata loanDataBytes + ) external payable returns (uint256 secondsExtended); + + function reduceLoanByInterest( + bytes32 loanId, + address receiver, + uint256 withdrawAmount + ) external returns (uint256 secondsReduced); + + function withdrawAccruedInterest(address loanToken) external; + + function getLenderInterestData(address lender, address loanToken) + external + view + returns ( + uint256 interestPaid, + uint256 interestPaidDate, + uint256 interestOwedPerDay, + uint256 interestUnPaid, + uint256 interestFeePercent, + uint256 principalTotal + ); + + function getLoanInterestData(bytes32 loanId) + external + view + returns ( + address loanToken, + uint256 interestOwedPerDay, + uint256 interestDepositTotal, + uint256 interestDepositRemaining + ); + + struct LoanReturnData { + bytes32 loanId; + address loanToken; + address collateralToken; + uint256 principal; + uint256 collateral; + uint256 interestOwedPerDay; + uint256 interestDepositRemaining; + uint256 startRate; // collateralToLoanRate + uint256 startMargin; + uint256 maintenanceMargin; + uint256 currentMargin; + uint256 maxLoanTerm; + uint256 endTimestamp; + uint256 maxLiquidatable; + uint256 maxSeizable; + } + + struct LoanReturnDataV2 { + bytes32 loanId; + address loanToken; + address collateralToken; + address borrower; + uint256 principal; + uint256 collateral; + uint256 interestOwedPerDay; + uint256 interestDepositRemaining; + uint256 startRate; /// collateralToLoanRate + uint256 startMargin; + uint256 maintenanceMargin; + uint256 currentMargin; + uint256 maxLoanTerm; + uint256 endTimestamp; + uint256 maxLiquidatable; + uint256 maxSeizable; + uint256 creationTimestamp; + } + + function getUserLoans( + address user, + uint256 start, + uint256 count, + uint256 loanType, + bool isLender, + bool unsafeOnly + ) external view returns (LoanReturnData[] memory loansData); + + function getUserLoansV2( + address user, + uint256 start, + uint256 count, + uint256 loanType, + bool isLender, + bool unsafeOnly + ) external view returns (LoanReturnDataV2[] memory loansDataV2); + + function getLoan(bytes32 loanId) external view returns (LoanReturnData memory loanData); + + function getLoanV2(bytes32 loanId) external view returns (LoanReturnDataV2 memory loanDataV2); + + function getActiveLoans( + uint256 start, + uint256 count, + bool unsafeOnly + ) external view returns (LoanReturnData[] memory loansData); + + function getActiveLoansV2( + uint256 start, + uint256 count, + bool unsafeOnly + ) external view returns (LoanReturnDataV2[] memory loansDataV2); + + ////// Protocol Migration ////// + + function setLegacyOracles(address[] calldata refs, address[] calldata oracles) external; + + function getLegacyOracle(address ref) external view returns (address); + + ////// Swaps External ////// + function swapExternal( + address sourceToken, + address destToken, + address receiver, + address returnToSender, + uint256 sourceTokenAmount, + uint256 requiredDestTokenAmount, + uint256 minReturn, + bytes calldata swapData + ) external returns (uint256 destTokenAmountReceived, uint256 sourceTokenAmountUsed); + + function getSwapExpectedReturn( + address sourceToken, + address destToken, + uint256 sourceTokenAmount + ) external view returns (uint256); + + function checkPriceDivergence( + address sourceToken, + address destToken, + uint256 sourceTokenAmount, + uint256 minReturn + ) public view; + + ////// Affiliates Module ////// + + function getUserNotFirstTradeFlag(address user) external view returns (bool); + + function setUserNotFirstTradeFlag(address user) external view returns (bool); + + function payTradingFeeToAffiliatesReferrer( + address referrer, + address trader, + address token, + uint256 tradingFeeTokenBaseAmount + ) external returns (uint256 affiliatesBonusSOVAmount, uint256 affiliatesBonusTokenAmount); + + function setAffiliatesReferrer(address user, address referrer) external; //onlyCallableByLoanPools + + function getReferralsList(address referrer) external view returns (address[] memory refList); + + function getAffiliatesReferrerBalances(address referrer) + external + view + returns (address[] memory referrerTokensList, uint256[] memory referrerTokensBalances); + + function getAffiliatesReferrerTokensList(address referrer) + external + view + returns (address[] memory tokensList); - function getTotalPrincipal(address lender, address loanToken) external view returns (uint256); + function getAffiliatesReferrerTokenBalance(address referrer, address token) + external + view + returns (uint256); - function minInitialMargin(bytes32 loanParamsId) external view returns (uint256); + function withdrawAffiliatesReferrerTokenFees( + address token, + address receiver, + uint256 amount + ) external returns (uint256 withdrawAmount); - ////// Loan Openings ////// + function withdrawAllAffiliatesReferrerTokenFees(address receiver) external; - function borrowOrTradeFromPool( - bytes32 loanParamsId, - bytes32 loanId, // if 0, start a new loan - bool isTorqueLoan, - uint256 initialMargin, - address[4] calldata sentAddresses, - // lender: must match loan if loanId provided - // borrower: must match loan if loanId provided - // receiver: receiver of funds (address(0) assumes borrower address) - // manager: delegated manager of loan unless address(0) - uint256[5] calldata sentValues, - // newRate: new loan interest rate - // newPrincipal: new loan size (borrowAmount + any borrowed interest) - // torqueInterest: new amount of interest to escrow for Torque loan (determines initial loan length) - // loanTokenReceived: total loanToken deposit (amount not sent to borrower in the case of Torque loans) - // collateralTokenReceived: total collateralToken deposit - bytes calldata loanDataBytes - ) external payable returns (uint256); - - function setDelegatedManager( - bytes32 loanId, - address delegated, - bool toggle - ) external; - - function getEstimatedMarginExposure( - address loanToken, - address collateralToken, - uint256 loanTokenSent, - uint256 collateralTokenSent, - uint256 interestRate, - uint256 newPrincipal - ) external view returns (uint256); - - function getRequiredCollateral( - address loanToken, - address collateralToken, - uint256 newPrincipal, - uint256 marginAmount, - bool isTorqueLoan - ) external view returns (uint256 collateralAmountRequired); - - function getBorrowAmount( - address loanToken, - address collateralToken, - uint256 collateralTokenAmount, - uint256 marginAmount, - bool isTorqueLoan - ) external view returns (uint256 borrowAmount); - - ////// Loan Closings ////// - - function liquidate( - bytes32 loanId, - address receiver, - uint256 closeAmount // denominated in loanToken - ) - external - payable - returns ( - uint256 loanCloseAmount, - uint256 seizedAmount, - address seizedToken - ); - - function rollover(bytes32 loanId, bytes calldata loanDataBytes) external; - - function closeWithDeposit( - bytes32 loanId, - address receiver, - uint256 depositAmount // denominated in loanToken - ) - external - payable - returns ( - uint256 loanCloseAmount, - uint256 withdrawAmount, - address withdrawToken - ); - - function closeWithSwap( - bytes32 loanId, - address receiver, - uint256 swapAmount, // denominated in collateralToken - bool returnTokenIsCollateral, // true: withdraws collateralToken, false: withdraws loanToken - bytes calldata loanDataBytes - ) - external - returns ( - uint256 loanCloseAmount, - uint256 withdrawAmount, - address withdrawToken - ); - - ////// Loan Maintenance ////// - - function depositCollateral( - bytes32 loanId, - uint256 depositAmount // must match msg.value if ether is sent - ) external payable; - - function withdrawCollateral( - bytes32 loanId, - address receiver, - uint256 withdrawAmount - ) external returns (uint256 actualWithdrawAmount); - - function extendLoanByInterest( - bytes32 loanId, - address payer, - uint256 depositAmount, - bool useCollateral, - bytes calldata loanDataBytes - ) external payable returns (uint256 secondsExtended); - - function reduceLoanByInterest( - bytes32 loanId, - address receiver, - uint256 withdrawAmount - ) external returns (uint256 secondsReduced); - - function withdrawAccruedInterest(address loanToken) external; - - function getLenderInterestData(address lender, address loanToken) - external - view - returns ( - uint256 interestPaid, - uint256 interestPaidDate, - uint256 interestOwedPerDay, - uint256 interestUnPaid, - uint256 interestFeePercent, - uint256 principalTotal - ); - - function getLoanInterestData(bytes32 loanId) - external - view - returns ( - address loanToken, - uint256 interestOwedPerDay, - uint256 interestDepositTotal, - uint256 interestDepositRemaining - ); - - struct LoanReturnData { - bytes32 loanId; - address loanToken; - address collateralToken; - uint256 principal; - uint256 collateral; - uint256 interestOwedPerDay; - uint256 interestDepositRemaining; - uint256 startRate; // collateralToLoanRate - uint256 startMargin; - uint256 maintenanceMargin; - uint256 currentMargin; - uint256 maxLoanTerm; - uint256 endTimestamp; - uint256 maxLiquidatable; - uint256 maxSeizable; - } - - struct LoanReturnDataV2 { - bytes32 loanId; - address loanToken; - address collateralToken; - address borrower; - uint256 principal; - uint256 collateral; - uint256 interestOwedPerDay; - uint256 interestDepositRemaining; - uint256 startRate; /// collateralToLoanRate - uint256 startMargin; - uint256 maintenanceMargin; - uint256 currentMargin; - uint256 maxLoanTerm; - uint256 endTimestamp; - uint256 maxLiquidatable; - uint256 maxSeizable; - uint256 creationTimestamp; - } - - function getUserLoans( - address user, - uint256 start, - uint256 count, - uint256 loanType, - bool isLender, - bool unsafeOnly - ) external view returns (LoanReturnData[] memory loansData); - - function getUserLoansV2( - address user, - uint256 start, - uint256 count, - uint256 loanType, - bool isLender, - bool unsafeOnly - ) external view returns (LoanReturnDataV2[] memory loansDataV2); - - function getLoan(bytes32 loanId) external view returns (LoanReturnData memory loanData); - - function getLoanV2(bytes32 loanId) external view returns (LoanReturnDataV2 memory loanDataV2); - - function getActiveLoans( - uint256 start, - uint256 count, - bool unsafeOnly - ) external view returns (LoanReturnData[] memory loansData); - - function getActiveLoansV2( - uint256 start, - uint256 count, - bool unsafeOnly - ) external view returns (LoanReturnDataV2[] memory loansDataV2); - - ////// Protocol Migration ////// - - function setLegacyOracles(address[] calldata refs, address[] calldata oracles) external; - - function getLegacyOracle(address ref) external view returns (address); - - ////// Swaps External ////// - function swapExternal( - address sourceToken, - address destToken, - address receiver, - address returnToSender, - uint256 sourceTokenAmount, - uint256 requiredDestTokenAmount, - uint256 minReturn, - bytes calldata swapData - ) external returns (uint256 destTokenAmountReceived, uint256 sourceTokenAmountUsed); - - function getSwapExpectedReturn( - address sourceToken, - address destToken, - uint256 sourceTokenAmount - ) external view returns (uint256); - - function checkPriceDivergence( - address sourceToken, - address destToken, - uint256 sourceTokenAmount, - uint256 minReturn - ) public view; - - ////// Affiliates Module ////// - - function getUserNotFirstTradeFlag(address user) external view returns (bool); - - function setUserNotFirstTradeFlag(address user) external view returns (bool); - - function payTradingFeeToAffiliatesReferrer( - address referrer, - address trader, - address token, - uint256 tradingFeeTokenBaseAmount - ) external returns (uint256 affiliatesBonusSOVAmount, uint256 affiliatesBonusTokenAmount); + function getProtocolAddress() external view returns (address); - function setAffiliatesReferrer(address user, address referrer) external; //onlyCallableByLoanPools + function getSovTokenAddress() external view returns (address); - function getReferralsList(address referrer) external view returns (address[] memory refList); + function getLockedSOVAddress() external view returns (address); - function getAffiliatesReferrerBalances(address referrer) - external - view - returns (address[] memory referrerTokensList, uint256[] memory referrerTokensBalances); + function getFeeRebatePercent() external view returns (uint256); - function getAffiliatesReferrerTokensList(address referrer) external view returns (address[] memory tokensList); + function getMinReferralsToPayout() external view returns (uint256); - function getAffiliatesReferrerTokenBalance(address referrer, address token) external view returns (uint256); + function getAffiliatesUserReferrer(address user) external view returns (address referrer); - function withdrawAffiliatesReferrerTokenFees( - address token, - address receiver, - uint256 amount - ) external returns (uint256 withdrawAmount); + function getAffiliateRewardsHeld(address referrer) external view returns (uint256); - function withdrawAllAffiliatesReferrerTokenFees(address receiver) external; + function getAffiliateTradingTokenFeePercent() + external + view + returns (uint256 affiliateTradingTokenFeePercent); - function getProtocolAddress() external view returns (address); + function getAffiliatesTokenRewardsValueInRbtc(address referrer) + external + view + returns (uint256 rbtcTotalAmount); - function getSovTokenAddress() external view returns (address); + function getSwapExternalFeePercent() external view returns (uint256 swapExternalFeePercent); - function getLockedSOVAddress() external view returns (address); + function setTradingRebateRewardsBasisPoint(uint256 newBasisPoint) external; - function getFeeRebatePercent() external view returns (uint256); + function getTradingRebateRewardsBasisPoint() external view returns (uint256); - function getMinReferralsToPayout() external view returns (uint256); + function getDedicatedSOVRebate() external view returns (uint256); - function getAffiliatesUserReferrer(address user) external view returns (address referrer); - - function getAffiliateRewardsHeld(address referrer) external view returns (uint256); - - function getAffiliateTradingTokenFeePercent() external view returns (uint256 affiliateTradingTokenFeePercent); - - function getAffiliatesTokenRewardsValueInRbtc(address referrer) external view returns (uint256 rbtcTotalAmount); - - function getSwapExternalFeePercent() external view returns (uint256 swapExternalFeePercent); - - function setTradingRebateRewardsBasisPoint(uint256 newBasisPoint) external; - - function getTradingRebateRewardsBasisPoint() external view returns (uint256); - - function getDedicatedSOVRebate() external view returns (uint256); - - function setRolloverFlexFeePercent(uint256 newRolloverFlexFeePercent) external; + function setRolloverFlexFeePercent(uint256 newRolloverFlexFeePercent) external; } diff --git a/contracts/interfaces/IWrbtc.sol b/contracts/interfaces/IWrbtc.sol index 72e9e1169..ca62933a8 100644 --- a/contracts/interfaces/IWrbtc.sol +++ b/contracts/interfaces/IWrbtc.sol @@ -6,7 +6,7 @@ pragma solidity >=0.5.0 <0.6.0; interface IWrbtc { - function deposit() external payable; + function deposit() external payable; - function withdraw(uint256 wad) external; + function withdraw(uint256 wad) external; } diff --git a/contracts/locked/ILockedSOV.sol b/contracts/locked/ILockedSOV.sol index b0cd1bd43..a64c4c18b 100644 --- a/contracts/locked/ILockedSOV.sol +++ b/contracts/locked/ILockedSOV.sol @@ -7,28 +7,28 @@ pragma solidity ^0.5.17; * @dev Only use it if you know what you are doing. */ interface ILockedSOV { - /** - * @notice Adds SOV to the user balance (Locked and Unlocked Balance based on `_basisPoint`). - * @param _userAddress The user whose locked balance has to be updated with `_sovAmount`. - * @param _sovAmount The amount of SOV to be added to the locked and/or unlocked balance. - * @param _basisPoint The % (in Basis Point)which determines how much will be unlocked immediately. - */ - function deposit( - address _userAddress, - uint256 _sovAmount, - uint256 _basisPoint - ) external; + /** + * @notice Adds SOV to the user balance (Locked and Unlocked Balance based on `_basisPoint`). + * @param _userAddress The user whose locked balance has to be updated with `_sovAmount`. + * @param _sovAmount The amount of SOV to be added to the locked and/or unlocked balance. + * @param _basisPoint The % (in Basis Point)which determines how much will be unlocked immediately. + */ + function deposit( + address _userAddress, + uint256 _sovAmount, + uint256 _basisPoint + ) external; - /** - * @notice Adds SOV to the locked balance of a user. - * @param _userAddress The user whose locked balance has to be updated with _sovAmount. - * @param _sovAmount The amount of SOV to be added to the locked balance. - */ - function depositSOV(address _userAddress, uint256 _sovAmount) external; + /** + * @notice Adds SOV to the locked balance of a user. + * @param _userAddress The user whose locked balance has to be updated with _sovAmount. + * @param _sovAmount The amount of SOV to be added to the locked balance. + */ + function depositSOV(address _userAddress, uint256 _sovAmount) external; - /** - * @notice Withdraws unlocked tokens and Stakes Locked tokens for a user who already have a vesting created. - * @param _userAddress The address of user tokens will be withdrawn. - */ - function withdrawAndStakeTokensFrom(address _userAddress) external; + /** + * @notice Withdraws unlocked tokens and Stakes Locked tokens for a user who already have a vesting created. + * @param _userAddress The address of user tokens will be withdrawn. + */ + function withdrawAndStakeTokensFrom(address _userAddress) external; } diff --git a/contracts/locked/LockedSOV.sol b/contracts/locked/LockedSOV.sol index 444b6d981..0e815b0c4 100644 --- a/contracts/locked/LockedSOV.sol +++ b/contracts/locked/LockedSOV.sol @@ -12,396 +12,418 @@ import "./ILockedSOV.sol"; * @notice This contract is used to receive reward from other contracts, Create Vesting and Stake Tokens. */ contract LockedSOV is ILockedSOV { - using SafeMath for uint256; - - uint256 public constant MAX_BASIS_POINT = 10000; - uint256 public constant MAX_DURATION = 37; - - /* Storage */ - - /// @notice True if the migration to a new Locked SOV Contract has started. - bool public migration; - - /// @notice The cliff is the time period after which the tokens begin to unlock. - uint256 public cliff; - /// @notice The duration is the time period after all tokens will have been unlocked. - uint256 public duration; - - /// @notice The SOV token contract. - IERC20 public SOV; - /// @notice The Vesting registry contract. - VestingRegistry public vestingRegistry; - /// @notice The New (Future) Locked SOV. - ILockedSOV public newLockedSOV; - - /// @notice The locked user balances. - mapping(address => uint256) private lockedBalances; - /// @notice The unlocked user balances. - mapping(address => uint256) private unlockedBalances; - /// @notice The contracts/wallets with admin power. - mapping(address => bool) private isAdmin; - - /* Events */ - - /// @notice Emitted when a new Admin is added to the admin list. - /// @param _initiator The address which initiated this event to be emitted. - /// @param _newAdmin The address of the new admin. - event AdminAdded(address indexed _initiator, address indexed _newAdmin); - - /// @notice Emitted when an admin is removed from the admin list. - /// @param _initiator The address which initiated this event to be emitted. - /// @param _removedAdmin The address of the removed admin. - event AdminRemoved(address indexed _initiator, address indexed _removedAdmin); - - /// @notice Emitted when Vesting Registry, Duration and/or Cliff is updated. - /// @param _initiator The address which initiated this event to be emitted. - /// @param _vestingRegistry The Vesting Registry Contract. - /// @param _cliff The time period after which the tokens begin to unlock. - /// @param _duration The time period after all tokens will have been unlocked. - event RegistryCliffAndDurationUpdated(address indexed _initiator, address indexed _vestingRegistry, uint256 _cliff, uint256 _duration); - - /// @notice Emitted when a new deposit is made. - /// @param _initiator The address which initiated this event to be emitted. - /// @param _userAddress The user to whose un/locked balance a new deposit was made. - /// @param _sovAmount The amount of SOV to be added to the un/locked balance. - /// @param _basisPoint The % (in Basis Point) which determines how much will be unlocked immediately. - event Deposited(address indexed _initiator, address indexed _userAddress, uint256 _sovAmount, uint256 _basisPoint); - - /// @notice Emitted when a user withdraws the fund. - /// @param _initiator The address which initiated this event to be emitted. - /// @param _userAddress The user whose unlocked balance has to be withdrawn. - /// @param _sovAmount The amount of SOV withdrawn from the unlocked balance. - event Withdrawn(address indexed _initiator, address indexed _userAddress, uint256 _sovAmount); - - /// @notice Emitted when a user creates a vesting for himself. - /// @param _initiator The address which initiated this event to be emitted. - /// @param _userAddress The user whose unlocked balance has to be withdrawn. - /// @param _vesting The Vesting Contract. - event VestingCreated(address indexed _initiator, address indexed _userAddress, address indexed _vesting); - - /// @notice Emitted when a user stakes tokens. - /// @param _initiator The address which initiated this event to be emitted. - /// @param _vesting The Vesting Contract. - /// @param _amount The amount of locked tokens staked by the user. - event TokenStaked(address indexed _initiator, address indexed _vesting, uint256 _amount); - - /// @notice Emitted when an admin initiates a migration to new Locked SOV Contract. - /// @param _initiator The address which initiated this event to be emitted. - /// @param _newLockedSOV The address of the new Locked SOV Contract. - event MigrationStarted(address indexed _initiator, address indexed _newLockedSOV); - - /// @notice Emitted when a user initiates the transfer to a new Locked SOV Contract. - /// @param _initiator The address which initiated this event to be emitted. - /// @param _amount The amount of locked tokens to transfer from this contract to the new one. - event UserTransfered(address indexed _initiator, uint256 _amount); - - /* Modifiers */ - - modifier onlyAdmin { - require(isAdmin[msg.sender], "Only admin can call this."); - _; - } - - modifier migrationAllowed { - require(migration, "Migration has not yet started."); - _; - } - - /* Constructor */ - - /** - * @notice Setup the required parameters. - * @param _SOV The SOV Token Address. - * @param _vestingRegistry The Vesting Registry Address. - * @param _cliff The time period after which the tokens begin to unlock. - * @param _duration The time period after all tokens will have been unlocked. - * @param _admins The list of Admins to be added. - */ - constructor( - address _SOV, - address _vestingRegistry, - uint256 _cliff, - uint256 _duration, - address[] memory _admins - ) public { - require(_SOV != address(0), "Invalid SOV Address."); - require(_vestingRegistry != address(0), "Vesting registry address is invalid."); - require(_duration < MAX_DURATION, "Duration is too long."); - - SOV = IERC20(_SOV); - vestingRegistry = VestingRegistry(_vestingRegistry); - cliff = _cliff * 4 weeks; - duration = _duration * 4 weeks; - - for (uint256 index = 0; index < _admins.length; index++) { - isAdmin[_admins[index]] = true; - } - } - - /* Public or External Functions */ - - /** - * @notice The function to add a new admin. - * @param _newAdmin The address of the new admin. - * @dev Only callable by an Admin. - */ - function addAdmin(address _newAdmin) public onlyAdmin { - require(_newAdmin != address(0), "Invalid Address."); - require(!isAdmin[_newAdmin], "Address is already admin."); - isAdmin[_newAdmin] = true; - - emit AdminAdded(msg.sender, _newAdmin); - } - - /** - * @notice The function to remove an admin. - * @param _adminToRemove The address of the admin which should be removed. - * @dev Only callable by an Admin. - */ - function removeAdmin(address _adminToRemove) public onlyAdmin { - require(isAdmin[_adminToRemove], "Address is not an admin."); - isAdmin[_adminToRemove] = false; - - emit AdminRemoved(msg.sender, _adminToRemove); - } - - /** - * @notice The function to update the Vesting Registry, Duration and Cliff. - * @param _vestingRegistry The Vesting Registry Address. - * @param _cliff The time period after which the tokens begin to unlock. - * @param _duration The time period after all tokens will have been unlocked. - * @dev IMPORTANT 1: You have to change Vesting Registry if you want to change Duration and/or Cliff. - * IMPORTANT 2: `_cliff` and `_duration` is multiplied by 4 weeks in this function. - */ - function changeRegistryCliffAndDuration( - address _vestingRegistry, - uint256 _cliff, - uint256 _duration - ) external onlyAdmin { - require(address(vestingRegistry) != _vestingRegistry, "Vesting Registry has to be different for changing duration and cliff."); - /// If duration is also zero, then it is similar to Unlocked SOV. - require(_duration != 0, "Duration cannot be zero."); - require(_duration < MAX_DURATION, "Duration is too long."); - - vestingRegistry = VestingRegistry(_vestingRegistry); - - cliff = _cliff * 4 weeks; - duration = _duration * 4 weeks; - - emit RegistryCliffAndDurationUpdated(msg.sender, _vestingRegistry, _cliff, _duration); - } - - /** - * @notice Adds SOV to the user balance (Locked and Unlocked Balance based on `_basisPoint`). - * @param _userAddress The user whose locked balance has to be updated with `_sovAmount`. - * @param _sovAmount The amount of SOV to be added to the locked and/or unlocked balance. - * @param _basisPoint The % (in Basis Point)which determines how much will be unlocked immediately. - */ - function deposit( - address _userAddress, - uint256 _sovAmount, - uint256 _basisPoint - ) external { - _deposit(_userAddress, _sovAmount, _basisPoint); - } - - /** - * @notice Adds SOV to the locked balance of a user. - * @param _userAddress The user whose locked balance has to be updated with _sovAmount. - * @param _sovAmount The amount of SOV to be added to the locked balance. - * @dev This is here because there are dependency with other contracts. - */ - function depositSOV(address _userAddress, uint256 _sovAmount) external { - _deposit(_userAddress, _sovAmount, 0); - } - - function _deposit( - address _userAddress, - uint256 _sovAmount, - uint256 _basisPoint - ) private { - // MAX_BASIS_POINT is not included because if 100% is unlocked, then LockedSOV is not required to be used. - require(_basisPoint < MAX_BASIS_POINT, "Basis Point has to be less than 10000."); - bool txStatus = SOV.transferFrom(msg.sender, address(this), _sovAmount); - require(txStatus, "Token transfer was not successful. Check receiver address."); - - uint256 unlockedBal = _sovAmount.mul(_basisPoint).div(MAX_BASIS_POINT); - - unlockedBalances[_userAddress] = unlockedBalances[_userAddress].add(unlockedBal); - lockedBalances[_userAddress] = lockedBalances[_userAddress].add(_sovAmount).sub(unlockedBal); - - emit Deposited(msg.sender, _userAddress, _sovAmount, _basisPoint); - } - - /** - * @notice A function to withdraw the unlocked balance. - * @param _receiverAddress If specified, the unlocked balance will go to this address, else to msg.sender. - */ - function withdraw(address _receiverAddress) public { - _withdraw(msg.sender, _receiverAddress); - } - - function _withdraw(address _sender, address _receiverAddress) private { - address userAddr = _receiverAddress; - if (_receiverAddress == address(0)) { - userAddr = _sender; - } - - uint256 amount = unlockedBalances[_sender]; - unlockedBalances[_sender] = 0; - - bool txStatus = SOV.transfer(userAddr, amount); - require(txStatus, "Token transfer was not successful. Check receiver address."); - - emit Withdrawn(_sender, userAddr, amount); - } - - /** - * @notice Creates vesting if not already created and Stakes tokens for a user. - * @dev Only use this function if the `duration` is small. - */ - function createVestingAndStake() public { - _createVestingAndStake(msg.sender); - } - - function _createVestingAndStake(address _sender) private { - address vestingAddr = _getVesting(_sender); - - if (vestingAddr == address(0)) { - vestingAddr = _createVesting(_sender); - } - - _stakeTokens(_sender, vestingAddr); - } - - /** - * @notice Creates vesting contract (if it hasn't been created yet) for the calling user. - * @return _vestingAddress The New Vesting Contract Created. - */ - function createVesting() public returns (address _vestingAddress) { - _vestingAddress = _createVesting(msg.sender); - } - - /** - * @notice Stakes tokens for a user who already have a vesting created. - * @dev The user should already have a vesting created, else this function will throw error. - */ - function stakeTokens() public { - VestingLogic vesting = VestingLogic(_getVesting(msg.sender)); - - require(cliff == vesting.cliff() && duration == vesting.duration(), "Wrong Vesting Schedule."); - - _stakeTokens(msg.sender, address(vesting)); - } - - /** - * @notice Withdraws unlocked tokens and Stakes Locked tokens for a user who already have a vesting created. - * @param _receiverAddress If specified, the unlocked balance will go to this address, else to msg.sender. - */ - function withdrawAndStakeTokens(address _receiverAddress) external { - _withdraw(msg.sender, _receiverAddress); - _createVestingAndStake(msg.sender); - } - - /** - * @notice Withdraws unlocked tokens and Stakes Locked tokens for a user who already have a vesting created. - * @param _userAddress The address of user tokens will be withdrawn. - */ - function withdrawAndStakeTokensFrom(address _userAddress) external { - _withdraw(_userAddress, _userAddress); - _createVestingAndStake(_userAddress); - } - - /** - * @notice Function to start the process of migration to new contract. - * @param _newLockedSOV The new locked sov contract address. - */ - function startMigration(address _newLockedSOV) external onlyAdmin { - require(_newLockedSOV != address(0), "New Locked SOV Address is Invalid."); - newLockedSOV = ILockedSOV(_newLockedSOV); - SOV.approve(_newLockedSOV, SOV.balanceOf(address(this))); - migration = true; - - emit MigrationStarted(msg.sender, _newLockedSOV); - } - - /** - * @notice Function to transfer the locked balance from this contract to new LockedSOV Contract. - * @dev Address is not specified to discourage selling lockedSOV to other address. - */ - function transfer() external migrationAllowed { - uint256 amount = lockedBalances[msg.sender]; - lockedBalances[msg.sender] = 0; - - newLockedSOV.depositSOV(msg.sender, amount); - - emit UserTransfered(msg.sender, amount); - } - - /* Internal Functions */ - - /** - * @notice Creates a Vesting Contract for a user. - * @param _tokenOwner The owner of the vesting contract. - * @return _vestingAddress The Vesting Contract Address. - * @dev Does not do anything if Vesting Contract was already created. - */ - function _createVesting(address _tokenOwner) internal returns (address _vestingAddress) { - /// Here zero is given in place of amount, as amount is not really used in `vestingRegistry.createVesting()`. - vestingRegistry.createVesting(_tokenOwner, 0, cliff, duration); - _vestingAddress = _getVesting(_tokenOwner); - emit VestingCreated(msg.sender, _tokenOwner, _vestingAddress); - } - - /** - * @notice Returns the Vesting Contract Address. - * @param _tokenOwner The owner of the vesting contract. - * @return _vestingAddress The Vesting Contract Address. - */ - function _getVesting(address _tokenOwner) internal view returns (address _vestingAddress) { - return vestingRegistry.getVesting(_tokenOwner); - } - - /** - * @notice Stakes the tokens in a particular vesting contract. - * @param _vesting The Vesting Contract Address. - */ - function _stakeTokens(address _sender, address _vesting) internal { - uint256 amount = lockedBalances[_sender]; - lockedBalances[_sender] = 0; - - require(SOV.approve(_vesting, amount), "Approve failed."); - VestingLogic(_vesting).stakeTokens(amount); - - emit TokenStaked(_sender, _vesting, amount); - } - - /* Getter or Read Functions */ - - /** - * @notice The function to get the locked balance of a user. - * @param _addr The address of the user to check the locked balance. - * @return _balance The locked balance of the address `_addr`. - */ - function getLockedBalance(address _addr) external view returns (uint256 _balance) { - return lockedBalances[_addr]; - } - - /** - * @notice The function to get the unlocked balance of a user. - * @param _addr The address of the user to check the unlocked balance. - * @return _balance The unlocked balance of the address `_addr`. - */ - function getUnlockedBalance(address _addr) external view returns (uint256 _balance) { - return unlockedBalances[_addr]; - } - - /** - * @notice The function to check is an address is admin or not. - * @param _addr The address of the user to check the admin status. - * @return _status True if admin, False otherwise. - */ - function adminStatus(address _addr) external view returns (bool _status) { - return isAdmin[_addr]; - } + using SafeMath for uint256; + + uint256 public constant MAX_BASIS_POINT = 10000; + uint256 public constant MAX_DURATION = 37; + + /* Storage */ + + /// @notice True if the migration to a new Locked SOV Contract has started. + bool public migration; + + /// @notice The cliff is the time period after which the tokens begin to unlock. + uint256 public cliff; + /// @notice The duration is the time period after all tokens will have been unlocked. + uint256 public duration; + + /// @notice The SOV token contract. + IERC20 public SOV; + /// @notice The Vesting registry contract. + VestingRegistry public vestingRegistry; + /// @notice The New (Future) Locked SOV. + ILockedSOV public newLockedSOV; + + /// @notice The locked user balances. + mapping(address => uint256) private lockedBalances; + /// @notice The unlocked user balances. + mapping(address => uint256) private unlockedBalances; + /// @notice The contracts/wallets with admin power. + mapping(address => bool) private isAdmin; + + /* Events */ + + /// @notice Emitted when a new Admin is added to the admin list. + /// @param _initiator The address which initiated this event to be emitted. + /// @param _newAdmin The address of the new admin. + event AdminAdded(address indexed _initiator, address indexed _newAdmin); + + /// @notice Emitted when an admin is removed from the admin list. + /// @param _initiator The address which initiated this event to be emitted. + /// @param _removedAdmin The address of the removed admin. + event AdminRemoved(address indexed _initiator, address indexed _removedAdmin); + + /// @notice Emitted when Vesting Registry, Duration and/or Cliff is updated. + /// @param _initiator The address which initiated this event to be emitted. + /// @param _vestingRegistry The Vesting Registry Contract. + /// @param _cliff The time period after which the tokens begin to unlock. + /// @param _duration The time period after all tokens will have been unlocked. + event RegistryCliffAndDurationUpdated( + address indexed _initiator, + address indexed _vestingRegistry, + uint256 _cliff, + uint256 _duration + ); + + /// @notice Emitted when a new deposit is made. + /// @param _initiator The address which initiated this event to be emitted. + /// @param _userAddress The user to whose un/locked balance a new deposit was made. + /// @param _sovAmount The amount of SOV to be added to the un/locked balance. + /// @param _basisPoint The % (in Basis Point) which determines how much will be unlocked immediately. + event Deposited( + address indexed _initiator, + address indexed _userAddress, + uint256 _sovAmount, + uint256 _basisPoint + ); + + /// @notice Emitted when a user withdraws the fund. + /// @param _initiator The address which initiated this event to be emitted. + /// @param _userAddress The user whose unlocked balance has to be withdrawn. + /// @param _sovAmount The amount of SOV withdrawn from the unlocked balance. + event Withdrawn(address indexed _initiator, address indexed _userAddress, uint256 _sovAmount); + + /// @notice Emitted when a user creates a vesting for himself. + /// @param _initiator The address which initiated this event to be emitted. + /// @param _userAddress The user whose unlocked balance has to be withdrawn. + /// @param _vesting The Vesting Contract. + event VestingCreated( + address indexed _initiator, + address indexed _userAddress, + address indexed _vesting + ); + + /// @notice Emitted when a user stakes tokens. + /// @param _initiator The address which initiated this event to be emitted. + /// @param _vesting The Vesting Contract. + /// @param _amount The amount of locked tokens staked by the user. + event TokenStaked(address indexed _initiator, address indexed _vesting, uint256 _amount); + + /// @notice Emitted when an admin initiates a migration to new Locked SOV Contract. + /// @param _initiator The address which initiated this event to be emitted. + /// @param _newLockedSOV The address of the new Locked SOV Contract. + event MigrationStarted(address indexed _initiator, address indexed _newLockedSOV); + + /// @notice Emitted when a user initiates the transfer to a new Locked SOV Contract. + /// @param _initiator The address which initiated this event to be emitted. + /// @param _amount The amount of locked tokens to transfer from this contract to the new one. + event UserTransfered(address indexed _initiator, uint256 _amount); + + /* Modifiers */ + + modifier onlyAdmin { + require(isAdmin[msg.sender], "Only admin can call this."); + _; + } + + modifier migrationAllowed { + require(migration, "Migration has not yet started."); + _; + } + + /* Constructor */ + + /** + * @notice Setup the required parameters. + * @param _SOV The SOV Token Address. + * @param _vestingRegistry The Vesting Registry Address. + * @param _cliff The time period after which the tokens begin to unlock. + * @param _duration The time period after all tokens will have been unlocked. + * @param _admins The list of Admins to be added. + */ + constructor( + address _SOV, + address _vestingRegistry, + uint256 _cliff, + uint256 _duration, + address[] memory _admins + ) public { + require(_SOV != address(0), "Invalid SOV Address."); + require(_vestingRegistry != address(0), "Vesting registry address is invalid."); + require(_duration < MAX_DURATION, "Duration is too long."); + + SOV = IERC20(_SOV); + vestingRegistry = VestingRegistry(_vestingRegistry); + cliff = _cliff * 4 weeks; + duration = _duration * 4 weeks; + + for (uint256 index = 0; index < _admins.length; index++) { + isAdmin[_admins[index]] = true; + } + } + + /* Public or External Functions */ + + /** + * @notice The function to add a new admin. + * @param _newAdmin The address of the new admin. + * @dev Only callable by an Admin. + */ + function addAdmin(address _newAdmin) public onlyAdmin { + require(_newAdmin != address(0), "Invalid Address."); + require(!isAdmin[_newAdmin], "Address is already admin."); + isAdmin[_newAdmin] = true; + + emit AdminAdded(msg.sender, _newAdmin); + } + + /** + * @notice The function to remove an admin. + * @param _adminToRemove The address of the admin which should be removed. + * @dev Only callable by an Admin. + */ + function removeAdmin(address _adminToRemove) public onlyAdmin { + require(isAdmin[_adminToRemove], "Address is not an admin."); + isAdmin[_adminToRemove] = false; + + emit AdminRemoved(msg.sender, _adminToRemove); + } + + /** + * @notice The function to update the Vesting Registry, Duration and Cliff. + * @param _vestingRegistry The Vesting Registry Address. + * @param _cliff The time period after which the tokens begin to unlock. + * @param _duration The time period after all tokens will have been unlocked. + * @dev IMPORTANT 1: You have to change Vesting Registry if you want to change Duration and/or Cliff. + * IMPORTANT 2: `_cliff` and `_duration` is multiplied by 4 weeks in this function. + */ + function changeRegistryCliffAndDuration( + address _vestingRegistry, + uint256 _cliff, + uint256 _duration + ) external onlyAdmin { + require( + address(vestingRegistry) != _vestingRegistry, + "Vesting Registry has to be different for changing duration and cliff." + ); + /// If duration is also zero, then it is similar to Unlocked SOV. + require(_duration != 0, "Duration cannot be zero."); + require(_duration < MAX_DURATION, "Duration is too long."); + + vestingRegistry = VestingRegistry(_vestingRegistry); + + cliff = _cliff * 4 weeks; + duration = _duration * 4 weeks; + + emit RegistryCliffAndDurationUpdated(msg.sender, _vestingRegistry, _cliff, _duration); + } + + /** + * @notice Adds SOV to the user balance (Locked and Unlocked Balance based on `_basisPoint`). + * @param _userAddress The user whose locked balance has to be updated with `_sovAmount`. + * @param _sovAmount The amount of SOV to be added to the locked and/or unlocked balance. + * @param _basisPoint The % (in Basis Point)which determines how much will be unlocked immediately. + */ + function deposit( + address _userAddress, + uint256 _sovAmount, + uint256 _basisPoint + ) external { + _deposit(_userAddress, _sovAmount, _basisPoint); + } + + /** + * @notice Adds SOV to the locked balance of a user. + * @param _userAddress The user whose locked balance has to be updated with _sovAmount. + * @param _sovAmount The amount of SOV to be added to the locked balance. + * @dev This is here because there are dependency with other contracts. + */ + function depositSOV(address _userAddress, uint256 _sovAmount) external { + _deposit(_userAddress, _sovAmount, 0); + } + + function _deposit( + address _userAddress, + uint256 _sovAmount, + uint256 _basisPoint + ) private { + // MAX_BASIS_POINT is not included because if 100% is unlocked, then LockedSOV is not required to be used. + require(_basisPoint < MAX_BASIS_POINT, "Basis Point has to be less than 10000."); + bool txStatus = SOV.transferFrom(msg.sender, address(this), _sovAmount); + require(txStatus, "Token transfer was not successful. Check receiver address."); + + uint256 unlockedBal = _sovAmount.mul(_basisPoint).div(MAX_BASIS_POINT); + + unlockedBalances[_userAddress] = unlockedBalances[_userAddress].add(unlockedBal); + lockedBalances[_userAddress] = lockedBalances[_userAddress].add(_sovAmount).sub( + unlockedBal + ); + + emit Deposited(msg.sender, _userAddress, _sovAmount, _basisPoint); + } + + /** + * @notice A function to withdraw the unlocked balance. + * @param _receiverAddress If specified, the unlocked balance will go to this address, else to msg.sender. + */ + function withdraw(address _receiverAddress) public { + _withdraw(msg.sender, _receiverAddress); + } + + function _withdraw(address _sender, address _receiverAddress) private { + address userAddr = _receiverAddress; + if (_receiverAddress == address(0)) { + userAddr = _sender; + } + + uint256 amount = unlockedBalances[_sender]; + unlockedBalances[_sender] = 0; + + bool txStatus = SOV.transfer(userAddr, amount); + require(txStatus, "Token transfer was not successful. Check receiver address."); + + emit Withdrawn(_sender, userAddr, amount); + } + + /** + * @notice Creates vesting if not already created and Stakes tokens for a user. + * @dev Only use this function if the `duration` is small. + */ + function createVestingAndStake() public { + _createVestingAndStake(msg.sender); + } + + function _createVestingAndStake(address _sender) private { + address vestingAddr = _getVesting(_sender); + + if (vestingAddr == address(0)) { + vestingAddr = _createVesting(_sender); + } + + _stakeTokens(_sender, vestingAddr); + } + + /** + * @notice Creates vesting contract (if it hasn't been created yet) for the calling user. + * @return _vestingAddress The New Vesting Contract Created. + */ + function createVesting() public returns (address _vestingAddress) { + _vestingAddress = _createVesting(msg.sender); + } + + /** + * @notice Stakes tokens for a user who already have a vesting created. + * @dev The user should already have a vesting created, else this function will throw error. + */ + function stakeTokens() public { + VestingLogic vesting = VestingLogic(_getVesting(msg.sender)); + + require( + cliff == vesting.cliff() && duration == vesting.duration(), + "Wrong Vesting Schedule." + ); + + _stakeTokens(msg.sender, address(vesting)); + } + + /** + * @notice Withdraws unlocked tokens and Stakes Locked tokens for a user who already have a vesting created. + * @param _receiverAddress If specified, the unlocked balance will go to this address, else to msg.sender. + */ + function withdrawAndStakeTokens(address _receiverAddress) external { + _withdraw(msg.sender, _receiverAddress); + _createVestingAndStake(msg.sender); + } + + /** + * @notice Withdraws unlocked tokens and Stakes Locked tokens for a user who already have a vesting created. + * @param _userAddress The address of user tokens will be withdrawn. + */ + function withdrawAndStakeTokensFrom(address _userAddress) external { + _withdraw(_userAddress, _userAddress); + _createVestingAndStake(_userAddress); + } + + /** + * @notice Function to start the process of migration to new contract. + * @param _newLockedSOV The new locked sov contract address. + */ + function startMigration(address _newLockedSOV) external onlyAdmin { + require(_newLockedSOV != address(0), "New Locked SOV Address is Invalid."); + newLockedSOV = ILockedSOV(_newLockedSOV); + SOV.approve(_newLockedSOV, SOV.balanceOf(address(this))); + migration = true; + + emit MigrationStarted(msg.sender, _newLockedSOV); + } + + /** + * @notice Function to transfer the locked balance from this contract to new LockedSOV Contract. + * @dev Address is not specified to discourage selling lockedSOV to other address. + */ + function transfer() external migrationAllowed { + uint256 amount = lockedBalances[msg.sender]; + lockedBalances[msg.sender] = 0; + + newLockedSOV.depositSOV(msg.sender, amount); + + emit UserTransfered(msg.sender, amount); + } + + /* Internal Functions */ + + /** + * @notice Creates a Vesting Contract for a user. + * @param _tokenOwner The owner of the vesting contract. + * @return _vestingAddress The Vesting Contract Address. + * @dev Does not do anything if Vesting Contract was already created. + */ + function _createVesting(address _tokenOwner) internal returns (address _vestingAddress) { + /// Here zero is given in place of amount, as amount is not really used in `vestingRegistry.createVesting()`. + vestingRegistry.createVesting(_tokenOwner, 0, cliff, duration); + _vestingAddress = _getVesting(_tokenOwner); + emit VestingCreated(msg.sender, _tokenOwner, _vestingAddress); + } + + /** + * @notice Returns the Vesting Contract Address. + * @param _tokenOwner The owner of the vesting contract. + * @return _vestingAddress The Vesting Contract Address. + */ + function _getVesting(address _tokenOwner) internal view returns (address _vestingAddress) { + return vestingRegistry.getVesting(_tokenOwner); + } + + /** + * @notice Stakes the tokens in a particular vesting contract. + * @param _vesting The Vesting Contract Address. + */ + function _stakeTokens(address _sender, address _vesting) internal { + uint256 amount = lockedBalances[_sender]; + lockedBalances[_sender] = 0; + + require(SOV.approve(_vesting, amount), "Approve failed."); + VestingLogic(_vesting).stakeTokens(amount); + + emit TokenStaked(_sender, _vesting, amount); + } + + /* Getter or Read Functions */ + + /** + * @notice The function to get the locked balance of a user. + * @param _addr The address of the user to check the locked balance. + * @return _balance The locked balance of the address `_addr`. + */ + function getLockedBalance(address _addr) external view returns (uint256 _balance) { + return lockedBalances[_addr]; + } + + /** + * @notice The function to get the unlocked balance of a user. + * @param _addr The address of the user to check the unlocked balance. + * @return _balance The unlocked balance of the address `_addr`. + */ + function getUnlockedBalance(address _addr) external view returns (uint256 _balance) { + return unlockedBalances[_addr]; + } + + /** + * @notice The function to check is an address is admin or not. + * @param _addr The address of the user to check the admin status. + * @return _status True if admin, False otherwise. + */ + function adminStatus(address _addr) external view returns (bool _status) { + return isAdmin[_addr]; + } } diff --git a/contracts/mixins/EnumerableAddressSet.sol b/contracts/mixins/EnumerableAddressSet.sol index bb4fe34ae..7312af318 100644 --- a/contracts/mixins/EnumerableAddressSet.sol +++ b/contracts/mixins/EnumerableAddressSet.sol @@ -18,65 +18,65 @@ pragma solidity ^0.5.0; * _Available since v2.5.0._ */ library EnumerableAddressSet { - struct AddressSet { - // Position of the value in the `values` array, plus 1 because index 0 - // means a value is not in the set. - mapping(address => uint256) index; - address[] values; - } - - /** - * @dev Add a value to a set. O(1). - * Returns false if the value was already in the set. - */ - function add(AddressSet storage set, address value) internal returns (bool) { - if (!contains(set, value)) { - set.index[value] = set.values.push(value); - return true; - } else { - return false; - } - } - - /** - * @dev Removes a value from a set. O(1). - * Returns false if the value was not present in the set. - */ - function remove(AddressSet storage set, address value) internal returns (bool) { - if (contains(set, value)) { - uint256 toDeleteIndex = set.index[value] - 1; - uint256 lastIndex = set.values.length - 1; - - // If the element we're deleting is the last one, we can just remove it without doing a swap - if (lastIndex != toDeleteIndex) { - address lastValue = set.values[lastIndex]; - - // Move the last value to the index where the deleted value is - set.values[toDeleteIndex] = lastValue; - // Update the index for the moved value - set.index[lastValue] = toDeleteIndex + 1; // All indexes are 1-based - } - - // Delete the index entry for the deleted value - delete set.index[value]; - - // Delete the old entry for the moved value - set.values.pop(); - - return true; - } else { - return false; - } - } - - /** - * @dev Returns true if the value is in the set. O(1). - */ - function contains(AddressSet storage set, address value) internal view returns (bool) { - return set.index[value] != 0; - } - - /** + struct AddressSet { + // Position of the value in the `values` array, plus 1 because index 0 + // means a value is not in the set. + mapping(address => uint256) index; + address[] values; + } + + /** + * @dev Add a value to a set. O(1). + * Returns false if the value was already in the set. + */ + function add(AddressSet storage set, address value) internal returns (bool) { + if (!contains(set, value)) { + set.index[value] = set.values.push(value); + return true; + } else { + return false; + } + } + + /** + * @dev Removes a value from a set. O(1). + * Returns false if the value was not present in the set. + */ + function remove(AddressSet storage set, address value) internal returns (bool) { + if (contains(set, value)) { + uint256 toDeleteIndex = set.index[value] - 1; + uint256 lastIndex = set.values.length - 1; + + // If the element we're deleting is the last one, we can just remove it without doing a swap + if (lastIndex != toDeleteIndex) { + address lastValue = set.values[lastIndex]; + + // Move the last value to the index where the deleted value is + set.values[toDeleteIndex] = lastValue; + // Update the index for the moved value + set.index[lastValue] = toDeleteIndex + 1; // All indexes are 1-based + } + + // Delete the index entry for the deleted value + delete set.index[value]; + + // Delete the old entry for the moved value + set.values.pop(); + + return true; + } else { + return false; + } + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function contains(AddressSet storage set, address value) internal view returns (bool) { + return set.index[value] != 0; + } + + /** * @dev Returns an array with all values in the set. O(N). * Note that there are no guarantees on the ordering of values inside the * array, and it may change when more values are added or removed. @@ -84,15 +84,15 @@ library EnumerableAddressSet { * WARNING: This function may run out of gas on large sets: use {length} and * {get} instead in these cases. */ - function enumerate(AddressSet storage set) internal view returns (address[] memory) { - address[] memory output = new address[](set.values.length); - for (uint256 i; i < set.values.length; i++) { - output[i] = set.values[i]; - } - return output; - } - - /** + function enumerate(AddressSet storage set) internal view returns (address[] memory) { + address[] memory output = new address[](set.values.length); + for (uint256 i; i < set.values.length; i++) { + output[i] = set.values[i]; + } + return output; + } + + /** * @dev Returns a chunk of array as recommended in enumerate() to avoid running of gas. * Note that there are no guarantees on the ordering of values inside the * array, and it may change when more values are added or removed. @@ -103,41 +103,41 @@ library EnumerableAddressSet { * @param start start index of chunk * @param count num of element to return; if count == 0 then returns all the elements from the @param start */ - function enumerateChunk( - AddressSet storage set, - uint256 start, - uint256 count - ) internal view returns (address[] memory output) { - uint256 end = start + count; - require(end >= start, "addition overflow"); - end = (set.values.length < end || count == 0) ? set.values.length : end; - if (end == 0 || start >= end) { - return output; - } - - output = new address[](end - start); - for (uint256 i; i < end - start; i++) { - output[i] = set.values[i + start]; - } - return output; - } - - /** - * @dev Returns the number of elements on the set. O(1). - */ - function length(AddressSet storage set) internal view returns (uint256) { - return set.values.length; - } - - /** @dev Returns the element stored at position `index` in the set. O(1). - * Note that there are no guarantees on the ordering of values inside the - * array, and it may change when more values are added or removed. - * - * Requirements: - * - * - `index` must be strictly less than {length}. - */ - function get(AddressSet storage set, uint256 index) internal view returns (address) { - return set.values[index]; - } + function enumerateChunk( + AddressSet storage set, + uint256 start, + uint256 count + ) internal view returns (address[] memory output) { + uint256 end = start + count; + require(end >= start, "addition overflow"); + end = (set.values.length < end || count == 0) ? set.values.length : end; + if (end == 0 || start >= end) { + return output; + } + + output = new address[](end - start); + for (uint256 i; i < end - start; i++) { + output[i] = set.values[i + start]; + } + return output; + } + + /** + * @dev Returns the number of elements on the set. O(1). + */ + function length(AddressSet storage set) internal view returns (uint256) { + return set.values.length; + } + + /** @dev Returns the element stored at position `index` in the set. O(1). + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function get(AddressSet storage set, uint256 index) internal view returns (address) { + return set.values[index]; + } } diff --git a/contracts/mixins/EnumerableBytes32Set.sol b/contracts/mixins/EnumerableBytes32Set.sol index 0aa94a20b..94eaa6901 100644 --- a/contracts/mixins/EnumerableBytes32Set.sol +++ b/contracts/mixins/EnumerableBytes32Set.sol @@ -17,183 +17,187 @@ pragma solidity 0.5.17; * Include with `using EnumerableBytes32Set for EnumerableBytes32Set.Bytes32Set;`. * */ library EnumerableBytes32Set { - struct Bytes32Set { - /// Position of the value in the `values` array, plus 1 because index 0 - /// means a value is not in the set. - mapping(bytes32 => uint256) index; - bytes32[] values; - } - - /** - * @notice Add an address value to a set. O(1). - * - * @param set The set of values. - * @param addrvalue The address to add. - * - * @return False if the value was already in the set. - */ - function addAddress(Bytes32Set storage set, address addrvalue) internal returns (bool) { - bytes32 value; - assembly { - value := addrvalue - } - return addBytes32(set, value); - } - - /** - * @notice Add a value to a set. O(1). - * - * @param set The set of values. - * @param value The new value to add. - * - * @return False if the value was already in the set. - */ - function addBytes32(Bytes32Set storage set, bytes32 value) internal returns (bool) { - if (!contains(set, value)) { - set.index[value] = set.values.push(value); - return true; - } else { - return false; - } - } - - /** - * @notice Remove an address value from a set. O(1). - * - * @param set The set of values. - * @param addrvalue The address to remove. - * - * @return False if the address was not present in the set. - */ - function removeAddress(Bytes32Set storage set, address addrvalue) internal returns (bool) { - bytes32 value; - assembly { - value := addrvalue - } - return removeBytes32(set, value); - } - - /** - * @notice Remove a value from a set. O(1). - * - * @param set The set of values. - * @param value The value to remove. - * - * @return False if the value was not present in the set. - */ - function removeBytes32(Bytes32Set storage set, bytes32 value) internal returns (bool) { - if (contains(set, value)) { - uint256 toDeleteIndex = set.index[value] - 1; - uint256 lastIndex = set.values.length - 1; - - /// If the element we're deleting is the last one, - /// we can just remove it without doing a swap. - if (lastIndex != toDeleteIndex) { - bytes32 lastValue = set.values[lastIndex]; - - /// Move the last value to the index where the deleted value is. - set.values[toDeleteIndex] = lastValue; - - /// Update the index for the moved value. - set.index[lastValue] = toDeleteIndex + 1; // All indexes are 1-based - } - - /// Delete the index entry for the deleted value. - delete set.index[value]; - - /// Delete the old entry for the moved value. - set.values.pop(); - - return true; - } else { - return false; - } - } - - /** - * @notice Find out whether a value exists in the set. - * - * @param set The set of values. - * @param value The value to find. - * - * @return True if the value is in the set. O(1). - */ - function contains(Bytes32Set storage set, bytes32 value) internal view returns (bool) { - return set.index[value] != 0; - } - - /** - * @dev Returns true if the value is in the set. O(1). - */ - function containsAddress(Bytes32Set storage set, address addrvalue) internal view returns (bool) { - bytes32 value; - assembly { - value := addrvalue - } - return set.index[value] != 0; - } - - /** - * @notice Get all set values. - * - * @param set The set of values. - * @param start The offset of the returning set. - * @param count The limit of number of values to return. - * - * @return An array with all values in the set. O(N). - * - * @dev Note that there are no guarantees on the ordering of values inside the - * array, and it may change when more values are added or removed. - * - * WARNING: This function may run out of gas on large sets: use {length} and - * {get} instead in these cases. - */ - function enumerate( - Bytes32Set storage set, - uint256 start, - uint256 count - ) internal view returns (bytes32[] memory output) { - uint256 end = start + count; - require(end >= start, "addition overflow"); - end = set.values.length < end ? set.values.length : end; - if (end == 0 || start >= end) { - return output; - } - - output = new bytes32[](end - start); - for (uint256 i; i < end - start; i++) { - output[i] = set.values[i + start]; - } - return output; - } - - /** - * @notice Get the legth of the set. - * - * @param set The set of values. - * - * @return the number of elements on the set. O(1). - */ - function length(Bytes32Set storage set) internal view returns (uint256) { - return set.values.length; - } - - /** - * @notice Get an item from the set by its index. - * - * @dev Note that there are no guarantees on the ordering of values inside the - * array, and it may change when more values are added or removed. - * - * Requirements: - * - * - `index` must be strictly less than {length}. - * - * @param set The set of values. - * @param index The index of the value to return. - * - * @return the element stored at position `index` in the set. O(1). - */ - function get(Bytes32Set storage set, uint256 index) internal view returns (bytes32) { - return set.values[index]; - } + struct Bytes32Set { + /// Position of the value in the `values` array, plus 1 because index 0 + /// means a value is not in the set. + mapping(bytes32 => uint256) index; + bytes32[] values; + } + + /** + * @notice Add an address value to a set. O(1). + * + * @param set The set of values. + * @param addrvalue The address to add. + * + * @return False if the value was already in the set. + */ + function addAddress(Bytes32Set storage set, address addrvalue) internal returns (bool) { + bytes32 value; + assembly { + value := addrvalue + } + return addBytes32(set, value); + } + + /** + * @notice Add a value to a set. O(1). + * + * @param set The set of values. + * @param value The new value to add. + * + * @return False if the value was already in the set. + */ + function addBytes32(Bytes32Set storage set, bytes32 value) internal returns (bool) { + if (!contains(set, value)) { + set.index[value] = set.values.push(value); + return true; + } else { + return false; + } + } + + /** + * @notice Remove an address value from a set. O(1). + * + * @param set The set of values. + * @param addrvalue The address to remove. + * + * @return False if the address was not present in the set. + */ + function removeAddress(Bytes32Set storage set, address addrvalue) internal returns (bool) { + bytes32 value; + assembly { + value := addrvalue + } + return removeBytes32(set, value); + } + + /** + * @notice Remove a value from a set. O(1). + * + * @param set The set of values. + * @param value The value to remove. + * + * @return False if the value was not present in the set. + */ + function removeBytes32(Bytes32Set storage set, bytes32 value) internal returns (bool) { + if (contains(set, value)) { + uint256 toDeleteIndex = set.index[value] - 1; + uint256 lastIndex = set.values.length - 1; + + /// If the element we're deleting is the last one, + /// we can just remove it without doing a swap. + if (lastIndex != toDeleteIndex) { + bytes32 lastValue = set.values[lastIndex]; + + /// Move the last value to the index where the deleted value is. + set.values[toDeleteIndex] = lastValue; + + /// Update the index for the moved value. + set.index[lastValue] = toDeleteIndex + 1; // All indexes are 1-based + } + + /// Delete the index entry for the deleted value. + delete set.index[value]; + + /// Delete the old entry for the moved value. + set.values.pop(); + + return true; + } else { + return false; + } + } + + /** + * @notice Find out whether a value exists in the set. + * + * @param set The set of values. + * @param value The value to find. + * + * @return True if the value is in the set. O(1). + */ + function contains(Bytes32Set storage set, bytes32 value) internal view returns (bool) { + return set.index[value] != 0; + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function containsAddress(Bytes32Set storage set, address addrvalue) + internal + view + returns (bool) + { + bytes32 value; + assembly { + value := addrvalue + } + return set.index[value] != 0; + } + + /** + * @notice Get all set values. + * + * @param set The set of values. + * @param start The offset of the returning set. + * @param count The limit of number of values to return. + * + * @return An array with all values in the set. O(N). + * + * @dev Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * WARNING: This function may run out of gas on large sets: use {length} and + * {get} instead in these cases. + */ + function enumerate( + Bytes32Set storage set, + uint256 start, + uint256 count + ) internal view returns (bytes32[] memory output) { + uint256 end = start + count; + require(end >= start, "addition overflow"); + end = set.values.length < end ? set.values.length : end; + if (end == 0 || start >= end) { + return output; + } + + output = new bytes32[](end - start); + for (uint256 i; i < end - start; i++) { + output[i] = set.values[i + start]; + } + return output; + } + + /** + * @notice Get the legth of the set. + * + * @param set The set of values. + * + * @return the number of elements on the set. O(1). + */ + function length(Bytes32Set storage set) internal view returns (uint256) { + return set.values.length; + } + + /** + * @notice Get an item from the set by its index. + * + * @dev Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + * + * @param set The set of values. + * @param index The index of the value to return. + * + * @return the element stored at position `index` in the set. O(1). + */ + function get(Bytes32Set storage set, uint256 index) internal view returns (bytes32) { + return set.values[index]; + } } diff --git a/contracts/mixins/EnumerableBytes4Set.sol b/contracts/mixins/EnumerableBytes4Set.sol index 775f66803..e0714aebc 100644 --- a/contracts/mixins/EnumerableBytes4Set.sol +++ b/contracts/mixins/EnumerableBytes4Set.sol @@ -17,183 +17,187 @@ pragma solidity 0.5.17; * Include with `using EnumerableBytes4Set for EnumerableBytes4Set.Bytes4Set;`. * */ library EnumerableBytes4Set { - struct Bytes4Set { - /// Position of the value in the `values` array, plus 1 because index 0 - /// means a value is not in the set. - mapping(bytes4 => uint256) index; - bytes4[] values; - } - - /** - * @notice Add an address value to a set. O(1). - * - * @param set The set of values. - * @param addrvalue The address to add. - * - * @return False if the value was already in the set. - */ - function addAddress(Bytes4Set storage set, address addrvalue) internal returns (bool) { - bytes4 value; - assembly { - value := addrvalue - } - return addBytes4(set, value); - } - - /** - * @notice Add a value to a set. O(1). - * - * @param set The set of values. - * @param value The new value to add. - * - * @return False if the value was already in the set. - */ - function addBytes4(Bytes4Set storage set, bytes4 value) internal returns (bool) { - if (!contains(set, value)) { - set.index[value] = set.values.push(value); - return true; - } else { - return false; - } - } - - /** - * @notice Remove an address value from a set. O(1). - * - * @param set The set of values. - * @param addrvalue The address to remove. - * - * @return False if the address was not present in the set. - */ - function removeAddress(Bytes4Set storage set, address addrvalue) internal returns (bool) { - bytes4 value; - assembly { - value := addrvalue - } - return removeBytes4(set, value); - } - - /** - * @notice Remove a value from a set. O(1). - * - * @param set The set of values. - * @param value The value to remove. - * - * @return False if the value was not present in the set. - */ - function removeBytes4(Bytes4Set storage set, bytes4 value) internal returns (bool) { - if (contains(set, value)) { - uint256 toDeleteIndex = set.index[value] - 1; - uint256 lastIndex = set.values.length - 1; - - /// If the element we're deleting is the last one, - /// we can just remove it without doing a swap. - if (lastIndex != toDeleteIndex) { - bytes4 lastValue = set.values[lastIndex]; - - /// Move the last value to the index where the deleted value is. - set.values[toDeleteIndex] = lastValue; - - /// Update the index for the moved value. - set.index[lastValue] = toDeleteIndex + 1; // All indexes are 1-based - } - - /// Delete the index entry for the deleted value. - delete set.index[value]; - - /// Delete the old entry for the moved value. - set.values.pop(); - - return true; - } else { - return false; - } - } - - /** - * @notice Find out whether a value exists in the set. - * - * @param set The set of values. - * @param value The value to find. - * - * @return True if the value is in the set. O(1). - */ - function contains(Bytes4Set storage set, bytes4 value) internal view returns (bool) { - return set.index[value] != 0; - } - - /** - * @dev Returns true if the value is in the set. O(1). - */ - function containsAddress(Bytes4Set storage set, address addrvalue) internal view returns (bool) { - bytes4 value; - assembly { - value := addrvalue - } - return set.index[value] != 0; - } - - /** - * @notice Get all set values. - * - * @param set The set of values. - * @param start The offset of the returning set. - * @param count The limit of number of values to return. - * - * @return An array with all values in the set. O(N). - * - * @dev Note that there are no guarantees on the ordering of values inside the - * array, and it may change when more values are added or removed. - * - * WARNING: This function may run out of gas on large sets: use {length} and - * {get} instead in these cases. - */ - function enumerate( - Bytes4Set storage set, - uint256 start, - uint256 count - ) internal view returns (bytes4[] memory output) { - uint256 end = start + count; - require(end >= start, "addition overflow"); - end = set.values.length < end ? set.values.length : end; - if (end == 0 || start >= end) { - return output; - } - - output = new bytes4[](end - start); - for (uint256 i; i < end - start; i++) { - output[i] = set.values[i + start]; - } - return output; - } - - /** - * @notice Get the legth of the set. - * - * @param set The set of values. - * - * @return the number of elements on the set. O(1). - */ - function length(Bytes4Set storage set) internal view returns (uint256) { - return set.values.length; - } - - /** - * @notice Get an item from the set by its index. - * - * @dev Note that there are no guarantees on the ordering of values inside the - * array, and it may change when more values are added or removed. - * - * Requirements: - * - * - `index` must be strictly less than {length}. - * - * @param set The set of values. - * @param index The index of the value to return. - * - * @return the element stored at position `index` in the set. O(1). - */ - function get(Bytes4Set storage set, uint256 index) internal view returns (bytes4) { - return set.values[index]; - } + struct Bytes4Set { + /// Position of the value in the `values` array, plus 1 because index 0 + /// means a value is not in the set. + mapping(bytes4 => uint256) index; + bytes4[] values; + } + + /** + * @notice Add an address value to a set. O(1). + * + * @param set The set of values. + * @param addrvalue The address to add. + * + * @return False if the value was already in the set. + */ + function addAddress(Bytes4Set storage set, address addrvalue) internal returns (bool) { + bytes4 value; + assembly { + value := addrvalue + } + return addBytes4(set, value); + } + + /** + * @notice Add a value to a set. O(1). + * + * @param set The set of values. + * @param value The new value to add. + * + * @return False if the value was already in the set. + */ + function addBytes4(Bytes4Set storage set, bytes4 value) internal returns (bool) { + if (!contains(set, value)) { + set.index[value] = set.values.push(value); + return true; + } else { + return false; + } + } + + /** + * @notice Remove an address value from a set. O(1). + * + * @param set The set of values. + * @param addrvalue The address to remove. + * + * @return False if the address was not present in the set. + */ + function removeAddress(Bytes4Set storage set, address addrvalue) internal returns (bool) { + bytes4 value; + assembly { + value := addrvalue + } + return removeBytes4(set, value); + } + + /** + * @notice Remove a value from a set. O(1). + * + * @param set The set of values. + * @param value The value to remove. + * + * @return False if the value was not present in the set. + */ + function removeBytes4(Bytes4Set storage set, bytes4 value) internal returns (bool) { + if (contains(set, value)) { + uint256 toDeleteIndex = set.index[value] - 1; + uint256 lastIndex = set.values.length - 1; + + /// If the element we're deleting is the last one, + /// we can just remove it without doing a swap. + if (lastIndex != toDeleteIndex) { + bytes4 lastValue = set.values[lastIndex]; + + /// Move the last value to the index where the deleted value is. + set.values[toDeleteIndex] = lastValue; + + /// Update the index for the moved value. + set.index[lastValue] = toDeleteIndex + 1; // All indexes are 1-based + } + + /// Delete the index entry for the deleted value. + delete set.index[value]; + + /// Delete the old entry for the moved value. + set.values.pop(); + + return true; + } else { + return false; + } + } + + /** + * @notice Find out whether a value exists in the set. + * + * @param set The set of values. + * @param value The value to find. + * + * @return True if the value is in the set. O(1). + */ + function contains(Bytes4Set storage set, bytes4 value) internal view returns (bool) { + return set.index[value] != 0; + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function containsAddress(Bytes4Set storage set, address addrvalue) + internal + view + returns (bool) + { + bytes4 value; + assembly { + value := addrvalue + } + return set.index[value] != 0; + } + + /** + * @notice Get all set values. + * + * @param set The set of values. + * @param start The offset of the returning set. + * @param count The limit of number of values to return. + * + * @return An array with all values in the set. O(N). + * + * @dev Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * WARNING: This function may run out of gas on large sets: use {length} and + * {get} instead in these cases. + */ + function enumerate( + Bytes4Set storage set, + uint256 start, + uint256 count + ) internal view returns (bytes4[] memory output) { + uint256 end = start + count; + require(end >= start, "addition overflow"); + end = set.values.length < end ? set.values.length : end; + if (end == 0 || start >= end) { + return output; + } + + output = new bytes4[](end - start); + for (uint256 i; i < end - start; i++) { + output[i] = set.values[i + start]; + } + return output; + } + + /** + * @notice Get the legth of the set. + * + * @param set The set of values. + * + * @return the number of elements on the set. O(1). + */ + function length(Bytes4Set storage set) internal view returns (uint256) { + return set.values.length; + } + + /** + * @notice Get an item from the set by its index. + * + * @dev Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + * + * @param set The set of values. + * @param index The index of the value to return. + * + * @return the element stored at position `index` in the set. O(1). + */ + function get(Bytes4Set storage set, uint256 index) internal view returns (bytes4) { + return set.values[index]; + } } diff --git a/contracts/mixins/FeesHelper.sol b/contracts/mixins/FeesHelper.sol index 328bf6d79..f8f9c8d24 100644 --- a/contracts/mixins/FeesHelper.sol +++ b/contracts/mixins/FeesHelper.sol @@ -21,27 +21,27 @@ import "../core/objects/LoanParamsStruct.sol"; * This contract calculates and pays lending/borrow fees and rewards. * */ contract FeesHelper is State, FeesEvents { - using SafeERC20 for IERC20; + using SafeERC20 for IERC20; - /** - * @notice Calculate trading fee. - * @param feeTokenAmount The amount of tokens to trade. - * @return The fee of the trade. - * */ - function _getTradingFee(uint256 feeTokenAmount) internal view returns (uint256) { - return feeTokenAmount.mul(tradingFeePercent).divCeil(10**20); - } - - /** - * @notice Calculate swap external fee. - * @param feeTokenAmount The amount of token to swap. - * @return The fee of the swap. - */ - function _getSwapExternalFee(uint256 feeTokenAmount) internal view returns (uint256) { - return feeTokenAmount.mul(swapExtrernalFeePercent).divCeil(10**20); - } - - /* + /** + * @notice Calculate trading fee. + * @param feeTokenAmount The amount of tokens to trade. + * @return The fee of the trade. + * */ + function _getTradingFee(uint256 feeTokenAmount) internal view returns (uint256) { + return feeTokenAmount.mul(tradingFeePercent).divCeil(10**20); + } + + /** + * @notice Calculate swap external fee. + * @param feeTokenAmount The amount of token to swap. + * @return The fee of the swap. + */ + function _getSwapExternalFee(uint256 feeTokenAmount) internal view returns (uint256) { + return feeTokenAmount.mul(swapExtrernalFeePercent).divCeil(10**20); + } + + /* // p3.9 from bzx peckshield-audit-report-bZxV2-v1.0rc1.pdf // cannot be applied solely nor with LoanOpenings.sol as it drives to some other tests failure function _getTradingFee(uint256 feeTokenAmount) internal view returns (uint256) { @@ -52,14 +52,14 @@ contract FeesHelper is State, FeesEvents { return collateralAmountRequired.sub(feeTokenAmount); }*/ - /** - * @notice Calculate the loan origination fee. - * @param feeTokenAmount The amount of tokens to borrow. - * @return The fee of the loan. - * */ - function _getBorrowingFee(uint256 feeTokenAmount) internal view returns (uint256) { - return feeTokenAmount.mul(borrowingFeePercent).divCeil(10**20); - /* + /** + * @notice Calculate the loan origination fee. + * @param feeTokenAmount The amount of tokens to borrow. + * @return The fee of the loan. + * */ + function _getBorrowingFee(uint256 feeTokenAmount) internal view returns (uint256) { + return feeTokenAmount.mul(borrowingFeePercent).divCeil(10**20); + /* // p3.9 from bzx peckshield-audit-report-bZxV2-v1.0rc1.pdf // cannot be applied solely nor with LoanOpenings.sol as it drives to some other tests failure uint256 collateralAmountRequired = @@ -67,189 +67,227 @@ contract FeesHelper is State, FeesEvents { 10**20 - borrowingFeePercent // never will overflow ); return collateralAmountRequired.sub(feeTokenAmount);*/ - } - - /** - * @notice Settle the trading fee and pay the token reward to the affiliates referrer. - * - * @param referrer The affiliate referrer address to send the reward to. - * @param trader The account that performs this trade. - * @param feeToken The address of the token in which the trading fee is paid. - * @param tradingFee The amount of tokens accrued as fees on the trading. - * - * @return affiliatesBonusSOVAmount the total SOV amount that is distributed to the referrer - * @return affiliatesBonusTokenAmount the total Token Base on the trading fee pairs that is distributed to the referrer - * */ - function _payTradingFeeToAffiliate( - address referrer, - address trader, - address feeToken, - uint256 tradingFee - ) internal returns (uint256 affiliatesBonusSOVAmount, uint256 affiliatesBonusTokenAmount) { - (affiliatesBonusSOVAmount, affiliatesBonusTokenAmount) = ProtocolAffiliatesInterface(address(this)) - .payTradingFeeToAffiliatesReferrer(referrer, trader, feeToken, tradingFee); - } - - /** - * @notice Settle the trading fee and pay the token reward to the user. - * @param user The address to send the reward to. - * @param loanId The Id of the associated loan - used for logging only. - * @param feeToken The address of the token in which the trading fee is paid. - * @param tradingFee The amount of tokens accrued as fees on the trading. - * */ - function _payTradingFee( - address user, - bytes32 loanId, - address feeToken, - address feeTokenPair, - uint256 tradingFee - ) internal { - uint256 protocolTradingFee = tradingFee; /// Trading fee paid to protocol. - if (tradingFee != 0) { - if (affiliatesUserReferrer[user] != address(0)) { - _payTradingFeeToAffiliate(affiliatesUserReferrer[user], user, feeToken, protocolTradingFee); - protocolTradingFee = (protocolTradingFee.sub(protocolTradingFee.mul(affiliateFeePercent).div(10**20))).sub( - protocolTradingFee.mul(affiliateTradingTokenFeePercent).div(10**20) - ); - } - - /// Increase the storage variable keeping track of the accumulated fees. - tradingFeeTokensHeld[feeToken] = tradingFeeTokensHeld[feeToken].add(protocolTradingFee); - - emit PayTradingFee(user, feeToken, loanId, protocolTradingFee); - - /// Pay the token reward to the user. - _payFeeReward(user, loanId, feeToken, feeTokenPair, tradingFee); - } - } - - /** - * @notice Settle the borrowing fee and pay the token reward to the user. - * @param user The address to send the reward to. - * @param loanId The Id of the associated loan - used for logging only. - * @param feeToken The address of the token in which the borrowig fee is paid. - * @param borrowingFee The height of the fee. - * */ - function _payBorrowingFee( - address user, - bytes32 loanId, - address feeToken, - address feeTokenPair, - uint256 borrowingFee - ) internal { - if (borrowingFee != 0) { - /// Increase the storage variable keeping track of the accumulated fees. - borrowingFeeTokensHeld[feeToken] = borrowingFeeTokensHeld[feeToken].add(borrowingFee); - - emit PayBorrowingFee(user, feeToken, loanId, borrowingFee); - - /// Pay the token reward to the user. - _payFeeReward(user, loanId, feeToken, feeTokenPair, borrowingFee); - } - } - - /** - * @notice Settle the lending fee (based on the interest). Pay no token reward to the user. - * @param user The address to send the reward to. - * @param feeToken The address of the token in which the lending fee is paid. - * @param lendingFee The height of the fee. - * */ - function _payLendingFee( - address user, - address feeToken, - uint256 lendingFee - ) internal { - if (lendingFee != 0) { - /// Increase the storage variable keeping track of the accumulated fees. - lendingFeeTokensHeld[feeToken] = lendingFeeTokensHeld[feeToken].add(lendingFee); - - emit PayLendingFee(user, feeToken, lendingFee); - - //// NOTE: Lenders do not receive a fee reward //// - } - } - - /// Settle and pay borrowers based on the fees generated by their interest payments. - function _settleFeeRewardForInterestExpense( - LoanInterest storage loanInterestLocal, - bytes32 loanId, - address feeToken, - address feeTokenPair, - address user, - uint256 interestTime - ) internal { - /// This represents the fee generated by a borrower's interest payment. - uint256 interestExpenseFee = - interestTime.sub(loanInterestLocal.updatedTimestamp).mul(loanInterestLocal.owedPerDay).mul(lendingFeePercent).div( - 1 days * 10**20 - ); + } - loanInterestLocal.updatedTimestamp = interestTime; - - if (interestExpenseFee != 0) { - _payFeeReward(user, loanId, feeToken, feeTokenPair, interestExpenseFee); - } - } - - /** - * @notice Pay the potocolToken reward to user. The reward is worth 50% of the trading/borrowing fee. - * @param user The address to send the reward to. - * @param loanId The Id of the associeated loan - used for logging only. - * @param feeToken The address of the token in which the trading/borrowing fee was paid. - * @param feeAmount The height of the fee. - * */ - function _payFeeReward( - address user, - bytes32 loanId, - address feeToken, - address feeTokenPair, - uint256 feeAmount - ) internal { - uint256 rewardAmount; - uint256 _feeRebatePercent = feeRebatePercent; - address _priceFeeds = priceFeeds; - - if (specialRebates[feeToken][feeTokenPair] > 0) { - _feeRebatePercent = specialRebates[feeToken][feeTokenPair]; - } - - /// Note: this should be refactored. - /// Calculate the reward amount, querying the price feed. - (bool success, bytes memory data) = - _priceFeeds.staticcall( - abi.encodeWithSelector( - IPriceFeeds(_priceFeeds).queryReturn.selector, - feeToken, - sovTokenAddress, /// Price rewards using BZRX price rather than vesting token price. - feeAmount.mul(_feeRebatePercent).div(10**20) - ) - ); - // solhint-disable-next-line no-inline-assembly - assembly { - if eq(success, 1) { - rewardAmount := mload(add(data, 32)) - } - } - - // Check the dedicated SOV that is used to pay trading rebate rewards - uint256 dedicatedSOV = ISovryn(address(this)).getDedicatedSOVRebate(); - if (rewardAmount != 0 && dedicatedSOV >= rewardAmount) { - IERC20(sovTokenAddress).approve(lockedSOVAddress, rewardAmount); - - (bool success, ) = - lockedSOVAddress.call( - abi.encodeWithSignature("deposit(address,uint256,uint256)", user, rewardAmount, tradingRebateRewardsBasisPoint) - ); - - if (success) { - protocolTokenPaid = protocolTokenPaid.add(rewardAmount); - - emit EarnReward(user, sovTokenAddress, loanId, _feeRebatePercent, rewardAmount, tradingRebateRewardsBasisPoint); - } else { - emit EarnRewardFail(user, sovTokenAddress, loanId, _feeRebatePercent, rewardAmount, tradingRebateRewardsBasisPoint); - } - } else if (rewardAmount != 0 && dedicatedSOV < rewardAmount) { - emit EarnRewardFail(user, sovTokenAddress, loanId, _feeRebatePercent, rewardAmount, tradingRebateRewardsBasisPoint); - } - } + /** + * @notice Settle the trading fee and pay the token reward to the affiliates referrer. + * + * @param referrer The affiliate referrer address to send the reward to. + * @param trader The account that performs this trade. + * @param feeToken The address of the token in which the trading fee is paid. + * @param tradingFee The amount of tokens accrued as fees on the trading. + * + * @return affiliatesBonusSOVAmount the total SOV amount that is distributed to the referrer + * @return affiliatesBonusTokenAmount the total Token Base on the trading fee pairs that is distributed to the referrer + * */ + function _payTradingFeeToAffiliate( + address referrer, + address trader, + address feeToken, + uint256 tradingFee + ) internal returns (uint256 affiliatesBonusSOVAmount, uint256 affiliatesBonusTokenAmount) { + (affiliatesBonusSOVAmount, affiliatesBonusTokenAmount) = ProtocolAffiliatesInterface( + address(this) + ) + .payTradingFeeToAffiliatesReferrer(referrer, trader, feeToken, tradingFee); + } + + /** + * @notice Settle the trading fee and pay the token reward to the user. + * @param user The address to send the reward to. + * @param loanId The Id of the associated loan - used for logging only. + * @param feeToken The address of the token in which the trading fee is paid. + * @param tradingFee The amount of tokens accrued as fees on the trading. + * */ + function _payTradingFee( + address user, + bytes32 loanId, + address feeToken, + address feeTokenPair, + uint256 tradingFee + ) internal { + uint256 protocolTradingFee = tradingFee; /// Trading fee paid to protocol. + if (tradingFee != 0) { + if (affiliatesUserReferrer[user] != address(0)) { + _payTradingFeeToAffiliate( + affiliatesUserReferrer[user], + user, + feeToken, + protocolTradingFee + ); + protocolTradingFee = ( + protocolTradingFee.sub(protocolTradingFee.mul(affiliateFeePercent).div(10**20)) + ) + .sub(protocolTradingFee.mul(affiliateTradingTokenFeePercent).div(10**20)); + } + + /// Increase the storage variable keeping track of the accumulated fees. + tradingFeeTokensHeld[feeToken] = tradingFeeTokensHeld[feeToken].add( + protocolTradingFee + ); + + emit PayTradingFee(user, feeToken, loanId, protocolTradingFee); + + /// Pay the token reward to the user. + _payFeeReward(user, loanId, feeToken, feeTokenPair, tradingFee); + } + } + + /** + * @notice Settle the borrowing fee and pay the token reward to the user. + * @param user The address to send the reward to. + * @param loanId The Id of the associated loan - used for logging only. + * @param feeToken The address of the token in which the borrowig fee is paid. + * @param borrowingFee The height of the fee. + * */ + function _payBorrowingFee( + address user, + bytes32 loanId, + address feeToken, + address feeTokenPair, + uint256 borrowingFee + ) internal { + if (borrowingFee != 0) { + /// Increase the storage variable keeping track of the accumulated fees. + borrowingFeeTokensHeld[feeToken] = borrowingFeeTokensHeld[feeToken].add(borrowingFee); + + emit PayBorrowingFee(user, feeToken, loanId, borrowingFee); + + /// Pay the token reward to the user. + _payFeeReward(user, loanId, feeToken, feeTokenPair, borrowingFee); + } + } + + /** + * @notice Settle the lending fee (based on the interest). Pay no token reward to the user. + * @param user The address to send the reward to. + * @param feeToken The address of the token in which the lending fee is paid. + * @param lendingFee The height of the fee. + * */ + function _payLendingFee( + address user, + address feeToken, + uint256 lendingFee + ) internal { + if (lendingFee != 0) { + /// Increase the storage variable keeping track of the accumulated fees. + lendingFeeTokensHeld[feeToken] = lendingFeeTokensHeld[feeToken].add(lendingFee); + + emit PayLendingFee(user, feeToken, lendingFee); + + //// NOTE: Lenders do not receive a fee reward //// + } + } + + /// Settle and pay borrowers based on the fees generated by their interest payments. + function _settleFeeRewardForInterestExpense( + LoanInterest storage loanInterestLocal, + bytes32 loanId, + address feeToken, + address feeTokenPair, + address user, + uint256 interestTime + ) internal { + /// This represents the fee generated by a borrower's interest payment. + uint256 interestExpenseFee = + interestTime + .sub(loanInterestLocal.updatedTimestamp) + .mul(loanInterestLocal.owedPerDay) + .mul(lendingFeePercent) + .div(1 days * 10**20); + + loanInterestLocal.updatedTimestamp = interestTime; + + if (interestExpenseFee != 0) { + _payFeeReward(user, loanId, feeToken, feeTokenPair, interestExpenseFee); + } + } + + /** + * @notice Pay the potocolToken reward to user. The reward is worth 50% of the trading/borrowing fee. + * @param user The address to send the reward to. + * @param loanId The Id of the associeated loan - used for logging only. + * @param feeToken The address of the token in which the trading/borrowing fee was paid. + * @param feeAmount The height of the fee. + * */ + function _payFeeReward( + address user, + bytes32 loanId, + address feeToken, + address feeTokenPair, + uint256 feeAmount + ) internal { + uint256 rewardAmount; + uint256 _feeRebatePercent = feeRebatePercent; + address _priceFeeds = priceFeeds; + + if (specialRebates[feeToken][feeTokenPair] > 0) { + _feeRebatePercent = specialRebates[feeToken][feeTokenPair]; + } + + /// Note: this should be refactored. + /// Calculate the reward amount, querying the price feed. + (bool success, bytes memory data) = + _priceFeeds.staticcall( + abi.encodeWithSelector( + IPriceFeeds(_priceFeeds).queryReturn.selector, + feeToken, + sovTokenAddress, /// Price rewards using BZRX price rather than vesting token price. + feeAmount.mul(_feeRebatePercent).div(10**20) + ) + ); + // solhint-disable-next-line no-inline-assembly + assembly { + if eq(success, 1) { + rewardAmount := mload(add(data, 32)) + } + } + + // Check the dedicated SOV that is used to pay trading rebate rewards + uint256 dedicatedSOV = ISovryn(address(this)).getDedicatedSOVRebate(); + if (rewardAmount != 0 && dedicatedSOV >= rewardAmount) { + IERC20(sovTokenAddress).approve(lockedSOVAddress, rewardAmount); + + (bool success, ) = + lockedSOVAddress.call( + abi.encodeWithSignature( + "deposit(address,uint256,uint256)", + user, + rewardAmount, + tradingRebateRewardsBasisPoint + ) + ); + + if (success) { + protocolTokenPaid = protocolTokenPaid.add(rewardAmount); + + emit EarnReward( + user, + sovTokenAddress, + loanId, + _feeRebatePercent, + rewardAmount, + tradingRebateRewardsBasisPoint + ); + } else { + emit EarnRewardFail( + user, + sovTokenAddress, + loanId, + _feeRebatePercent, + rewardAmount, + tradingRebateRewardsBasisPoint + ); + } + } else if (rewardAmount != 0 && dedicatedSOV < rewardAmount) { + emit EarnRewardFail( + user, + sovTokenAddress, + loanId, + _feeRebatePercent, + rewardAmount, + tradingRebateRewardsBasisPoint + ); + } + } } diff --git a/contracts/mixins/InterestUser.sol b/contracts/mixins/InterestUser.sol index 01d0b45f0..409a87b18 100644 --- a/contracts/mixins/InterestUser.sol +++ b/contracts/mixins/InterestUser.sol @@ -18,60 +18,69 @@ import "./FeesHelper.sol"; * This contract pays loan interests. * */ contract InterestUser is VaultController, FeesHelper { - using SafeERC20 for IERC20; + using SafeERC20 for IERC20; - /// Triggered whenever interest is paid to lender. - event PayInterestTransfer(address indexed interestToken, address indexed lender, uint256 effectiveInterest); + /// Triggered whenever interest is paid to lender. + event PayInterestTransfer( + address indexed interestToken, + address indexed lender, + uint256 effectiveInterest + ); - /** - * @notice Internal function to pay interest of a loan. - * @dev Calls _payInterestTransfer internal function to transfer tokens. - * @param lender The account address of the lender. - * @param interestToken The token address to pay interest with. - * */ - function _payInterest(address lender, address interestToken) internal { - LenderInterest storage lenderInterestLocal = lenderInterest[lender][interestToken]; + /** + * @notice Internal function to pay interest of a loan. + * @dev Calls _payInterestTransfer internal function to transfer tokens. + * @param lender The account address of the lender. + * @param interestToken The token address to pay interest with. + * */ + function _payInterest(address lender, address interestToken) internal { + LenderInterest storage lenderInterestLocal = lenderInterest[lender][interestToken]; - uint256 interestOwedNow = 0; - if (lenderInterestLocal.owedPerDay != 0 && lenderInterestLocal.updatedTimestamp != 0) { - interestOwedNow = block.timestamp.sub(lenderInterestLocal.updatedTimestamp).mul(lenderInterestLocal.owedPerDay).div(1 days); + uint256 interestOwedNow = 0; + if (lenderInterestLocal.owedPerDay != 0 && lenderInterestLocal.updatedTimestamp != 0) { + interestOwedNow = block + .timestamp + .sub(lenderInterestLocal.updatedTimestamp) + .mul(lenderInterestLocal.owedPerDay) + .div(1 days); - lenderInterestLocal.updatedTimestamp = block.timestamp; + lenderInterestLocal.updatedTimestamp = block.timestamp; - if (interestOwedNow > lenderInterestLocal.owedTotal) interestOwedNow = lenderInterestLocal.owedTotal; + if (interestOwedNow > lenderInterestLocal.owedTotal) + interestOwedNow = lenderInterestLocal.owedTotal; - if (interestOwedNow != 0) { - lenderInterestLocal.paidTotal = lenderInterestLocal.paidTotal.add(interestOwedNow); - lenderInterestLocal.owedTotal = lenderInterestLocal.owedTotal.sub(interestOwedNow); + if (interestOwedNow != 0) { + lenderInterestLocal.paidTotal = lenderInterestLocal.paidTotal.add(interestOwedNow); + lenderInterestLocal.owedTotal = lenderInterestLocal.owedTotal.sub(interestOwedNow); - _payInterestTransfer(lender, interestToken, interestOwedNow); - } - } else { - lenderInterestLocal.updatedTimestamp = block.timestamp; - } - } + _payInterestTransfer(lender, interestToken, interestOwedNow); + } + } else { + lenderInterestLocal.updatedTimestamp = block.timestamp; + } + } - /** - * @notice Internal function to transfer tokens for the interest of a loan. - * @param lender The account address of the lender. - * @param interestToken The token address to pay interest with. - * @param interestOwedNow The amount of interest to pay. - * */ - function _payInterestTransfer( - address lender, - address interestToken, - uint256 interestOwedNow - ) internal { - uint256 lendingFee = interestOwedNow.mul(lendingFeePercent).div(10**20); - /// TODO: refactor: data incapsulation violation and DRY design principles - /// uint256 lendingFee = interestOwedNow.mul(lendingFeePercent).divCeil(10**20); is better but produces errors in tests because of this + /** + * @notice Internal function to transfer tokens for the interest of a loan. + * @param lender The account address of the lender. + * @param interestToken The token address to pay interest with. + * @param interestOwedNow The amount of interest to pay. + * */ + function _payInterestTransfer( + address lender, + address interestToken, + uint256 interestOwedNow + ) internal { + uint256 lendingFee = interestOwedNow.mul(lendingFeePercent).div(10**20); + /// TODO: refactor: data incapsulation violation and DRY design principles + /// uint256 lendingFee = interestOwedNow.mul(lendingFeePercent).divCeil(10**20); is better but produces errors in tests because of this - _payLendingFee(lender, interestToken, lendingFee); + _payLendingFee(lender, interestToken, lendingFee); - /// Transfers the interest to the lender, less the interest fee. - vaultWithdraw(interestToken, lender, interestOwedNow.sub(lendingFee)); + /// Transfers the interest to the lender, less the interest fee. + vaultWithdraw(interestToken, lender, interestOwedNow.sub(lendingFee)); - /// Event Log - emit PayInterestTransfer(interestToken, lender, interestOwedNow.sub(lendingFee)); - } + /// Event Log + emit PayInterestTransfer(interestToken, lender, interestOwedNow.sub(lendingFee)); + } } diff --git a/contracts/mixins/LiquidationHelper.sol b/contracts/mixins/LiquidationHelper.sol index bdd423743..859afede3 100644 --- a/contracts/mixins/LiquidationHelper.sol +++ b/contracts/mixins/LiquidationHelper.sol @@ -15,61 +15,61 @@ import "../core/State.sol"; * This contract computes the liquidation amount. * */ contract LiquidationHelper is State { - /** - * @notice Compute how much needs to be liquidated in order to restore the - * desired margin (maintenance + 5%). - * - * @param principal The total borrowed amount (in loan tokens). - * @param collateral The collateral (in collateral tokens). - * @param currentMargin The current margin. - * @param maintenanceMargin The maintenance (minimum) margin. - * @param collateralToLoanRate The exchange rate from collateral to loan - * tokens. - * - * @return maxLiquidatable The collateral you can get liquidating. - * @return maxSeizable The loan you available for liquidation. - * @return incentivePercent The discount on collateral. - * */ - function _getLiquidationAmounts( - uint256 principal, - uint256 collateral, - uint256 currentMargin, - uint256 maintenanceMargin, - uint256 collateralToLoanRate - ) - internal - view - returns ( - uint256 maxLiquidatable, - uint256 maxSeizable, - uint256 incentivePercent - ) - { - incentivePercent = liquidationIncentivePercent; - if (currentMargin > maintenanceMargin || collateralToLoanRate == 0) { - return (maxLiquidatable, maxSeizable, incentivePercent); - } else if (currentMargin <= incentivePercent) { - return (principal, collateral, currentMargin); - } + /** + * @notice Compute how much needs to be liquidated in order to restore the + * desired margin (maintenance + 5%). + * + * @param principal The total borrowed amount (in loan tokens). + * @param collateral The collateral (in collateral tokens). + * @param currentMargin The current margin. + * @param maintenanceMargin The maintenance (minimum) margin. + * @param collateralToLoanRate The exchange rate from collateral to loan + * tokens. + * + * @return maxLiquidatable The collateral you can get liquidating. + * @return maxSeizable The loan you available for liquidation. + * @return incentivePercent The discount on collateral. + * */ + function _getLiquidationAmounts( + uint256 principal, + uint256 collateral, + uint256 currentMargin, + uint256 maintenanceMargin, + uint256 collateralToLoanRate + ) + internal + view + returns ( + uint256 maxLiquidatable, + uint256 maxSeizable, + uint256 incentivePercent + ) + { + incentivePercent = liquidationIncentivePercent; + if (currentMargin > maintenanceMargin || collateralToLoanRate == 0) { + return (maxLiquidatable, maxSeizable, incentivePercent); + } else if (currentMargin <= incentivePercent) { + return (principal, collateral, currentMargin); + } - /// 5 percentage points above maintenance. - uint256 desiredMargin = maintenanceMargin.add(5 ether); + /// 5 percentage points above maintenance. + uint256 desiredMargin = maintenanceMargin.add(5 ether); - /// maxLiquidatable = ((1 + desiredMargin)*principal - collateralToLoanRate*collateral) / (desiredMargin - 0.05) - maxLiquidatable = desiredMargin.add(10**20).mul(principal).div(10**20); - maxLiquidatable = maxLiquidatable.sub(collateral.mul(collateralToLoanRate).div(10**18)); - maxLiquidatable = maxLiquidatable.mul(10**20).div(desiredMargin.sub(incentivePercent)); - if (maxLiquidatable > principal) { - maxLiquidatable = principal; - } + /// maxLiquidatable = ((1 + desiredMargin)*principal - collateralToLoanRate*collateral) / (desiredMargin - 0.05) + maxLiquidatable = desiredMargin.add(10**20).mul(principal).div(10**20); + maxLiquidatable = maxLiquidatable.sub(collateral.mul(collateralToLoanRate).div(10**18)); + maxLiquidatable = maxLiquidatable.mul(10**20).div(desiredMargin.sub(incentivePercent)); + if (maxLiquidatable > principal) { + maxLiquidatable = principal; + } - /// maxSeizable = maxLiquidatable * (1 + incentivePercent) / collateralToLoanRate - maxSeizable = maxLiquidatable.mul(incentivePercent.add(10**20)); - maxSeizable = maxSeizable.div(collateralToLoanRate).div(100); - if (maxSeizable > collateral) { - maxSeizable = collateral; - } + /// maxSeizable = maxLiquidatable * (1 + incentivePercent) / collateralToLoanRate + maxSeizable = maxLiquidatable.mul(incentivePercent.add(10**20)); + maxSeizable = maxSeizable.div(collateralToLoanRate).div(100); + if (maxSeizable > collateral) { + maxSeizable = collateral; + } - return (maxLiquidatable, maxSeizable, incentivePercent); - } + return (maxLiquidatable, maxSeizable, incentivePercent); + } } diff --git a/contracts/mixins/ModuleCommonFunctionalities.sol b/contracts/mixins/ModuleCommonFunctionalities.sol index b41904466..adfa9ca6b 100644 --- a/contracts/mixins/ModuleCommonFunctionalities.sol +++ b/contracts/mixins/ModuleCommonFunctionalities.sol @@ -3,8 +3,8 @@ pragma solidity 0.5.17; import "../core/State.sol"; contract ModuleCommonFunctionalities is State { - modifier whenNotPaused() { - require(!pause, "Paused"); - _; - } + modifier whenNotPaused() { + require(!pause, "Paused"); + _; + } } diff --git a/contracts/mixins/ProtocolTokenUser.sol b/contracts/mixins/ProtocolTokenUser.sol index 93889174a..707a7db9c 100644 --- a/contracts/mixins/ProtocolTokenUser.sol +++ b/contracts/mixins/ProtocolTokenUser.sol @@ -16,32 +16,35 @@ import "../openzeppelin/SafeERC20.sol"; * This contract implements functionality to withdraw protocol tokens. * */ contract ProtocolTokenUser is State { - using SafeERC20 for IERC20; - - /** - * @notice Internal function to withdraw an amount of protocol tokens from this contract. - * - * @param receiver The address of the recipient. - * @param amount The amount of tokens to withdraw. - * - * @return The protocol token address. - * @return Withdrawal success (true/false). - * */ - function _withdrawProtocolToken(address receiver, uint256 amount) internal returns (address, bool) { - uint256 withdrawAmount = amount; - - uint256 tokenBalance = protocolTokenHeld; - if (withdrawAmount > tokenBalance) { - withdrawAmount = tokenBalance; - } - if (withdrawAmount == 0) { - return (protocolTokenAddress, false); - } - - protocolTokenHeld = tokenBalance.sub(withdrawAmount); - - IERC20(protocolTokenAddress).safeTransfer(receiver, withdrawAmount); - - return (protocolTokenAddress, true); - } + using SafeERC20 for IERC20; + + /** + * @notice Internal function to withdraw an amount of protocol tokens from this contract. + * + * @param receiver The address of the recipient. + * @param amount The amount of tokens to withdraw. + * + * @return The protocol token address. + * @return Withdrawal success (true/false). + * */ + function _withdrawProtocolToken(address receiver, uint256 amount) + internal + returns (address, bool) + { + uint256 withdrawAmount = amount; + + uint256 tokenBalance = protocolTokenHeld; + if (withdrawAmount > tokenBalance) { + withdrawAmount = tokenBalance; + } + if (withdrawAmount == 0) { + return (protocolTokenAddress, false); + } + + protocolTokenHeld = tokenBalance.sub(withdrawAmount); + + IERC20(protocolTokenAddress).safeTransfer(receiver, withdrawAmount); + + return (protocolTokenAddress, true); + } } diff --git a/contracts/mixins/RewardHelper.sol b/contracts/mixins/RewardHelper.sol index 8cb0d9293..628ff69c5 100644 --- a/contracts/mixins/RewardHelper.sol +++ b/contracts/mixins/RewardHelper.sol @@ -13,29 +13,34 @@ import "../feeds/IPriceFeeds.sol"; * outstanding on it. * */ contract RewardHelper is State { - using SafeMath for uint256; + using SafeMath for uint256; - /** - * @notice Calculate the reward of a rollover transaction. - * - * @param collateralToken The address of the collateral token. - * @param loanToken The address of the loan token. - * @param positionSize The amount of value of the position. - * - * @return The base fee + the flex fee. - */ - function _getRolloverReward( - address collateralToken, - address loanToken, - uint256 positionSize - ) internal view returns (uint256 reward) { - uint256 positionSizeInCollateralToken = IPriceFeeds(priceFeeds).queryReturn(loanToken, collateralToken, positionSize); - uint256 rolloverBaseRewardInCollateralToken = - IPriceFeeds(priceFeeds).queryReturn(address(wrbtcToken), collateralToken, rolloverBaseReward); + /** + * @notice Calculate the reward of a rollover transaction. + * + * @param collateralToken The address of the collateral token. + * @param loanToken The address of the loan token. + * @param positionSize The amount of value of the position. + * + * @return The base fee + the flex fee. + */ + function _getRolloverReward( + address collateralToken, + address loanToken, + uint256 positionSize + ) internal view returns (uint256 reward) { + uint256 positionSizeInCollateralToken = + IPriceFeeds(priceFeeds).queryReturn(loanToken, collateralToken, positionSize); + uint256 rolloverBaseRewardInCollateralToken = + IPriceFeeds(priceFeeds).queryReturn( + address(wrbtcToken), + collateralToken, + rolloverBaseReward + ); - return - rolloverBaseRewardInCollateralToken - .mul(2) /// baseFee - .add(positionSizeInCollateralToken.mul(rolloverFlexFeePercent).div(10**20)); /// flexFee = 0.1% of position size - } + return + rolloverBaseRewardInCollateralToken + .mul(2) /// baseFee + .add(positionSizeInCollateralToken.mul(rolloverFlexFeePercent).div(10**20)); /// flexFee = 0.1% of position size + } } diff --git a/contracts/mixins/VaultController.sol b/contracts/mixins/VaultController.sol index f01054d78..2801fd587 100644 --- a/contracts/mixins/VaultController.sol +++ b/contracts/mixins/VaultController.sol @@ -17,119 +17,119 @@ import "../core/State.sol"; * other tokens from the vault. * */ contract VaultController is State { - using SafeERC20 for IERC20; + using SafeERC20 for IERC20; - event VaultDeposit(address indexed asset, address indexed from, uint256 amount); - event VaultWithdraw(address indexed asset, address indexed to, uint256 amount); + event VaultDeposit(address indexed asset, address indexed from, uint256 amount); + event VaultWithdraw(address indexed asset, address indexed to, uint256 amount); - /** - * @notice Deposit wrBTC into the vault. - * - * @param from The address of the account paying the deposit. - * @param value The amount of wrBTC tokens to transfer. - */ - function vaultEtherDeposit(address from, uint256 value) internal { - IWrbtcERC20 _wrbtcToken = wrbtcToken; - _wrbtcToken.deposit.value(value)(); + /** + * @notice Deposit wrBTC into the vault. + * + * @param from The address of the account paying the deposit. + * @param value The amount of wrBTC tokens to transfer. + */ + function vaultEtherDeposit(address from, uint256 value) internal { + IWrbtcERC20 _wrbtcToken = wrbtcToken; + _wrbtcToken.deposit.value(value)(); - emit VaultDeposit(address(_wrbtcToken), from, value); - } + emit VaultDeposit(address(_wrbtcToken), from, value); + } - /** - * @notice Withdraw wrBTC from the vault. - * - * @param to The address of the recipient. - * @param value The amount of wrBTC tokens to transfer. - */ - function vaultEtherWithdraw(address to, uint256 value) internal { - if (value != 0) { - IWrbtcERC20 _wrbtcToken = wrbtcToken; - uint256 balance = address(this).balance; - if (value > balance) { - _wrbtcToken.withdraw(value - balance); - } - Address.sendValue(to, value); + /** + * @notice Withdraw wrBTC from the vault. + * + * @param to The address of the recipient. + * @param value The amount of wrBTC tokens to transfer. + */ + function vaultEtherWithdraw(address to, uint256 value) internal { + if (value != 0) { + IWrbtcERC20 _wrbtcToken = wrbtcToken; + uint256 balance = address(this).balance; + if (value > balance) { + _wrbtcToken.withdraw(value - balance); + } + Address.sendValue(to, value); - emit VaultWithdraw(address(_wrbtcToken), to, value); - } - } + emit VaultWithdraw(address(_wrbtcToken), to, value); + } + } - /** - * @notice Deposit tokens into the vault. - * - * @param token The address of the token instance. - * @param from The address of the account paying the deposit. - * @param value The amount of tokens to transfer. - */ - function vaultDeposit( - address token, - address from, - uint256 value - ) internal { - if (value != 0) { - IERC20(token).safeTransferFrom(from, address(this), value); + /** + * @notice Deposit tokens into the vault. + * + * @param token The address of the token instance. + * @param from The address of the account paying the deposit. + * @param value The amount of tokens to transfer. + */ + function vaultDeposit( + address token, + address from, + uint256 value + ) internal { + if (value != 0) { + IERC20(token).safeTransferFrom(from, address(this), value); - emit VaultDeposit(token, from, value); - } - } + emit VaultDeposit(token, from, value); + } + } - /** - * @notice Withdraw tokens from the vault. - * - * @param token The address of the token instance. - * @param to The address of the recipient. - * @param value The amount of tokens to transfer. - */ - function vaultWithdraw( - address token, - address to, - uint256 value - ) internal { - if (value != 0) { - IERC20(token).safeTransfer(to, value); + /** + * @notice Withdraw tokens from the vault. + * + * @param token The address of the token instance. + * @param to The address of the recipient. + * @param value The amount of tokens to transfer. + */ + function vaultWithdraw( + address token, + address to, + uint256 value + ) internal { + if (value != 0) { + IERC20(token).safeTransfer(to, value); - emit VaultWithdraw(token, to, value); - } - } + emit VaultWithdraw(token, to, value); + } + } - /** - * @notice Transfer tokens from an account into another one. - * - * @param token The address of the token instance. - * @param from The address of the account paying. - * @param to The address of the recipient. - * @param value The amount of tokens to transfer. - */ - function vaultTransfer( - address token, - address from, - address to, - uint256 value - ) internal { - if (value != 0) { - if (from == address(this)) { - IERC20(token).safeTransfer(to, value); - } else { - IERC20(token).safeTransferFrom(from, to, value); - } - } - } + /** + * @notice Transfer tokens from an account into another one. + * + * @param token The address of the token instance. + * @param from The address of the account paying. + * @param to The address of the recipient. + * @param value The amount of tokens to transfer. + */ + function vaultTransfer( + address token, + address from, + address to, + uint256 value + ) internal { + if (value != 0) { + if (from == address(this)) { + IERC20(token).safeTransfer(to, value); + } else { + IERC20(token).safeTransferFrom(from, to, value); + } + } + } - /** - * @notice Approve an allowance of tokens to be spent by an account. - * - * @param token The address of the token instance. - * @param to The address of the spender. - * @param value The amount of tokens to allow. - */ - function vaultApprove( - address token, - address to, - uint256 value - ) internal { - if (value != 0 && IERC20(token).allowance(address(this), to) != 0) { - IERC20(token).safeApprove(to, 0); - } - IERC20(token).safeApprove(to, value); - } + /** + * @notice Approve an allowance of tokens to be spent by an account. + * + * @param token The address of the token instance. + * @param to The address of the spender. + * @param value The amount of tokens to allow. + */ + function vaultApprove( + address token, + address to, + uint256 value + ) internal { + if (value != 0 && IERC20(token).allowance(address(this), to) != 0) { + IERC20(token).safeApprove(to, 0); + } + IERC20(token).safeApprove(to, value); + } } diff --git a/contracts/mockup/BProPriceFeedMockup.sol b/contracts/mockup/BProPriceFeedMockup.sol index 30c1e5497..c3141ee6b 100644 --- a/contracts/mockup/BProPriceFeedMockup.sol +++ b/contracts/mockup/BProPriceFeedMockup.sol @@ -1,17 +1,17 @@ pragma solidity 0.5.17; contract BProPriceFeedMockup { - uint256 public value; + uint256 public value; - /** - * @dev BPro USD PRICE - * @return the BPro USD Price [using mocPrecision] - */ - function bproUsdPrice() public view returns (uint256) { - return value; - } + /** + * @dev BPro USD PRICE + * @return the BPro USD Price [using mocPrecision] + */ + function bproUsdPrice() public view returns (uint256) { + return value; + } - function setValue(uint256 _value) public { - value = _value; - } + function setValue(uint256 _value) public { + value = _value; + } } diff --git a/contracts/mockup/BlockMockUp.sol b/contracts/mockup/BlockMockUp.sol index 5b850e748..cda554629 100644 --- a/contracts/mockup/BlockMockUp.sol +++ b/contracts/mockup/BlockMockUp.sol @@ -4,21 +4,21 @@ pragma solidity 0.5.17; * @title Used to get and set mock block number. */ contract BlockMockUp { - uint256 public blockNum; + uint256 public blockNum; - /** - * @notice To get the `blockNum`. - * @return _blockNum The block number. - */ - function getBlockNum() public view returns (uint256 _blockNum) { - return blockNum; - } + /** + * @notice To get the `blockNum`. + * @return _blockNum The block number. + */ + function getBlockNum() public view returns (uint256 _blockNum) { + return blockNum; + } - /** - * @notice To set the `blockNum`. - * @param _blockNum The block number. - */ - function setBlockNum(uint256 _blockNum) public { - blockNum = _blockNum; - } + /** + * @notice To set the `blockNum`. + * @param _blockNum The block number. + */ + function setBlockNum(uint256 _blockNum) public { + blockNum = _blockNum; + } } diff --git a/contracts/mockup/DummyContract.sol b/contracts/mockup/DummyContract.sol index 66b17860c..5343b4486 100644 --- a/contracts/mockup/DummyContract.sol +++ b/contracts/mockup/DummyContract.sol @@ -1,9 +1,9 @@ pragma solidity ^0.5.17; contract DummyContract { - function approveTokens( - address cSOV1, - address cSOV2, - address SOV - ) public {} + function approveTokens( + address cSOV1, + address cSOV2, + address SOV + ) public {} } diff --git a/contracts/mockup/FeeSharingProxyMockup.sol b/contracts/mockup/FeeSharingProxyMockup.sol index 82eed6ecb..b91500dc4 100644 --- a/contracts/mockup/FeeSharingProxyMockup.sol +++ b/contracts/mockup/FeeSharingProxyMockup.sol @@ -3,37 +3,41 @@ pragma solidity ^0.5.17; import "../governance/FeeSharingProxy/FeeSharingLogic.sol"; contract FeeSharingProxyMockup is FeeSharingLogic { - struct TestData { - address loanPoolToken; - uint32 maxCheckpoints; - address receiver; - } + struct TestData { + address loanPoolToken; + uint32 maxCheckpoints; + address receiver; + } - TestData public testData; + TestData public testData; - constructor(IProtocol _protocol, IStaking _staking) public { - protocol = _protocol; - staking = _staking; - } + constructor(IProtocol _protocol, IStaking _staking) public { + protocol = _protocol; + staking = _staking; + } - function withdraw( - address _loanPoolToken, - uint32 _maxCheckpoints, - address _receiver - ) public { - testData = TestData(_loanPoolToken, _maxCheckpoints, _receiver); - } + function withdraw( + address _loanPoolToken, + uint32 _maxCheckpoints, + address _receiver + ) public { + testData = TestData(_loanPoolToken, _maxCheckpoints, _receiver); + } - function trueWithdraw( - address _loanPoolToken, - uint32 _maxCheckpoints, - address _receiver - ) public { - super.withdraw(_loanPoolToken, _maxCheckpoints, _receiver); - } + function trueWithdraw( + address _loanPoolToken, + uint32 _maxCheckpoints, + address _receiver + ) public { + super.withdraw(_loanPoolToken, _maxCheckpoints, _receiver); + } - function addCheckPoint(address loanPoolToken, uint256 poolTokenAmount) public { - uint96 amount96 = safe96(poolTokenAmount, "FeeSharingProxy::withdrawFees: pool token amount exceeds 96 bits"); - _addCheckpoint(loanPoolToken, amount96); - } + function addCheckPoint(address loanPoolToken, uint256 poolTokenAmount) public { + uint96 amount96 = + safe96( + poolTokenAmount, + "FeeSharingProxy::withdrawFees: pool token amount exceeds 96 bits" + ); + _addCheckpoint(loanPoolToken, amount96); + } } diff --git a/contracts/mockup/GovernorAlphaMockup.sol b/contracts/mockup/GovernorAlphaMockup.sol index 98f48052b..de2f2e14f 100644 --- a/contracts/mockup/GovernorAlphaMockup.sol +++ b/contracts/mockup/GovernorAlphaMockup.sol @@ -4,21 +4,21 @@ pragma experimental ABIEncoderV2; import "../governance/GovernorAlpha.sol"; contract GovernorAlphaMockup is GovernorAlpha { - constructor( - address timelock_, - address staking_, - address guardian_, - uint96 quorumVotes_, - uint96 _minPercentageVotes - ) public GovernorAlpha(timelock_, staking_, guardian_, quorumVotes_, _minPercentageVotes) {} + constructor( + address timelock_, + address staking_, + address guardian_, + uint96 quorumVotes_, + uint96 _minPercentageVotes + ) public GovernorAlpha(timelock_, staking_, guardian_, quorumVotes_, _minPercentageVotes) {} - function votingPeriod() public pure returns (uint256) { - return 10; - } + function votingPeriod() public pure returns (uint256) { + return 10; + } - function queueProposals(uint256[] calldata proposalIds) external { - for (uint256 i = 0; i < proposalIds.length; i++) { - queue(proposalIds[i]); - } - } + function queueProposals(uint256[] calldata proposalIds) external { + for (uint256 i = 0; i < proposalIds.length; i++) { + queue(proposalIds[i]); + } + } } diff --git a/contracts/mockup/LiquidityMiningMockup.sol b/contracts/mockup/LiquidityMiningMockup.sol index 26ea1ad0e..a4d001a25 100644 --- a/contracts/mockup/LiquidityMiningMockup.sol +++ b/contracts/mockup/LiquidityMiningMockup.sol @@ -4,13 +4,17 @@ pragma experimental ABIEncoderV2; import "../farm/LiquidityMining.sol"; contract LiquidityMiningMockup is LiquidityMining { - function getPassedBlocksWithBonusMultiplier(uint256 _from, uint256 _to) public view returns (uint256) { - return _getPassedBlocksWithBonusMultiplier(_from, _to); - } + function getPassedBlocksWithBonusMultiplier(uint256 _from, uint256 _to) + public + view + returns (uint256) + { + return _getPassedBlocksWithBonusMultiplier(_from, _to); + } - function getPoolAccumulatedReward(address _poolToken) public view returns (uint256, uint256) { - uint256 poolId = _getPoolId(_poolToken); - PoolInfo storage pool = poolInfoList[poolId]; - return _getPoolAccumulatedReward(pool); - } + function getPoolAccumulatedReward(address _poolToken) public view returns (uint256, uint256) { + uint256 poolId = _getPoolId(_poolToken); + PoolInfo storage pool = poolInfoList[poolId]; + return _getPoolAccumulatedReward(pool); + } } diff --git a/contracts/mockup/LiquidityPoolV1ConverterMockup.sol b/contracts/mockup/LiquidityPoolV1ConverterMockup.sol index 3be388402..42214f7d3 100644 --- a/contracts/mockup/LiquidityPoolV1ConverterMockup.sol +++ b/contracts/mockup/LiquidityPoolV1ConverterMockup.sol @@ -3,33 +3,33 @@ pragma solidity 0.5.17; import "../interfaces/IERC20.sol"; contract LiquidityPoolV1ConverterMockup { - IERC20[] public reserveTokens; - IERC20 wrbtcToken; - uint256 totalFeeMockupValue; - address feesController; - - constructor(IERC20 _token0, IERC20 _token1) public { - reserveTokens.push(_token0); - reserveTokens.push(_token1); - } - - function setFeesController(address _feesController) public { - feesController = _feesController; - } - - function setWrbtcToken(IERC20 _wrbtcToken) public { - wrbtcToken = _wrbtcToken; - } - - function setTotalFeeMockupValue(uint256 _totalFeeMockupValue) public { - totalFeeMockupValue = _totalFeeMockupValue; - } - - function withdrawFees(address _receiver) external returns (uint256) { - require(msg.sender == feesController, "unauthorized"); - - // transfer wrbtc - wrbtcToken.transfer(_receiver, totalFeeMockupValue); - return totalFeeMockupValue; - } + IERC20[] public reserveTokens; + IERC20 wrbtcToken; + uint256 totalFeeMockupValue; + address feesController; + + constructor(IERC20 _token0, IERC20 _token1) public { + reserveTokens.push(_token0); + reserveTokens.push(_token1); + } + + function setFeesController(address _feesController) public { + feesController = _feesController; + } + + function setWrbtcToken(IERC20 _wrbtcToken) public { + wrbtcToken = _wrbtcToken; + } + + function setTotalFeeMockupValue(uint256 _totalFeeMockupValue) public { + totalFeeMockupValue = _totalFeeMockupValue; + } + + function withdrawFees(address _receiver) external returns (uint256) { + require(msg.sender == feesController, "unauthorized"); + + // transfer wrbtc + wrbtcToken.transfer(_receiver, totalFeeMockupValue); + return totalFeeMockupValue; + } } diff --git a/contracts/mockup/LoanTokenLogicLMMockup.sol b/contracts/mockup/LoanTokenLogicLMMockup.sol index 21711601d..60c1a4e97 100644 --- a/contracts/mockup/LoanTokenLogicLMMockup.sol +++ b/contracts/mockup/LoanTokenLogicLMMockup.sol @@ -4,11 +4,15 @@ pragma experimental ABIEncoderV2; import "../connectors/loantoken/modules/beaconLogicLM/LoanTokenLogicLM.sol"; contract LoanTokenLogicLMMockup is LoanTokenLogicLM { - function burn(address receiver, uint256 burnAmount) external nonReentrant returns (uint256 loanAmountPaid) { - _callOptionalReturn( - 0x2c34D66a5ca8686330e100372Eb3FDFB5aEECD0B, //Random EOA for testing - abi.encodeWithSelector(IERC20(receiver).transfer.selector, receiver, burnAmount), - "error" - ); - } + function burn(address receiver, uint256 burnAmount) + external + nonReentrant + returns (uint256 loanAmountPaid) + { + _callOptionalReturn( + 0x2c34D66a5ca8686330e100372Eb3FDFB5aEECD0B, //Random EOA for testing + abi.encodeWithSelector(IERC20(receiver).transfer.selector, receiver, burnAmount), + "error" + ); + } } diff --git a/contracts/mockup/LoanTokenLogicLMV2Mockup.sol b/contracts/mockup/LoanTokenLogicLMV2Mockup.sol index 973d376ed..623f0ff40 100644 --- a/contracts/mockup/LoanTokenLogicLMV2Mockup.sol +++ b/contracts/mockup/LoanTokenLogicLMV2Mockup.sol @@ -3,124 +3,132 @@ pragma solidity 0.5.17; import "../connectors/loantoken/modules/beaconLogicLM/LoanTokenLogicLM.sol"; contract LoanTokenLogicLMV1Mockup is LoanTokenLogicLM { - function getListFunctionSignatures() external pure returns (bytes4[] memory functionSignatures, bytes32 moduleName) { - bytes4[] memory res = new bytes4[](34); - - // Loan Token Logic Standard - res[0] = this.borrow.selector; - res[1] = this.marginTrade.selector; - res[2] = this.marginTradeAffiliate.selector; - res[3] = this.transfer.selector; - res[4] = this.transferFrom.selector; - res[5] = this.profitOf.selector; - res[6] = this.tokenPrice.selector; - res[7] = this.checkpointPrice.selector; - res[8] = this.marketLiquidity.selector; - res[9] = this.avgBorrowInterestRate.selector; - res[10] = this.borrowInterestRate.selector; - res[11] = this.nextBorrowInterestRate.selector; - res[12] = this.supplyInterestRate.selector; - res[13] = this.nextSupplyInterestRate.selector; - res[14] = this.totalSupplyInterestRate.selector; - res[15] = this.totalAssetBorrow.selector; - res[16] = this.totalAssetSupply.selector; - res[17] = this.getMaxEscrowAmount.selector; - res[18] = this.assetBalanceOf.selector; - res[19] = this.getEstimatedMarginDetails.selector; - res[20] = this.getDepositAmountForBorrow.selector; - res[21] = this.getBorrowAmountForDeposit.selector; - res[22] = this.checkPriceDivergence.selector; - res[23] = this.checkPause.selector; - res[24] = this.setLiquidityMiningAddress.selector; - res[25] = this.calculateSupplyInterestRate.selector; - - // Loan Token LM & OVERLOADING function - /** - * @notice BE CAREFUL, - * LoanTokenLogicStandard also has mint & burn function (overloading). - * You need to compute the function signature manually --> bytes4(keccak256("mint(address,uint256,bool)")) - */ - res[26] = bytes4(keccak256("mint(address,uint256)")); /// LoanTokenLogicStandard - res[27] = bytes4(keccak256("mint(address,uint256,bool)")); /// LoanTokenLogicLM - res[28] = bytes4(keccak256("burn(address,uint256)")); /// LoanTokenLogicStandard - res[29] = bytes4(keccak256("burn(address,uint256,bool)")); /// LoanTokenLogicLM - - // Advanced Token - res[30] = this.approve.selector; - - // Advanced Token Storage - // res[31] = this.totalSupply.selector; - res[31] = this.balanceOf.selector; - res[32] = this.allowance.selector; - - // Loan Token Logic Storage Additional Variable - res[33] = this.getLiquidityMiningAddress.selector; - - return (res, stringToBytes32("LoanTokenLogicLM")); - } + function getListFunctionSignatures() + external + pure + returns (bytes4[] memory functionSignatures, bytes32 moduleName) + { + bytes4[] memory res = new bytes4[](34); + + // Loan Token Logic Standard + res[0] = this.borrow.selector; + res[1] = this.marginTrade.selector; + res[2] = this.marginTradeAffiliate.selector; + res[3] = this.transfer.selector; + res[4] = this.transferFrom.selector; + res[5] = this.profitOf.selector; + res[6] = this.tokenPrice.selector; + res[7] = this.checkpointPrice.selector; + res[8] = this.marketLiquidity.selector; + res[9] = this.avgBorrowInterestRate.selector; + res[10] = this.borrowInterestRate.selector; + res[11] = this.nextBorrowInterestRate.selector; + res[12] = this.supplyInterestRate.selector; + res[13] = this.nextSupplyInterestRate.selector; + res[14] = this.totalSupplyInterestRate.selector; + res[15] = this.totalAssetBorrow.selector; + res[16] = this.totalAssetSupply.selector; + res[17] = this.getMaxEscrowAmount.selector; + res[18] = this.assetBalanceOf.selector; + res[19] = this.getEstimatedMarginDetails.selector; + res[20] = this.getDepositAmountForBorrow.selector; + res[21] = this.getBorrowAmountForDeposit.selector; + res[22] = this.checkPriceDivergence.selector; + res[23] = this.checkPause.selector; + res[24] = this.setLiquidityMiningAddress.selector; + res[25] = this.calculateSupplyInterestRate.selector; + + // Loan Token LM & OVERLOADING function + /** + * @notice BE CAREFUL, + * LoanTokenLogicStandard also has mint & burn function (overloading). + * You need to compute the function signature manually --> bytes4(keccak256("mint(address,uint256,bool)")) + */ + res[26] = bytes4(keccak256("mint(address,uint256)")); /// LoanTokenLogicStandard + res[27] = bytes4(keccak256("mint(address,uint256,bool)")); /// LoanTokenLogicLM + res[28] = bytes4(keccak256("burn(address,uint256)")); /// LoanTokenLogicStandard + res[29] = bytes4(keccak256("burn(address,uint256,bool)")); /// LoanTokenLogicLM + + // Advanced Token + res[30] = this.approve.selector; + + // Advanced Token Storage + // res[31] = this.totalSupply.selector; + res[31] = this.balanceOf.selector; + res[32] = this.allowance.selector; + + // Loan Token Logic Storage Additional Variable + res[33] = this.getLiquidityMiningAddress.selector; + + return (res, stringToBytes32("LoanTokenLogicLM")); + } } contract LoanTokenLogicLMV2Mockup is LoanTokenLogicLM { - function testNewFunction() external pure returns (bool) { - return true; - } - - function getListFunctionSignatures() external pure returns (bytes4[] memory functionSignatures, bytes32 moduleName) { - bytes4[] memory res = new bytes4[](36); - - // Loan Token Logic Standard - res[0] = this.borrow.selector; - res[1] = this.marginTrade.selector; - res[2] = this.marginTradeAffiliate.selector; - res[3] = this.transfer.selector; - res[4] = this.transferFrom.selector; - res[5] = this.profitOf.selector; - res[6] = this.tokenPrice.selector; - res[7] = this.checkpointPrice.selector; - res[8] = this.marketLiquidity.selector; - res[9] = this.avgBorrowInterestRate.selector; - res[10] = this.borrowInterestRate.selector; - res[11] = this.nextBorrowInterestRate.selector; - res[12] = this.supplyInterestRate.selector; - res[13] = this.nextSupplyInterestRate.selector; - res[14] = this.totalSupplyInterestRate.selector; - res[15] = this.totalAssetBorrow.selector; - res[16] = this.totalAssetSupply.selector; - res[17] = this.getMaxEscrowAmount.selector; - res[18] = this.assetBalanceOf.selector; - res[19] = this.getEstimatedMarginDetails.selector; - res[20] = this.getDepositAmountForBorrow.selector; - res[21] = this.getBorrowAmountForDeposit.selector; - res[22] = this.checkPriceDivergence.selector; - res[23] = this.checkPause.selector; - res[24] = this.setLiquidityMiningAddress.selector; - res[25] = this.calculateSupplyInterestRate.selector; - - // Loan Token LM & OVERLOADING function - /** - * @notice BE CAREFUL, - * LoanTokenLogicStandard also has mint & burn function (overloading). - * You need to compute the function signature manually --> bytes4(keccak256("mint(address,uint256,bool)")) - */ - res[26] = bytes4(keccak256("mint(address,uint256)")); /// LoanTokenLogicStandard - res[27] = bytes4(keccak256("mint(address,uint256,bool)")); /// LoanTokenLogicLM - res[28] = bytes4(keccak256("burn(address,uint256)")); /// LoanTokenLogicStandard - res[29] = bytes4(keccak256("burn(address,uint256,bool)")); /// LoanTokenLogicLM - - // Advanced Token - res[30] = this.approve.selector; - - // Advanced Token Storage - res[31] = this.totalSupply.selector; - res[32] = this.balanceOf.selector; - res[33] = this.allowance.selector; - - // Loan Token Logic Storage Additional Variable - res[34] = this.getLiquidityMiningAddress.selector; - - // Mockup - res[35] = this.testNewFunction.selector; - - return (res, stringToBytes32("LoanTokenLogicLM")); - } + function testNewFunction() external pure returns (bool) { + return true; + } + + function getListFunctionSignatures() + external + pure + returns (bytes4[] memory functionSignatures, bytes32 moduleName) + { + bytes4[] memory res = new bytes4[](36); + + // Loan Token Logic Standard + res[0] = this.borrow.selector; + res[1] = this.marginTrade.selector; + res[2] = this.marginTradeAffiliate.selector; + res[3] = this.transfer.selector; + res[4] = this.transferFrom.selector; + res[5] = this.profitOf.selector; + res[6] = this.tokenPrice.selector; + res[7] = this.checkpointPrice.selector; + res[8] = this.marketLiquidity.selector; + res[9] = this.avgBorrowInterestRate.selector; + res[10] = this.borrowInterestRate.selector; + res[11] = this.nextBorrowInterestRate.selector; + res[12] = this.supplyInterestRate.selector; + res[13] = this.nextSupplyInterestRate.selector; + res[14] = this.totalSupplyInterestRate.selector; + res[15] = this.totalAssetBorrow.selector; + res[16] = this.totalAssetSupply.selector; + res[17] = this.getMaxEscrowAmount.selector; + res[18] = this.assetBalanceOf.selector; + res[19] = this.getEstimatedMarginDetails.selector; + res[20] = this.getDepositAmountForBorrow.selector; + res[21] = this.getBorrowAmountForDeposit.selector; + res[22] = this.checkPriceDivergence.selector; + res[23] = this.checkPause.selector; + res[24] = this.setLiquidityMiningAddress.selector; + res[25] = this.calculateSupplyInterestRate.selector; + + // Loan Token LM & OVERLOADING function + /** + * @notice BE CAREFUL, + * LoanTokenLogicStandard also has mint & burn function (overloading). + * You need to compute the function signature manually --> bytes4(keccak256("mint(address,uint256,bool)")) + */ + res[26] = bytes4(keccak256("mint(address,uint256)")); /// LoanTokenLogicStandard + res[27] = bytes4(keccak256("mint(address,uint256,bool)")); /// LoanTokenLogicLM + res[28] = bytes4(keccak256("burn(address,uint256)")); /// LoanTokenLogicStandard + res[29] = bytes4(keccak256("burn(address,uint256,bool)")); /// LoanTokenLogicLM + + // Advanced Token + res[30] = this.approve.selector; + + // Advanced Token Storage + res[31] = this.totalSupply.selector; + res[32] = this.balanceOf.selector; + res[33] = this.allowance.selector; + + // Loan Token Logic Storage Additional Variable + res[34] = this.getLiquidityMiningAddress.selector; + + // Mockup + res[35] = this.testNewFunction.selector; + + return (res, stringToBytes32("LoanTokenLogicLM")); + } } diff --git a/contracts/mockup/LockedSOVMockup.sol b/contracts/mockup/LockedSOVMockup.sol index a7f3909a1..ecba19946 100644 --- a/contracts/mockup/LockedSOVMockup.sol +++ b/contracts/mockup/LockedSOVMockup.sol @@ -9,171 +9,178 @@ import "../interfaces/IERC20.sol"; * @dev This is not a complete mockup of the Locked SOV Contract. */ contract LockedSOVMockup { - using SafeMath for uint256; - - /* Storage */ - - /// @notice The SOV token contract. - IERC20 public SOV; - - /// @notice The locked user balances. - mapping(address => uint256) lockedBalances; - /// @notice The unlocked user balances. - mapping(address => uint256) unlockedBalances; - /// @notice The contracts/wallets with admin power. - mapping(address => bool) isAdmin; - - /* Events */ - - /// @notice Emitted when a new Admin is added to the admin list. - /// @param _initiator The address which initiated this event to be emitted. - /// @param _newAdmin The address of the new admin. - event AdminAdded(address indexed _initiator, address indexed _newAdmin); - - /// @notice Emitted when an admin is removed from the admin list. - /// @param _initiator The address which initiated this event to be emitted. - /// @param _removedAdmin The address of the removed admin. - event AdminRemoved(address indexed _initiator, address indexed _removedAdmin); - - event Deposited(address indexed _initiator, address indexed _userAddress, uint256 _sovAmount, uint256 _basisPoint); - - event Withdrawn(address indexed _initiator, address indexed _userAddress, uint256 _sovAmount); - - event TokensStaked(address indexed _initiator, address indexed _vesting, uint256 _amount); - - /* Modifiers */ - - modifier onlyAdmin { - require(isAdmin[msg.sender], "Only admin can call this."); - _; - } - - /* Functions */ - - /** - * @notice Setup the required parameters. - * @param _SOV The SOV token address. - * @param _admins The list of admins to be added. - */ - constructor(address _SOV, address[] memory _admins) public { - require(_SOV != address(0), "Invalid SOV Address."); - SOV = IERC20(_SOV); - for (uint256 index = 0; index < _admins.length; index++) { - isAdmin[_admins[index]] = true; - } - } - - /** - * @notice The function to add a new admin. - * @param _newAdmin The address of the new admin. - */ - function addAdmin(address _newAdmin) public onlyAdmin { - require(_newAdmin != address(0), "Invalid Address"); - require(!isAdmin[_newAdmin], "Address is already admin"); - isAdmin[_newAdmin] = true; - - emit AdminAdded(msg.sender, _newAdmin); - } - - /** - * @notice The function to remove an admin. - * @param _adminToRemove The address of the admin which should be removed. - */ - function removeAdmin(address _adminToRemove) public onlyAdmin { - require(isAdmin[_adminToRemove], "Address is not an admin"); - isAdmin[_adminToRemove] = false; - - emit AdminRemoved(msg.sender, _adminToRemove); - } - - /** - * @notice Adds SOV to the user balance (Locked and Unlocked Balance based on `_basisPoint`). - * @param _userAddress The user whose locked balance has to be updated with `_sovAmount`. - * @param _sovAmount The amount of SOV to be added to the locked and/or unlocked balance. - * @param _basisPoint The % (in Basis Point)which determines how much will be unlocked immediately. - */ - function deposit( - address _userAddress, - uint256 _sovAmount, - uint256 _basisPoint - ) external { - _deposit(_userAddress, _sovAmount, _basisPoint); - } - - /** - * @notice Adds SOV to the locked balance of a user. - * @param _userAddress The user whose locked balance has to be updated with _sovAmount. - * @param _sovAmount The amount of SOV to be added to the locked balance. - * @dev This is here because there are dependency with other contracts. - */ - function depositSOV(address _userAddress, uint256 _sovAmount) external { - _deposit(_userAddress, _sovAmount, 0); - } - - function _deposit( - address _userAddress, - uint256 _sovAmount, - uint256 _basisPoint - ) private { - // 10000 is not included because if 100% is unlocked, then LockedSOV is not required to be used. - require(_basisPoint < 10000, "Basis Point has to be less than 10000."); - bool txStatus = SOV.transferFrom(msg.sender, address(this), _sovAmount); - require(txStatus, "Token transfer was not successful. Check receiver address."); - - uint256 unlockedBal = _sovAmount.mul(_basisPoint).div(10000); - - unlockedBalances[_userAddress] = unlockedBalances[_userAddress].add(unlockedBal); - lockedBalances[_userAddress] = lockedBalances[_userAddress].add(_sovAmount).sub(unlockedBal); - - emit Deposited(msg.sender, _userAddress, _sovAmount, _basisPoint); - } - - /** - * @notice Withdraws unlocked tokens and Stakes Locked tokens for a user who already have a vesting created. - * @param _userAddress The address of user tokens will be withdrawn. - */ - function withdrawAndStakeTokensFrom(address _userAddress) external { - _withdraw(_userAddress, _userAddress); - _createVestingAndStake(_userAddress); - } - - function _withdraw(address _sender, address _receiverAddress) private { - address userAddr = _receiverAddress; - if (_receiverAddress == address(0)) { - userAddr = _sender; - } - - uint256 amount = unlockedBalances[_sender]; - unlockedBalances[_sender] = 0; - - bool txStatus = SOV.transfer(userAddr, amount); - require(txStatus, "Token transfer was not successful. Check receiver address."); - - emit Withdrawn(_sender, userAddr, amount); - } - - function _createVestingAndStake(address _sender) private { - uint256 amount = lockedBalances[_sender]; - lockedBalances[_sender] = 0; - - emit TokensStaked(_sender, address(0), amount); - } - - /** - * @notice The function to get the locked balance of a user. - * @param _addr The address of the user to check the locked balance. - * @return _balance The locked balance of the address `_addr`. - */ - function getLockedBalance(address _addr) public view returns (uint256 _balance) { - return lockedBalances[_addr]; - } - - /** - * @notice The function to get the unlocked balance of a user. - * @param _addr The address of the user to check the unlocked balance. - * @return _balance The unlocked balance of the address `_addr`. - */ - function getUnlockedBalance(address _addr) external view returns (uint256 _balance) { - return unlockedBalances[_addr]; - } + using SafeMath for uint256; + + /* Storage */ + + /// @notice The SOV token contract. + IERC20 public SOV; + + /// @notice The locked user balances. + mapping(address => uint256) lockedBalances; + /// @notice The unlocked user balances. + mapping(address => uint256) unlockedBalances; + /// @notice The contracts/wallets with admin power. + mapping(address => bool) isAdmin; + + /* Events */ + + /// @notice Emitted when a new Admin is added to the admin list. + /// @param _initiator The address which initiated this event to be emitted. + /// @param _newAdmin The address of the new admin. + event AdminAdded(address indexed _initiator, address indexed _newAdmin); + + /// @notice Emitted when an admin is removed from the admin list. + /// @param _initiator The address which initiated this event to be emitted. + /// @param _removedAdmin The address of the removed admin. + event AdminRemoved(address indexed _initiator, address indexed _removedAdmin); + + event Deposited( + address indexed _initiator, + address indexed _userAddress, + uint256 _sovAmount, + uint256 _basisPoint + ); + + event Withdrawn(address indexed _initiator, address indexed _userAddress, uint256 _sovAmount); + + event TokensStaked(address indexed _initiator, address indexed _vesting, uint256 _amount); + + /* Modifiers */ + + modifier onlyAdmin { + require(isAdmin[msg.sender], "Only admin can call this."); + _; + } + + /* Functions */ + + /** + * @notice Setup the required parameters. + * @param _SOV The SOV token address. + * @param _admins The list of admins to be added. + */ + constructor(address _SOV, address[] memory _admins) public { + require(_SOV != address(0), "Invalid SOV Address."); + SOV = IERC20(_SOV); + for (uint256 index = 0; index < _admins.length; index++) { + isAdmin[_admins[index]] = true; + } + } + + /** + * @notice The function to add a new admin. + * @param _newAdmin The address of the new admin. + */ + function addAdmin(address _newAdmin) public onlyAdmin { + require(_newAdmin != address(0), "Invalid Address"); + require(!isAdmin[_newAdmin], "Address is already admin"); + isAdmin[_newAdmin] = true; + + emit AdminAdded(msg.sender, _newAdmin); + } + + /** + * @notice The function to remove an admin. + * @param _adminToRemove The address of the admin which should be removed. + */ + function removeAdmin(address _adminToRemove) public onlyAdmin { + require(isAdmin[_adminToRemove], "Address is not an admin"); + isAdmin[_adminToRemove] = false; + + emit AdminRemoved(msg.sender, _adminToRemove); + } + + /** + * @notice Adds SOV to the user balance (Locked and Unlocked Balance based on `_basisPoint`). + * @param _userAddress The user whose locked balance has to be updated with `_sovAmount`. + * @param _sovAmount The amount of SOV to be added to the locked and/or unlocked balance. + * @param _basisPoint The % (in Basis Point)which determines how much will be unlocked immediately. + */ + function deposit( + address _userAddress, + uint256 _sovAmount, + uint256 _basisPoint + ) external { + _deposit(_userAddress, _sovAmount, _basisPoint); + } + + /** + * @notice Adds SOV to the locked balance of a user. + * @param _userAddress The user whose locked balance has to be updated with _sovAmount. + * @param _sovAmount The amount of SOV to be added to the locked balance. + * @dev This is here because there are dependency with other contracts. + */ + function depositSOV(address _userAddress, uint256 _sovAmount) external { + _deposit(_userAddress, _sovAmount, 0); + } + + function _deposit( + address _userAddress, + uint256 _sovAmount, + uint256 _basisPoint + ) private { + // 10000 is not included because if 100% is unlocked, then LockedSOV is not required to be used. + require(_basisPoint < 10000, "Basis Point has to be less than 10000."); + bool txStatus = SOV.transferFrom(msg.sender, address(this), _sovAmount); + require(txStatus, "Token transfer was not successful. Check receiver address."); + + uint256 unlockedBal = _sovAmount.mul(_basisPoint).div(10000); + + unlockedBalances[_userAddress] = unlockedBalances[_userAddress].add(unlockedBal); + lockedBalances[_userAddress] = lockedBalances[_userAddress].add(_sovAmount).sub( + unlockedBal + ); + + emit Deposited(msg.sender, _userAddress, _sovAmount, _basisPoint); + } + + /** + * @notice Withdraws unlocked tokens and Stakes Locked tokens for a user who already have a vesting created. + * @param _userAddress The address of user tokens will be withdrawn. + */ + function withdrawAndStakeTokensFrom(address _userAddress) external { + _withdraw(_userAddress, _userAddress); + _createVestingAndStake(_userAddress); + } + + function _withdraw(address _sender, address _receiverAddress) private { + address userAddr = _receiverAddress; + if (_receiverAddress == address(0)) { + userAddr = _sender; + } + + uint256 amount = unlockedBalances[_sender]; + unlockedBalances[_sender] = 0; + + bool txStatus = SOV.transfer(userAddr, amount); + require(txStatus, "Token transfer was not successful. Check receiver address."); + + emit Withdrawn(_sender, userAddr, amount); + } + + function _createVestingAndStake(address _sender) private { + uint256 amount = lockedBalances[_sender]; + lockedBalances[_sender] = 0; + + emit TokensStaked(_sender, address(0), amount); + } + + /** + * @notice The function to get the locked balance of a user. + * @param _addr The address of the user to check the locked balance. + * @return _balance The locked balance of the address `_addr`. + */ + function getLockedBalance(address _addr) public view returns (uint256 _balance) { + return lockedBalances[_addr]; + } + + /** + * @notice The function to get the unlocked balance of a user. + * @param _addr The address of the user to check the unlocked balance. + * @return _balance The unlocked balance of the address `_addr`. + */ + function getUnlockedBalance(address _addr) external view returns (uint256 _balance) { + return unlockedBalances[_addr]; + } } diff --git a/contracts/mockup/MockAffiliates.sol b/contracts/mockup/MockAffiliates.sol index 30459f838..a3e832374 100644 --- a/contracts/mockup/MockAffiliates.sol +++ b/contracts/mockup/MockAffiliates.sol @@ -3,11 +3,11 @@ pragma solidity 0.5.17; import "../modules/Affiliates.sol"; contract MockAffiliates is Affiliates { - function getAffiliatesUserReferrer(address user) public view returns (address) { - return affiliatesUserReferrer[user]; // REFACTOR: will be useful if affiliatesUserReferrer visibillity is not public - } + function getAffiliatesUserReferrer(address user) public view returns (address) { + return affiliatesUserReferrer[user]; // REFACTOR: will be useful if affiliatesUserReferrer visibillity is not public + } - function initialize(address target) external onlyOwner { - _setTarget(this.getAffiliatesUserReferrer.selector, target); - } + function initialize(address target) external onlyOwner { + _setTarget(this.getAffiliatesUserReferrer.selector, target); + } } diff --git a/contracts/mockup/MockFourYearVestingLogic.sol b/contracts/mockup/MockFourYearVestingLogic.sol index e63abdcb8..dfc442459 100644 --- a/contracts/mockup/MockFourYearVestingLogic.sol +++ b/contracts/mockup/MockFourYearVestingLogic.sol @@ -3,10 +3,10 @@ pragma solidity ^0.5.17; import "../governance/Vesting/fouryear/FourYearVestingLogic.sol"; contract MockFourYearVestingLogic is FourYearVestingLogic { - /** - * @notice gets duration left - */ - function getDurationLeft() external view returns (uint256) { - return durationLeft; - } + /** + * @notice gets duration left + */ + function getDurationLeft() external view returns (uint256) { + return durationLeft; + } } diff --git a/contracts/mockup/MockLoanTokenLogic.sol b/contracts/mockup/MockLoanTokenLogic.sol index 74101d486..0ebe57f84 100644 --- a/contracts/mockup/MockLoanTokenLogic.sol +++ b/contracts/mockup/MockLoanTokenLogic.sol @@ -7,91 +7,99 @@ import "../modules/interfaces/ProtocolAffiliatesInterface.sol"; import "../interfaces/ILoanTokenModules.sol"; contract MockLoanTokenLogic is LoanTokenLogicLM { - /*function getAffiliatesUserReferrer(address user) public view returns (address) { + /*function getAffiliatesUserReferrer(address user) public view returns (address) { return affiliatesUserReferrer[user]; // REFACTOR: will be useful if affiliatesUserReferrer visibillity is not public }*/ - function getListFunctionSignatures() external pure returns (bytes4[] memory functionSignatures, bytes32 moduleName) { - bytes4[] memory res = new bytes4[](38); - - // Loan Token Logic Standard - res[0] = this.borrow.selector; - res[1] = this.marginTrade.selector; - res[2] = this.marginTradeAffiliate.selector; - res[3] = this.transfer.selector; - res[4] = this.transferFrom.selector; - res[5] = this.profitOf.selector; - res[6] = this.tokenPrice.selector; - res[7] = this.checkpointPrice.selector; - res[8] = this.marketLiquidity.selector; - res[9] = this.avgBorrowInterestRate.selector; - res[10] = this.borrowInterestRate.selector; - res[11] = this.nextBorrowInterestRate.selector; - res[12] = this.supplyInterestRate.selector; - res[13] = this.nextSupplyInterestRate.selector; - res[14] = this.totalSupplyInterestRate.selector; - res[15] = this.totalAssetBorrow.selector; - res[16] = this.totalAssetSupply.selector; - res[17] = this.getMaxEscrowAmount.selector; - res[18] = this.assetBalanceOf.selector; - res[19] = this.getEstimatedMarginDetails.selector; - res[20] = this.getDepositAmountForBorrow.selector; - res[21] = this.getBorrowAmountForDeposit.selector; - res[22] = this.checkPriceDivergence.selector; - res[23] = this.checkPause.selector; - res[24] = this.setLiquidityMiningAddress.selector; - res[25] = this.calculateSupplyInterestRate.selector; - - // Loan Token LM & OVERLOADING function - /** - * @notice BE CAREFUL, - * LoanTokenLogicStandard also has mint & burn function (overloading). - * You need to compute the function signature manually --> bytes4(keccak256("mint(address,uint256,bool)")) - */ - - res[26] = bytes4(keccak256("mint(address,uint256)")); /// LoanTokenLogicStandard - res[27] = bytes4(keccak256("mint(address,uint256,bool)")); /// LoanTokenLogicLM - res[28] = bytes4(keccak256("burn(address,uint256)")); /// LoanTokenLogicStandard - res[29] = bytes4(keccak256("burn(address,uint256,bool)")); /// LoanTokenLogicLM - - // Advanced Token - res[30] = this.approve.selector; - - // Advanced Token Storage - res[31] = this.totalSupply.selector; - res[32] = this.balanceOf.selector; - res[33] = this.allowance.selector; - - // Mock - res[34] = this.setAffiliatesReferrer.selector; - res[35] = this.setUserNotFirstTradeFlag.selector; - res[36] = this.getMarginBorrowAmountAndRate.selector; - - // Loan Token Logic Storage Additional Variable - res[37] = this.getLiquidityMiningAddress.selector; - - return (res, stringToBytes32("MockLoanTokenLogic")); - } - - function setAffiliatesReferrer(address user, address referrer) public { - ProtocolAffiliatesInterface(sovrynContractAddress).setAffiliatesReferrer(user, referrer); - } - - function setUserNotFirstTradeFlag(address user) public { - ProtocolAffiliatesInterface(sovrynContractAddress).setUserNotFirstTradeFlag(user); - } - - function getMarginBorrowAmountAndRate(uint256 leverageAmount, uint256 depositAmount) public view returns (uint256, uint256) { - return _getMarginBorrowAmountAndRate(leverageAmount, depositAmount); - } - - /*function initialize(address target) external onlyOwner { + function getListFunctionSignatures() + external + pure + returns (bytes4[] memory functionSignatures, bytes32 moduleName) + { + bytes4[] memory res = new bytes4[](38); + + // Loan Token Logic Standard + res[0] = this.borrow.selector; + res[1] = this.marginTrade.selector; + res[2] = this.marginTradeAffiliate.selector; + res[3] = this.transfer.selector; + res[4] = this.transferFrom.selector; + res[5] = this.profitOf.selector; + res[6] = this.tokenPrice.selector; + res[7] = this.checkpointPrice.selector; + res[8] = this.marketLiquidity.selector; + res[9] = this.avgBorrowInterestRate.selector; + res[10] = this.borrowInterestRate.selector; + res[11] = this.nextBorrowInterestRate.selector; + res[12] = this.supplyInterestRate.selector; + res[13] = this.nextSupplyInterestRate.selector; + res[14] = this.totalSupplyInterestRate.selector; + res[15] = this.totalAssetBorrow.selector; + res[16] = this.totalAssetSupply.selector; + res[17] = this.getMaxEscrowAmount.selector; + res[18] = this.assetBalanceOf.selector; + res[19] = this.getEstimatedMarginDetails.selector; + res[20] = this.getDepositAmountForBorrow.selector; + res[21] = this.getBorrowAmountForDeposit.selector; + res[22] = this.checkPriceDivergence.selector; + res[23] = this.checkPause.selector; + res[24] = this.setLiquidityMiningAddress.selector; + res[25] = this.calculateSupplyInterestRate.selector; + + // Loan Token LM & OVERLOADING function + /** + * @notice BE CAREFUL, + * LoanTokenLogicStandard also has mint & burn function (overloading). + * You need to compute the function signature manually --> bytes4(keccak256("mint(address,uint256,bool)")) + */ + + res[26] = bytes4(keccak256("mint(address,uint256)")); /// LoanTokenLogicStandard + res[27] = bytes4(keccak256("mint(address,uint256,bool)")); /// LoanTokenLogicLM + res[28] = bytes4(keccak256("burn(address,uint256)")); /// LoanTokenLogicStandard + res[29] = bytes4(keccak256("burn(address,uint256,bool)")); /// LoanTokenLogicLM + + // Advanced Token + res[30] = this.approve.selector; + + // Advanced Token Storage + res[31] = this.totalSupply.selector; + res[32] = this.balanceOf.selector; + res[33] = this.allowance.selector; + + // Mock + res[34] = this.setAffiliatesReferrer.selector; + res[35] = this.setUserNotFirstTradeFlag.selector; + res[36] = this.getMarginBorrowAmountAndRate.selector; + + // Loan Token Logic Storage Additional Variable + res[37] = this.getLiquidityMiningAddress.selector; + + return (res, stringToBytes32("MockLoanTokenLogic")); + } + + function setAffiliatesReferrer(address user, address referrer) public { + ProtocolAffiliatesInterface(sovrynContractAddress).setAffiliatesReferrer(user, referrer); + } + + function setUserNotFirstTradeFlag(address user) public { + ProtocolAffiliatesInterface(sovrynContractAddress).setUserNotFirstTradeFlag(user); + } + + function getMarginBorrowAmountAndRate(uint256 leverageAmount, uint256 depositAmount) + public + view + returns (uint256, uint256) + { + return _getMarginBorrowAmountAndRate(leverageAmount, depositAmount); + } + + /*function initialize(address target) external onlyOwner { _setTarget(this.setAffiliatesUserReferrer.selector, target); }*/ } contract ILoanTokenModulesMock is ILoanTokenModules { - function setAffiliatesReferrer(address user, address referrer) external; + function setAffiliatesReferrer(address user, address referrer) external; - function setUserNotFirstTradeFlag(address user) external; + function setUserNotFirstTradeFlag(address user) external; } diff --git a/contracts/mockup/PriceFeedRSKOracleMockup.sol b/contracts/mockup/PriceFeedRSKOracleMockup.sol index 7e3922e87..d0eaad881 100644 --- a/contracts/mockup/PriceFeedRSKOracleMockup.sol +++ b/contracts/mockup/PriceFeedRSKOracleMockup.sol @@ -1,18 +1,18 @@ pragma solidity 0.5.17; contract PriceFeedRSKOracleMockup { - uint256 public value; - bool public has; + uint256 public value; + bool public has; - function getPricing() public view returns (uint256, uint256) { - return (value, block.timestamp); - } + function getPricing() public view returns (uint256, uint256) { + return (value, block.timestamp); + } - function setValue(uint256 _value) public { - value = _value; - } + function setValue(uint256 _value) public { + value = _value; + } - function setHas(bool _has) public { - has = _has; - } + function setHas(bool _has) public { + has = _has; + } } diff --git a/contracts/mockup/PriceFeedsMoCMockup.sol b/contracts/mockup/PriceFeedsMoCMockup.sol index c11fdd62c..201b7b4d6 100644 --- a/contracts/mockup/PriceFeedsMoCMockup.sol +++ b/contracts/mockup/PriceFeedsMoCMockup.sol @@ -5,18 +5,18 @@ import "../feeds/testnet/PriceFeedsMoC.sol"; // This contract is only for test purposes // https://github.com/money-on-chain/Amphiraos-Oracle/blob/master/contracts/medianizer/medianizer.sol contract PriceFeedsMoCMockup is Medianizer { - uint256 public value; - bool public has; + uint256 public value; + bool public has; - function peek() external view returns (bytes32, bool) { - return (bytes32(value), has); - } + function peek() external view returns (bytes32, bool) { + return (bytes32(value), has); + } - function setValue(uint256 _value) public { - value = _value; - } + function setValue(uint256 _value) public { + value = _value; + } - function setHas(bool _has) public { - has = _has; - } + function setHas(bool _has) public { + has = _has; + } } diff --git a/contracts/mockup/ProtocolSettingsMockup.sol b/contracts/mockup/ProtocolSettingsMockup.sol index 696c782b1..05232ce13 100644 --- a/contracts/mockup/ProtocolSettingsMockup.sol +++ b/contracts/mockup/ProtocolSettingsMockup.sol @@ -3,65 +3,65 @@ pragma solidity 0.5.17; import "../modules/ProtocolSettings.sol"; contract ProtocolSettingsMockup is ProtocolSettings { - function setLendingFeeTokensHeld(address token, uint256 amout) public { - lendingFeeTokensHeld[token] = amout; - } + function setLendingFeeTokensHeld(address token, uint256 amout) public { + lendingFeeTokensHeld[token] = amout; + } - function setTradingFeeTokensHeld(address token, uint256 amout) public { - tradingFeeTokensHeld[token] = amout; - } + function setTradingFeeTokensHeld(address token, uint256 amout) public { + tradingFeeTokensHeld[token] = amout; + } - function setBorrowingFeeTokensHeld(address token, uint256 amout) public { - borrowingFeeTokensHeld[token] = amout; - } + function setBorrowingFeeTokensHeld(address token, uint256 amout) public { + borrowingFeeTokensHeld[token] = amout; + } - function initialize(address target) external onlyOwner { - _setTarget(this.setPriceFeedContract.selector, target); - _setTarget(this.setSwapsImplContract.selector, target); - _setTarget(this.setLoanPool.selector, target); - _setTarget(this.setSupportedTokens.selector, target); - _setTarget(this.setLendingFeePercent.selector, target); - _setTarget(this.setTradingFeePercent.selector, target); - _setTarget(this.setBorrowingFeePercent.selector, target); - _setTarget(this.setSwapExternalFeePercent.selector, target); - _setTarget(this.setAffiliateFeePercent.selector, target); - _setTarget(this.setAffiliateTradingTokenFeePercent.selector, target); - _setTarget(this.setLiquidationIncentivePercent.selector, target); - _setTarget(this.setMaxDisagreement.selector, target); - _setTarget(this.setSourceBuffer.selector, target); - _setTarget(this.setMaxSwapSize.selector, target); - _setTarget(this.setFeesController.selector, target); - _setTarget(this.withdrawFees.selector, target); - _setTarget(this.withdrawLendingFees.selector, target); - _setTarget(this.withdrawTradingFees.selector, target); - _setTarget(this.withdrawBorrowingFees.selector, target); - _setTarget(this.withdrawProtocolToken.selector, target); - _setTarget(this.depositProtocolToken.selector, target); - _setTarget(this.getLoanPoolsList.selector, target); - _setTarget(this.isLoanPool.selector, target); - _setTarget(this.setSovrynSwapContractRegistryAddress.selector, target); - _setTarget(this.setWrbtcToken.selector, target); - _setTarget(this.setSovrynProtocolAddress.selector, target); - _setTarget(this.setProtocolTokenAddress.selector, target); - _setTarget(this.setSOVTokenAddress.selector, target); - _setTarget(this.setLockedSOVAddress.selector, target); - _setTarget(this.setMinReferralsToPayoutAffiliates.selector, target); - _setTarget(this.setRolloverBaseReward.selector, target); + function initialize(address target) external onlyOwner { + _setTarget(this.setPriceFeedContract.selector, target); + _setTarget(this.setSwapsImplContract.selector, target); + _setTarget(this.setLoanPool.selector, target); + _setTarget(this.setSupportedTokens.selector, target); + _setTarget(this.setLendingFeePercent.selector, target); + _setTarget(this.setTradingFeePercent.selector, target); + _setTarget(this.setBorrowingFeePercent.selector, target); + _setTarget(this.setSwapExternalFeePercent.selector, target); + _setTarget(this.setAffiliateFeePercent.selector, target); + _setTarget(this.setAffiliateTradingTokenFeePercent.selector, target); + _setTarget(this.setLiquidationIncentivePercent.selector, target); + _setTarget(this.setMaxDisagreement.selector, target); + _setTarget(this.setSourceBuffer.selector, target); + _setTarget(this.setMaxSwapSize.selector, target); + _setTarget(this.setFeesController.selector, target); + _setTarget(this.withdrawFees.selector, target); + _setTarget(this.withdrawLendingFees.selector, target); + _setTarget(this.withdrawTradingFees.selector, target); + _setTarget(this.withdrawBorrowingFees.selector, target); + _setTarget(this.withdrawProtocolToken.selector, target); + _setTarget(this.depositProtocolToken.selector, target); + _setTarget(this.getLoanPoolsList.selector, target); + _setTarget(this.isLoanPool.selector, target); + _setTarget(this.setSovrynSwapContractRegistryAddress.selector, target); + _setTarget(this.setWrbtcToken.selector, target); + _setTarget(this.setSovrynProtocolAddress.selector, target); + _setTarget(this.setProtocolTokenAddress.selector, target); + _setTarget(this.setSOVTokenAddress.selector, target); + _setTarget(this.setLockedSOVAddress.selector, target); + _setTarget(this.setMinReferralsToPayoutAffiliates.selector, target); + _setTarget(this.setRolloverBaseReward.selector, target); - _setTarget(this.setLendingFeeTokensHeld.selector, target); - _setTarget(this.setTradingFeeTokensHeld.selector, target); - _setTarget(this.setBorrowingFeeTokensHeld.selector, target); - _setTarget(this.getSpecialRebates.selector, target); + _setTarget(this.setLendingFeeTokensHeld.selector, target); + _setTarget(this.setTradingFeeTokensHeld.selector, target); + _setTarget(this.setBorrowingFeeTokensHeld.selector, target); + _setTarget(this.getSpecialRebates.selector, target); - _setTarget(this.getProtocolAddress.selector, target); - _setTarget(this.getSovTokenAddress.selector, target); - _setTarget(this.getLockedSOVAddress.selector, target); + _setTarget(this.getProtocolAddress.selector, target); + _setTarget(this.getSovTokenAddress.selector, target); + _setTarget(this.getLockedSOVAddress.selector, target); - _setTarget(this.getFeeRebatePercent.selector, target); - _setTarget(this.getSwapExternalFeePercent.selector, target); + _setTarget(this.getFeeRebatePercent.selector, target); + _setTarget(this.getSwapExternalFeePercent.selector, target); - _setTarget(this.setTradingRebateRewardsBasisPoint.selector, target); - _setTarget(this.getTradingRebateRewardsBasisPoint.selector, target); - _setTarget(this.getDedicatedSOVRebate.selector, target); - } + _setTarget(this.setTradingRebateRewardsBasisPoint.selector, target); + _setTarget(this.getTradingRebateRewardsBasisPoint.selector, target); + _setTarget(this.getDedicatedSOVRebate.selector, target); + } } diff --git a/contracts/mockup/RBTCWrapperProxyMockup.sol b/contracts/mockup/RBTCWrapperProxyMockup.sol index 8927bf31e..e407c9345 100644 --- a/contracts/mockup/RBTCWrapperProxyMockup.sol +++ b/contracts/mockup/RBTCWrapperProxyMockup.sol @@ -3,21 +3,21 @@ pragma solidity 0.5.17; import "../farm/LiquidityMining.sol"; contract RBTCWrapperProxyMockup { - LiquidityMining public liquidityMining; + LiquidityMining public liquidityMining; - constructor(LiquidityMining _liquidityMining) public { - liquidityMining = _liquidityMining; - } + constructor(LiquidityMining _liquidityMining) public { + liquidityMining = _liquidityMining; + } - function claimReward(address _poolToken) public { - liquidityMining.claimReward(_poolToken, msg.sender); - } + function claimReward(address _poolToken) public { + liquidityMining.claimReward(_poolToken, msg.sender); + } - function claimRewardFromAllPools() public { - liquidityMining.claimRewardFromAllPools(msg.sender); - } + function claimRewardFromAllPools() public { + liquidityMining.claimRewardFromAllPools(msg.sender); + } - function withdraw(address _poolToken, uint256 _amount) public { - liquidityMining.withdraw(_poolToken, _amount, msg.sender); - } + function withdraw(address _poolToken, uint256 _amount) public { + liquidityMining.withdraw(_poolToken, _amount, msg.sender); + } } diff --git a/contracts/mockup/StakingMock.sol b/contracts/mockup/StakingMock.sol index 85ba3dd13..331459f1f 100644 --- a/contracts/mockup/StakingMock.sol +++ b/contracts/mockup/StakingMock.sol @@ -4,22 +4,22 @@ import "../governance/Staking/Staking.sol"; import "./BlockMockUp.sol"; contract StakingMock is Staking { - ///@notice the block mock up contract - BlockMockUp public blockMockUp; + ///@notice the block mock up contract + BlockMockUp public blockMockUp; - /** - * @notice gets block number from BlockMockUp - * @param _blockMockUp the address of BlockMockUp - */ - function setBlockMockUpAddr(address _blockMockUp) public onlyOwner { - require(_blockMockUp != address(0), "block mockup address invalid"); - blockMockUp = BlockMockUp(_blockMockUp); - } + /** + * @notice gets block number from BlockMockUp + * @param _blockMockUp the address of BlockMockUp + */ + function setBlockMockUpAddr(address _blockMockUp) public onlyOwner { + require(_blockMockUp != address(0), "block mockup address invalid"); + blockMockUp = BlockMockUp(_blockMockUp); + } - /** - * @notice Determine the current Block Number from BlockMockUp - * */ - function _getCurrentBlockNumber() internal view returns (uint256) { - return blockMockUp.getBlockNum(); - } + /** + * @notice Determine the current Block Number from BlockMockUp + * */ + function _getCurrentBlockNumber() internal view returns (uint256) { + return blockMockUp.getBlockNum(); + } } diff --git a/contracts/mockup/StakingMockup.sol b/contracts/mockup/StakingMockup.sol index dcdd1c10f..d142e7904 100644 --- a/contracts/mockup/StakingMockup.sol +++ b/contracts/mockup/StakingMockup.sol @@ -3,94 +3,104 @@ pragma solidity ^0.5.17; import "../governance/Staking/Staking.sol"; contract StakingMockup is Staking { - function balanceOf_MultipliedByTwo(address account) external view returns (uint256) { - return balanceOf(account) * 2; - } + function balanceOf_MultipliedByTwo(address account) external view returns (uint256) { + return balanceOf(account) * 2; + } - uint96 priorTotalVotingPower; + uint96 priorTotalVotingPower; - function MOCK_priorTotalVotingPower(uint96 _priorTotalVotingPower) public { - priorTotalVotingPower = _priorTotalVotingPower; - } + function MOCK_priorTotalVotingPower(uint96 _priorTotalVotingPower) public { + priorTotalVotingPower = _priorTotalVotingPower; + } - function getPriorTotalVotingPower(uint32 blockNumber, uint256 time) public view returns (uint96 totalVotingPower) { - return priorTotalVotingPower != 0 ? priorTotalVotingPower : super.getPriorTotalVotingPower(blockNumber, time); - } + function getPriorTotalVotingPower(uint32 blockNumber, uint256 time) + public + view + returns (uint96 totalVotingPower) + { + return + priorTotalVotingPower != 0 + ? priorTotalVotingPower + : super.getPriorTotalVotingPower(blockNumber, time); + } - uint96 priorWeightedStake; + uint96 priorWeightedStake; - function MOCK_priorWeightedStake(uint96 _priorWeightedStake) public { - priorWeightedStake = _priorWeightedStake; - } + function MOCK_priorWeightedStake(uint96 _priorWeightedStake) public { + priorWeightedStake = _priorWeightedStake; + } - function getPriorWeightedStake( - address account, - uint256 blockNumber, - uint256 date - ) public view returns (uint96) { - return priorWeightedStake != 0 ? priorWeightedStake : super.getPriorWeightedStake(account, blockNumber, date); - } + function getPriorWeightedStake( + address account, + uint256 blockNumber, + uint256 date + ) public view returns (uint96) { + return + priorWeightedStake != 0 + ? priorWeightedStake + : super.getPriorWeightedStake(account, blockNumber, date); + } - function calculatePriorWeightedStake( - address account, - uint256 blockNumber, - uint256 date - ) public { - super.getPriorWeightedStake(account, blockNumber, date); - } + function calculatePriorWeightedStake( + address account, + uint256 blockNumber, + uint256 date + ) public { + super.getPriorWeightedStake(account, blockNumber, date); + } - /** - * @dev We need this function to simulate zero delegate checkpoint value. - */ - function setDelegateStake( - address delegatee, - uint256 lockedTS, - uint96 value - ) public { - uint32 nCheckpoints = numDelegateStakingCheckpoints[delegatee][lockedTS]; - uint96 staked = delegateStakingCheckpoints[delegatee][lockedTS][nCheckpoints - 1].stake; - _writeDelegateCheckpoint(delegatee, lockedTS, nCheckpoints, 0); - } + /** + * @dev We need this function to simulate zero delegate checkpoint value. + */ + function setDelegateStake( + address delegatee, + uint256 lockedTS, + uint96 value + ) public { + uint32 nCheckpoints = numDelegateStakingCheckpoints[delegatee][lockedTS]; + uint96 staked = delegateStakingCheckpoints[delegatee][lockedTS][nCheckpoints - 1].stake; + _writeDelegateCheckpoint(delegatee, lockedTS, nCheckpoints, 0); + } - /** - * @notice Add vesting contract's code hash to a map of code hashes. - * @param vesting The address of Vesting contract. - * @dev We need it to use _isVestingContract() function instead of isContract() - */ - function addContractCodeHash(address vesting) public onlyAuthorized { - bytes32 codeHash = _getCodeHash(vesting); - vestingCodeHashes[codeHash] = true; - emit ContractCodeHashAdded(codeHash); - } + /** + * @notice Add vesting contract's code hash to a map of code hashes. + * @param vesting The address of Vesting contract. + * @dev We need it to use _isVestingContract() function instead of isContract() + */ + function addContractCodeHash(address vesting) public onlyAuthorized { + bytes32 codeHash = _getCodeHash(vesting); + vestingCodeHashes[codeHash] = true; + emit ContractCodeHashAdded(codeHash); + } - /** - * @notice Add vesting contract's code hash to a map of code hashes. - * @param vesting The address of Vesting contract. - * @dev We need it to use _isVestingContract() function instead of isContract() - */ - function removeContractCodeHash(address vesting) public onlyAuthorized { - bytes32 codeHash = _getCodeHash(vesting); - vestingCodeHashes[codeHash] = false; - emit ContractCodeHashRemoved(codeHash); - } + /** + * @notice Add vesting contract's code hash to a map of code hashes. + * @param vesting The address of Vesting contract. + * @dev We need it to use _isVestingContract() function instead of isContract() + */ + function removeContractCodeHash(address vesting) public onlyAuthorized { + bytes32 codeHash = _getCodeHash(vesting); + vestingCodeHashes[codeHash] = false; + emit ContractCodeHashRemoved(codeHash); + } - /** - * @notice Return hash of contract code - */ - function _getCodeHash(address _contract) internal view returns (bytes32) { - bytes32 codeHash; - assembly { - codeHash := extcodehash(_contract) - } - return codeHash; - } + /** + * @notice Return hash of contract code + */ + function _getCodeHash(address _contract) internal view returns (bytes32) { + bytes32 codeHash; + assembly { + codeHash := extcodehash(_contract) + } + return codeHash; + } - /** - * @notice Return flag whether the given address is a registered vesting contract. - * @param stakerAddress the address to check - */ - function isVestingContract(address stakerAddress) public view returns (bool) { - bytes32 codeHash = _getCodeHash(stakerAddress); - return vestingCodeHashes[codeHash]; - } + /** + * @notice Return flag whether the given address is a registered vesting contract. + * @param stakerAddress the address to check + */ + function isVestingContract(address stakerAddress) public view returns (bool) { + bytes32 codeHash = _getCodeHash(stakerAddress); + return vestingCodeHashes[codeHash]; + } } diff --git a/contracts/mockup/StakingRewardsMockUp.sol b/contracts/mockup/StakingRewardsMockUp.sol index d1c3cb1dc..2a931a2af 100644 --- a/contracts/mockup/StakingRewardsMockUp.sol +++ b/contracts/mockup/StakingRewardsMockUp.sol @@ -8,24 +8,24 @@ import "./BlockMockUp.sol"; * @notice This is used for Testing * */ contract StakingRewardsMockUp is StakingRewards { - ///@notice the block mock up contract - BlockMockUp public blockMockUp; + ///@notice the block mock up contract + BlockMockUp public blockMockUp; - using SafeMath for uint256; + using SafeMath for uint256; - /** - * @notice gets block number from BlockMockUp - * @param _blockMockUp the address of BlockMockUp - */ - function setBlockMockUpAddr(address _blockMockUp) public onlyOwner { - require(_blockMockUp != address(0), "block mockup address invalid"); - blockMockUp = BlockMockUp(_blockMockUp); - } + /** + * @notice gets block number from BlockMockUp + * @param _blockMockUp the address of BlockMockUp + */ + function setBlockMockUpAddr(address _blockMockUp) public onlyOwner { + require(_blockMockUp != address(0), "block mockup address invalid"); + blockMockUp = BlockMockUp(_blockMockUp); + } - /** - * @notice Determine the current Block Number from BlockMockUp - * */ - function _getCurrentBlockNumber() internal view returns (uint256) { - return blockMockUp.getBlockNum(); - } + /** + * @notice Determine the current Block Number from BlockMockUp + * */ + function _getCurrentBlockNumber() internal view returns (uint256) { + return blockMockUp.getBlockNum(); + } } diff --git a/contracts/mockup/TimelockHarness.sol b/contracts/mockup/TimelockHarness.sol index 4d2cc7547..9c1c78745 100644 --- a/contracts/mockup/TimelockHarness.sol +++ b/contracts/mockup/TimelockHarness.sol @@ -3,38 +3,38 @@ pragma solidity ^0.5.16; import "../governance/Timelock.sol"; interface Administered { - function _acceptAdmin() external returns (uint256); + function _acceptAdmin() external returns (uint256); } contract TimelockHarness is Timelock { - constructor(address admin_, uint256 delay_) public Timelock(admin_, delay_) {} + constructor(address admin_, uint256 delay_) public Timelock(admin_, delay_) {} - function setDelayWithoutChecking(uint256 delay_) public { - delay = delay_; + function setDelayWithoutChecking(uint256 delay_) public { + delay = delay_; - emit NewDelay(delay); - } + emit NewDelay(delay); + } - function harnessSetPendingAdmin(address pendingAdmin_) public { - pendingAdmin = pendingAdmin_; - } + function harnessSetPendingAdmin(address pendingAdmin_) public { + pendingAdmin = pendingAdmin_; + } - function harnessSetAdmin(address admin_) public { - admin = admin_; - } + function harnessSetAdmin(address admin_) public { + admin = admin_; + } } contract TimelockTest is Timelock { - constructor(address admin_, uint256 delay_) public Timelock(admin_, 2 days) { - delay = delay_; - } - - function harnessSetAdmin(address admin_) public { - require(msg.sender == admin); - admin = admin_; - } - - function harnessAcceptAdmin(Administered administered) public { - administered._acceptAdmin(); - } + constructor(address admin_, uint256 delay_) public Timelock(admin_, 2 days) { + delay = delay_; + } + + function harnessSetAdmin(address admin_) public { + require(msg.sender == admin); + admin = admin_; + } + + function harnessAcceptAdmin(Administered administered) public { + administered._acceptAdmin(); + } } diff --git a/contracts/mockup/VestingLogicMockup.sol b/contracts/mockup/VestingLogicMockup.sol index cca4005e9..433d93655 100644 --- a/contracts/mockup/VestingLogicMockup.sol +++ b/contracts/mockup/VestingLogicMockup.sol @@ -4,18 +4,18 @@ pragma experimental ABIEncoderV2; import "../governance/Vesting/VestingLogic.sol"; contract VestingLogicMockup is VestingLogic { - /** - * @dev we had a bug in a loop: "i < endDate" instead of "i <= endDate" - */ - function delegate(address _delegatee) public onlyTokenOwner { - require(_delegatee != address(0), "delegatee address invalid"); + /** + * @dev we had a bug in a loop: "i < endDate" instead of "i <= endDate" + */ + function delegate(address _delegatee) public onlyTokenOwner { + require(_delegatee != address(0), "delegatee address invalid"); - /// @dev Withdraw for each unlocked position. - /// @dev Don't change FOUR_WEEKS to TWO_WEEKS, a lot of vestings already deployed with FOUR_WEEKS - /// workaround found, but it doesn't work with TWO_WEEKS - for (uint256 i = startDate + cliff; i < endDate; i += FOUR_WEEKS) { - staking.delegate(_delegatee, i); - } - emit VotesDelegated(msg.sender, _delegatee); - } + /// @dev Withdraw for each unlocked position. + /// @dev Don't change FOUR_WEEKS to TWO_WEEKS, a lot of vestings already deployed with FOUR_WEEKS + /// workaround found, but it doesn't work with TWO_WEEKS + for (uint256 i = startDate + cliff; i < endDate; i += FOUR_WEEKS) { + staking.delegate(_delegatee, i); + } + emit VotesDelegated(msg.sender, _delegatee); + } } diff --git a/contracts/mockup/VestingRegistryLogicMockUp.sol b/contracts/mockup/VestingRegistryLogicMockUp.sol index 277168726..a8fe41e5f 100644 --- a/contracts/mockup/VestingRegistryLogicMockUp.sol +++ b/contracts/mockup/VestingRegistryLogicMockUp.sol @@ -3,7 +3,7 @@ pragma experimental ABIEncoderV2; import "../governance/Vesting/VestingRegistryLogic.sol"; contract VestingRegistryLogicMockup is VestingRegistryLogic { - function isVestingAdress(address _vestingAddress) external view returns (bool isVestingAddr) { - return true; - } + function isVestingAdress(address _vestingAddress) external view returns (bool isVestingAddr) { + return true; + } } diff --git a/contracts/mockup/lockedSOVFailedMockup.sol b/contracts/mockup/lockedSOVFailedMockup.sol index 4db597eed..7917cf876 100644 --- a/contracts/mockup/lockedSOVFailedMockup.sol +++ b/contracts/mockup/lockedSOVFailedMockup.sol @@ -9,94 +9,94 @@ import "../interfaces/IERC20.sol"; * @dev This is not a complete interface of the Locked SOV Contract. */ contract LockedSOVFailedMockup { - using SafeMath for uint256; - - /* Storage */ - - /// @notice The SOV token contract. - IERC20 public SOV; - - /// @notice The user balances. - mapping(address => uint256) lockedBalances; - /// @notice The user balances. - mapping(address => bool) isAdmin; - - /* Events */ - - /// @notice Emitted when a new Admin is added to the admin list. - /// @param _initiator The address which initiated this event to be emitted. - /// @param _newAdmin The address of the new admin. - event AdminAdded(address indexed _initiator, address indexed _newAdmin); - - /// @notice Emitted when an admin is removed from the admin list. - /// @param _initiator The address which initiated this event to be emitted. - /// @param _removedAdmin The address of the removed admin. - event AdminRemoved(address indexed _initiator, address indexed _removedAdmin); - - /* Modifiers */ - - modifier onlyAdmin { - require(isAdmin[msg.sender], "Only admin can call this."); - _; - } - - /* Functions */ - - /** - * @notice Setup the required parameters. - * @param _SOV The SOV token address. - * @param _admins The list of admins to be added. - */ - constructor(address _SOV, address[] memory _admins) public { - require(_SOV != address(0), "Invalid SOV Address."); - SOV = IERC20(_SOV); - for (uint256 index = 0; index < _admins.length; index++) { - isAdmin[_admins[index]] = true; - } - } - - /** - * @notice The function to add a new admin. - * @param _newAdmin The address of the new admin. - */ - function addAdmin(address _newAdmin) public onlyAdmin { - require(_newAdmin != address(0), "Invalid Address"); - require(!isAdmin[_newAdmin], "Address is already admin"); - isAdmin[_newAdmin] = true; - - emit AdminAdded(msg.sender, _newAdmin); - } - - /** - * @notice The function to remove an admin. - * @param _adminToRemove The address of the admin which should be removed. - */ - function removeAdmin(address _adminToRemove) public onlyAdmin { - require(isAdmin[_adminToRemove], "Address is not an admin"); - isAdmin[_adminToRemove] = false; - - emit AdminRemoved(msg.sender, _adminToRemove); - } - - /** - * @notice Adds SOV to the locked balance of a user. - * @param _userAddress The user whose locked balance has to be updated with _sovAmount. - * @param _sovAmount The amount of SOV to be added to the locked balance. - */ - function depositSOV(address _userAddress, uint256 _sovAmount) external { - revert("For testing purposes"); - bool txStatus = SOV.transferFrom(msg.sender, address(this), _sovAmount); - require(txStatus, "Token transfer was not successful. Check receiver address."); - - lockedBalances[_userAddress] = lockedBalances[_userAddress].add(_sovAmount); - } - - /** - * @notice The function to get the locked balance of a user. - * @param _addr The address of the user to check the locked balance. - * @return _balance The locked balance of the address `_addr`. - */ - function getLockedBalance(address _addr) public view returns (uint256 _balance) { - return lockedBalances[_addr]; - } + using SafeMath for uint256; + + /* Storage */ + + /// @notice The SOV token contract. + IERC20 public SOV; + + /// @notice The user balances. + mapping(address => uint256) lockedBalances; + /// @notice The user balances. + mapping(address => bool) isAdmin; + + /* Events */ + + /// @notice Emitted when a new Admin is added to the admin list. + /// @param _initiator The address which initiated this event to be emitted. + /// @param _newAdmin The address of the new admin. + event AdminAdded(address indexed _initiator, address indexed _newAdmin); + + /// @notice Emitted when an admin is removed from the admin list. + /// @param _initiator The address which initiated this event to be emitted. + /// @param _removedAdmin The address of the removed admin. + event AdminRemoved(address indexed _initiator, address indexed _removedAdmin); + + /* Modifiers */ + + modifier onlyAdmin { + require(isAdmin[msg.sender], "Only admin can call this."); + _; + } + + /* Functions */ + + /** + * @notice Setup the required parameters. + * @param _SOV The SOV token address. + * @param _admins The list of admins to be added. + */ + constructor(address _SOV, address[] memory _admins) public { + require(_SOV != address(0), "Invalid SOV Address."); + SOV = IERC20(_SOV); + for (uint256 index = 0; index < _admins.length; index++) { + isAdmin[_admins[index]] = true; + } + } + + /** + * @notice The function to add a new admin. + * @param _newAdmin The address of the new admin. + */ + function addAdmin(address _newAdmin) public onlyAdmin { + require(_newAdmin != address(0), "Invalid Address"); + require(!isAdmin[_newAdmin], "Address is already admin"); + isAdmin[_newAdmin] = true; + + emit AdminAdded(msg.sender, _newAdmin); + } + + /** + * @notice The function to remove an admin. + * @param _adminToRemove The address of the admin which should be removed. + */ + function removeAdmin(address _adminToRemove) public onlyAdmin { + require(isAdmin[_adminToRemove], "Address is not an admin"); + isAdmin[_adminToRemove] = false; + + emit AdminRemoved(msg.sender, _adminToRemove); + } + + /** + * @notice Adds SOV to the locked balance of a user. + * @param _userAddress The user whose locked balance has to be updated with _sovAmount. + * @param _sovAmount The amount of SOV to be added to the locked balance. + */ + function depositSOV(address _userAddress, uint256 _sovAmount) external { + revert("For testing purposes"); + bool txStatus = SOV.transferFrom(msg.sender, address(this), _sovAmount); + require(txStatus, "Token transfer was not successful. Check receiver address."); + + lockedBalances[_userAddress] = lockedBalances[_userAddress].add(_sovAmount); + } + + /** + * @notice The function to get the locked balance of a user. + * @param _addr The address of the user to check the locked balance. + * @return _balance The locked balance of the address `_addr`. + */ + function getLockedBalance(address _addr) public view returns (uint256 _balance) { + return lockedBalances[_addr]; + } } diff --git a/contracts/mockup/previousLoanToken/PreviousLoanToken.sol b/contracts/mockup/previousLoanToken/PreviousLoanToken.sol index a5beb0ad0..6cce81e43 100644 --- a/contracts/mockup/previousLoanToken/PreviousLoanToken.sol +++ b/contracts/mockup/previousLoanToken/PreviousLoanToken.sol @@ -9,76 +9,76 @@ import "../../connectors/loantoken/AdvancedTokenStorage.sol"; //@todo can I change this proxy to EIP-1822 proxy standard, please. https://eips.ethereum.org/EIPS/eip-1822. It's really hard to work with this. contract PreviousLoanToken is AdvancedTokenStorage { - // It is important to maintain the variables order so the delegate calls can access sovrynContractAddress and wrbtcTokenAddress - address public sovrynContractAddress; - address public wrbtcTokenAddress; - address internal target_; + // It is important to maintain the variables order so the delegate calls can access sovrynContractAddress and wrbtcTokenAddress + address public sovrynContractAddress; + address public wrbtcTokenAddress; + address internal target_; - constructor( - address _newOwner, - address _newTarget, - address _sovrynContractAddress, - address _wrbtcTokenAddress - ) public { - transferOwnership(_newOwner); - _setTarget(_newTarget); - _setSovrynContractAddress(_sovrynContractAddress); - _setWrbtcTokenAddress(_wrbtcTokenAddress); - } + constructor( + address _newOwner, + address _newTarget, + address _sovrynContractAddress, + address _wrbtcTokenAddress + ) public { + transferOwnership(_newOwner); + _setTarget(_newTarget); + _setSovrynContractAddress(_sovrynContractAddress); + _setWrbtcTokenAddress(_wrbtcTokenAddress); + } - function() external payable { - if (gasleft() <= 2300) { - return; - } + function() external payable { + if (gasleft() <= 2300) { + return; + } - address target = target_; - bytes memory data = msg.data; - assembly { - let result := delegatecall(gas, target, add(data, 0x20), mload(data), 0, 0) - let size := returndatasize - let ptr := mload(0x40) - returndatacopy(ptr, 0, size) - switch result - case 0 { - revert(ptr, size) - } - default { - return(ptr, size) - } - } - } + address target = target_; + bytes memory data = msg.data; + assembly { + let result := delegatecall(gas, target, add(data, 0x20), mload(data), 0, 0) + let size := returndatasize + let ptr := mload(0x40) + returndatacopy(ptr, 0, size) + switch result + case 0 { + revert(ptr, size) + } + default { + return(ptr, size) + } + } + } - function setTarget(address _newTarget) public onlyOwner { - _setTarget(_newTarget); - } + function setTarget(address _newTarget) public onlyOwner { + _setTarget(_newTarget); + } - function _setTarget(address _newTarget) internal { - require(Address.isContract(_newTarget), "target not a contract"); - target_ = _newTarget; - } + function _setTarget(address _newTarget) internal { + require(Address.isContract(_newTarget), "target not a contract"); + target_ = _newTarget; + } - function _setSovrynContractAddress(address _sovrynContractAddress) internal { - require(Address.isContract(_sovrynContractAddress), "sovryn not a contract"); - sovrynContractAddress = _sovrynContractAddress; - } + function _setSovrynContractAddress(address _sovrynContractAddress) internal { + require(Address.isContract(_sovrynContractAddress), "sovryn not a contract"); + sovrynContractAddress = _sovrynContractAddress; + } - function _setWrbtcTokenAddress(address _wrbtcTokenAddress) internal { - require(Address.isContract(_wrbtcTokenAddress), "wrbtc not a contract"); - wrbtcTokenAddress = _wrbtcTokenAddress; - } + function _setWrbtcTokenAddress(address _wrbtcTokenAddress) internal { + require(Address.isContract(_wrbtcTokenAddress), "wrbtc not a contract"); + wrbtcTokenAddress = _wrbtcTokenAddress; + } - //@todo add check for double init, idk but init usually can be called only once. - function initialize( - address _loanTokenAddress, - string memory _name, - string memory _symbol - ) public onlyOwner { - loanTokenAddress = _loanTokenAddress; + //@todo add check for double init, idk but init usually can be called only once. + function initialize( + address _loanTokenAddress, + string memory _name, + string memory _symbol + ) public onlyOwner { + loanTokenAddress = _loanTokenAddress; - name = _name; - symbol = _symbol; - decimals = IERC20(loanTokenAddress).decimals(); + name = _name; + symbol = _symbol; + decimals = IERC20(loanTokenAddress).decimals(); - initialPrice = 10**18; // starting price of 1 - } + initialPrice = 10**18; // starting price of 1 + } } diff --git a/contracts/mockup/previousLoanToken/PreviousLoanTokenSettingsLowerAdmin.sol b/contracts/mockup/previousLoanToken/PreviousLoanTokenSettingsLowerAdmin.sol index 915cc5980..de5b15443 100644 --- a/contracts/mockup/previousLoanToken/PreviousLoanTokenSettingsLowerAdmin.sol +++ b/contracts/mockup/previousLoanToken/PreviousLoanTokenSettingsLowerAdmin.sol @@ -11,133 +11,151 @@ import "../../connectors/loantoken/AdvancedTokenStorage.sol"; // It is a LoanToken implementation! contract PreviousLoanTokenSettingsLowerAdmin is AdvancedTokenStorage { - using SafeMath for uint256; - - // It is important to maintain the variables order so the delegate calls can access sovrynContractAddress - - // ------------- MUST BE THE SAME AS IN LoanToken CONTRACT ------------------- - address public sovrynContractAddress; - address public wrbtcTokenAddress; - address internal target_; - // ------------- END MUST BE THE SAME AS IN LoanToken CONTRACT ------------------- - - event SetTransactionLimits(address[] addresses, uint256[] limits); - - //@todo check for restrictions in this contract - modifier onlyAdmin() { - require(msg.sender == address(this) || msg.sender == owner(), "unauthorized"); - _; - } - - //@todo add check for double init, idk but init usually can be called only once. - function init( - address _loanTokenAddress, - string memory _name, - string memory _symbol - ) public onlyOwner { - loanTokenAddress = _loanTokenAddress; - - name = _name; - symbol = _symbol; - decimals = IERC20(loanTokenAddress).decimals(); - - initialPrice = 10**18; // starting price of 1 - } - - function() external { - revert("LoanTokenSettingsLowerAdmin - fallback not allowed"); - } - - function setupLoanParams(LoanParamsStruct.LoanParams[] memory loanParamsList, bool areTorqueLoans) public onlyAdmin { - bytes32[] memory loanParamsIdList; - address _loanTokenAddress = loanTokenAddress; - - for (uint256 i = 0; i < loanParamsList.length; i++) { - loanParamsList[i].loanToken = _loanTokenAddress; - loanParamsList[i].maxLoanTerm = areTorqueLoans ? 0 : 28 days; - } - - loanParamsIdList = ProtocolSettingsLike(sovrynContractAddress).setupLoanParams(loanParamsList); - for (uint256 i = 0; i < loanParamsIdList.length; i++) { - loanParamsIds[ - uint256( - keccak256( - abi.encodePacked( - loanParamsList[i].collateralToken, - areTorqueLoans // isTorqueLoan - ) - ) - ) - ] = loanParamsIdList[i]; - } - } - - function disableLoanParams(address[] calldata collateralTokens, bool[] calldata isTorqueLoans) external onlyAdmin { - require(collateralTokens.length == isTorqueLoans.length, "count mismatch"); - - bytes32[] memory loanParamsIdList = new bytes32[](collateralTokens.length); - for (uint256 i = 0; i < collateralTokens.length; i++) { - uint256 id = uint256(keccak256(abi.encodePacked(collateralTokens[i], isTorqueLoans[i]))); - loanParamsIdList[i] = loanParamsIds[id]; - delete loanParamsIds[id]; - } - - ProtocolSettingsLike(sovrynContractAddress).disableLoanParams(loanParamsIdList); - } - - // These params should be percentages represented like so: 5% = 5000000000000000000 - // rateMultiplier + baseRate can't exceed 100% - function setDemandCurve( - uint256 _baseRate, - uint256 _rateMultiplier, - uint256 _lowUtilBaseRate, - uint256 _lowUtilRateMultiplier, - uint256 _targetLevel, - uint256 _kinkLevel, - uint256 _maxScaleRate - ) public onlyAdmin { - require(_rateMultiplier.add(_baseRate) <= WEI_PERCENT_PRECISION, "curve params too high"); - require(_lowUtilRateMultiplier.add(_lowUtilBaseRate) <= WEI_PERCENT_PRECISION, "curve params too high"); - - require(_targetLevel <= WEI_PERCENT_PRECISION && _kinkLevel <= WEI_PERCENT_PRECISION, "levels too high"); - - baseRate = _baseRate; - rateMultiplier = _rateMultiplier; - lowUtilBaseRate = _lowUtilBaseRate; - lowUtilRateMultiplier = _lowUtilRateMultiplier; - - targetLevel = _targetLevel; // 80 ether - kinkLevel = _kinkLevel; // 90 ether - maxScaleRate = _maxScaleRate; // 100 ether - } - - function toggleFunctionPause( - string memory funcId, // example: "mint(uint256,uint256)" - bool isPaused - ) public onlyAdmin { - // keccak256("iToken_FunctionPause") - bytes32 slot = - keccak256( - abi.encodePacked( - bytes4(keccak256(abi.encodePacked(funcId))), - uint256(0xd46a704bc285dbd6ff5ad3863506260b1df02812f4f857c8cc852317a6ac64f2) - ) - ); - assembly { - sstore(slot, isPaused) - } - } - - /** - * sets the transaction limit per token address - * @param addresses the token addresses - * @param limits the limit denominated in the currency of the token address - * */ - function setTransactionLimits(address[] memory addresses, uint256[] memory limits) public onlyOwner { - require(addresses.length == limits.length, "mismatched array lengths"); - for (uint256 i = 0; i < addresses.length; i++) { - transactionLimit[addresses[i]] = limits[i]; - } - emit SetTransactionLimits(addresses, limits); - } + using SafeMath for uint256; + + // It is important to maintain the variables order so the delegate calls can access sovrynContractAddress + + // ------------- MUST BE THE SAME AS IN LoanToken CONTRACT ------------------- + address public sovrynContractAddress; + address public wrbtcTokenAddress; + address internal target_; + // ------------- END MUST BE THE SAME AS IN LoanToken CONTRACT ------------------- + + event SetTransactionLimits(address[] addresses, uint256[] limits); + + //@todo check for restrictions in this contract + modifier onlyAdmin() { + require(msg.sender == address(this) || msg.sender == owner(), "unauthorized"); + _; + } + + //@todo add check for double init, idk but init usually can be called only once. + function init( + address _loanTokenAddress, + string memory _name, + string memory _symbol + ) public onlyOwner { + loanTokenAddress = _loanTokenAddress; + + name = _name; + symbol = _symbol; + decimals = IERC20(loanTokenAddress).decimals(); + + initialPrice = 10**18; // starting price of 1 + } + + function() external { + revert("LoanTokenSettingsLowerAdmin - fallback not allowed"); + } + + function setupLoanParams( + LoanParamsStruct.LoanParams[] memory loanParamsList, + bool areTorqueLoans + ) public onlyAdmin { + bytes32[] memory loanParamsIdList; + address _loanTokenAddress = loanTokenAddress; + + for (uint256 i = 0; i < loanParamsList.length; i++) { + loanParamsList[i].loanToken = _loanTokenAddress; + loanParamsList[i].maxLoanTerm = areTorqueLoans ? 0 : 28 days; + } + + loanParamsIdList = ProtocolSettingsLike(sovrynContractAddress).setupLoanParams( + loanParamsList + ); + for (uint256 i = 0; i < loanParamsIdList.length; i++) { + loanParamsIds[ + uint256( + keccak256( + abi.encodePacked( + loanParamsList[i].collateralToken, + areTorqueLoans // isTorqueLoan + ) + ) + ) + ] = loanParamsIdList[i]; + } + } + + function disableLoanParams(address[] calldata collateralTokens, bool[] calldata isTorqueLoans) + external + onlyAdmin + { + require(collateralTokens.length == isTorqueLoans.length, "count mismatch"); + + bytes32[] memory loanParamsIdList = new bytes32[](collateralTokens.length); + for (uint256 i = 0; i < collateralTokens.length; i++) { + uint256 id = + uint256(keccak256(abi.encodePacked(collateralTokens[i], isTorqueLoans[i]))); + loanParamsIdList[i] = loanParamsIds[id]; + delete loanParamsIds[id]; + } + + ProtocolSettingsLike(sovrynContractAddress).disableLoanParams(loanParamsIdList); + } + + // These params should be percentages represented like so: 5% = 5000000000000000000 + // rateMultiplier + baseRate can't exceed 100% + function setDemandCurve( + uint256 _baseRate, + uint256 _rateMultiplier, + uint256 _lowUtilBaseRate, + uint256 _lowUtilRateMultiplier, + uint256 _targetLevel, + uint256 _kinkLevel, + uint256 _maxScaleRate + ) public onlyAdmin { + require(_rateMultiplier.add(_baseRate) <= WEI_PERCENT_PRECISION, "curve params too high"); + require( + _lowUtilRateMultiplier.add(_lowUtilBaseRate) <= WEI_PERCENT_PRECISION, + "curve params too high" + ); + + require( + _targetLevel <= WEI_PERCENT_PRECISION && _kinkLevel <= WEI_PERCENT_PRECISION, + "levels too high" + ); + + baseRate = _baseRate; + rateMultiplier = _rateMultiplier; + lowUtilBaseRate = _lowUtilBaseRate; + lowUtilRateMultiplier = _lowUtilRateMultiplier; + + targetLevel = _targetLevel; // 80 ether + kinkLevel = _kinkLevel; // 90 ether + maxScaleRate = _maxScaleRate; // 100 ether + } + + function toggleFunctionPause( + string memory funcId, // example: "mint(uint256,uint256)" + bool isPaused + ) public onlyAdmin { + // keccak256("iToken_FunctionPause") + bytes32 slot = + keccak256( + abi.encodePacked( + bytes4(keccak256(abi.encodePacked(funcId))), + uint256(0xd46a704bc285dbd6ff5ad3863506260b1df02812f4f857c8cc852317a6ac64f2) + ) + ); + assembly { + sstore(slot, isPaused) + } + } + + /** + * sets the transaction limit per token address + * @param addresses the token addresses + * @param limits the limit denominated in the currency of the token address + * */ + function setTransactionLimits(address[] memory addresses, uint256[] memory limits) + public + onlyOwner + { + require(addresses.length == limits.length, "mismatched array lengths"); + for (uint256 i = 0; i < addresses.length; i++) { + transactionLimit[addresses[i]] = limits[i]; + } + emit SetTransactionLimits(addresses, limits); + } } diff --git a/contracts/mockup/proxy/ImplementationMockup.sol b/contracts/mockup/proxy/ImplementationMockup.sol index 65dda08d0..e99af0c6d 100644 --- a/contracts/mockup/proxy/ImplementationMockup.sol +++ b/contracts/mockup/proxy/ImplementationMockup.sol @@ -3,12 +3,12 @@ pragma solidity ^0.5.17; import "./StorageMockup.sol"; contract ImplementationMockup is StorageMockup { - function setValue(uint256 _value) public { - value = _value; - emit ValueChanged(_value); - } + function setValue(uint256 _value) public { + value = _value; + emit ValueChanged(_value); + } - function getValue() public view returns (uint256) { - return value; - } + function getValue() public view returns (uint256) { + return value; + } } diff --git a/contracts/mockup/proxy/StorageMockup.sol b/contracts/mockup/proxy/StorageMockup.sol index 43fb77adb..46b53b73d 100644 --- a/contracts/mockup/proxy/StorageMockup.sol +++ b/contracts/mockup/proxy/StorageMockup.sol @@ -1,7 +1,7 @@ pragma solidity ^0.5.17; contract StorageMockup { - uint256 value; + uint256 value; - event ValueChanged(uint256 value); + event ValueChanged(uint256 value); } diff --git a/contracts/mockup/setGet.sol b/contracts/mockup/setGet.sol index 4f6bb2c09..8870a2f83 100644 --- a/contracts/mockup/setGet.sol +++ b/contracts/mockup/setGet.sol @@ -6,23 +6,23 @@ pragma solidity 0.5.17; * @dev This is going to be used for testing purposes. */ contract setGet { - uint256 public value; + uint256 public value; - event valueSet(uint256 indexed _value); + event valueSet(uint256 indexed _value); - /** - * @notice To get the `value`. - * @return _value The value. - */ - function get() public returns (uint256 _value) { - return value; - } + /** + * @notice To get the `value`. + * @return _value The value. + */ + function get() public returns (uint256 _value) { + return value; + } - /** - * @notice To set the `value`. - * @param _value The value. - */ - function set(uint256 _value) public { - value = _value; - } + /** + * @notice To set the `value`. + * @param _value The value. + */ + function set(uint256 _value) public { + value = _value; + } } diff --git a/contracts/modules/Affiliates.sol b/contracts/modules/Affiliates.sol index 1dfa528b6..27b7329cf 100644 --- a/contracts/modules/Affiliates.sol +++ b/contracts/modules/Affiliates.sol @@ -22,441 +22,497 @@ import "../mixins/ModuleCommonFunctionalities.sol"; * Storage: from State, functions called from Protocol by delegatecall */ contract Affiliates is State, AffiliatesEvents, ModuleCommonFunctionalities { - using SafeERC20 for IERC20; - - /** - * @notice Void constructor. - */ - // solhint-disable-next-line no-empty-blocks - constructor() public {} - - /** - * @notice Avoid calls to this contract except for those explicitly declared. - */ - function() external { - revert("Affiliates - fallback not allowed"); - } - - /** - * @notice Set delegate callable functions by proxy contract. - * @dev This contract is designed as a module, this way logic can be - * expanded and upgraded w/o losing storage that is kept in the protocol (State.sol) - * initialize() is used to register in the proxy external (module) functions - * to be called via the proxy. - * @param target The address of a new logic implementation. - */ - function initialize(address target) external onlyOwner { - address prevModuleContractAddress = logicTargets[this.setAffiliatesReferrer.selector]; - _setTarget(this.setAffiliatesReferrer.selector, target); - _setTarget(this.getUserNotFirstTradeFlag.selector, target); - _setTarget(this.getReferralsList.selector, target); - _setTarget(this.setUserNotFirstTradeFlag.selector, target); - _setTarget(this.payTradingFeeToAffiliatesReferrer.selector, target); - _setTarget(this.getAffiliatesReferrerBalances.selector, target); - _setTarget(this.getAffiliatesReferrerTokenBalance.selector, target); - _setTarget(this.getAffiliatesReferrerTokensList.selector, target); - _setTarget(this.withdrawAffiliatesReferrerTokenFees.selector, target); - _setTarget(this.withdrawAllAffiliatesReferrerTokenFees.selector, target); - _setTarget(this.getMinReferralsToPayout.selector, target); - _setTarget(this.getAffiliatesUserReferrer.selector, target); - _setTarget(this.getAffiliateRewardsHeld.selector, target); - _setTarget(this.getAffiliateTradingTokenFeePercent.selector, target); - _setTarget(this.getAffiliatesTokenRewardsValueInRbtc.selector, target); - emit ProtocolModuleContractReplaced(prevModuleContractAddress, target, "Affiliates"); - } - - /** - * @notice Function modifier to avoid any other calls not coming from loan pools. - */ - modifier onlyCallableByLoanPools() { - require(loanPoolToUnderlying[msg.sender] != address(0), "Affiliates: not authorized"); - _; - } - - /** - * @notice Function modifier to avoid any other calls not coming from within protocol functions. - */ - modifier onlyCallableInternal() { - require(msg.sender == protocolAddress, "Affiliates: not authorized"); - _; - } - - /** - * @notice Data structure comprised of 3 flags to compute the result of setting a referrer. - */ - struct SetAffiliatesReferrerResult { - bool success; - bool alreadySet; - bool userNotFirstTradeFlag; - } - - /** - * @notice Loan pool calls this function to tell affiliates - * a user coming from a referrer is trading and should be registered if not yet. - * Taking into account some user status flags may lead to the user and referrer - * become added or not to the affiliates record. - * - * @param user The address of the user that is trading on loan pools. - * @param referrer The address of the referrer the user is coming from. - */ - function setAffiliatesReferrer(address user, address referrer) external onlyCallableByLoanPools whenNotPaused { - SetAffiliatesReferrerResult memory result; - - result.userNotFirstTradeFlag = getUserNotFirstTradeFlag(user); - result.alreadySet = affiliatesUserReferrer[user] != address(0); - result.success = !(result.userNotFirstTradeFlag || result.alreadySet || user == referrer); - if (result.success) { - affiliatesUserReferrer[user] = referrer; - referralsList[referrer].add(user); - emit SetAffiliatesReferrer(user, referrer); - } else { - emit SetAffiliatesReferrerFail(user, referrer, result.alreadySet, result.userNotFirstTradeFlag); - } - } - - /** - * @notice Getter to query the referrals coming from a referrer. - * @param referrer The address of a given referrer. - * @return The referralsList mapping value by referrer. - */ - function getReferralsList(address referrer) external view returns (address[] memory refList) { - refList = referralsList[referrer].enumerate(); - return refList; - } - - /** - * @notice Getter to query the not-first-trade flag of a user. - * @param user The address of a given user. - * @return The userNotFirstTradeFlag mapping value by user. - */ - function getUserNotFirstTradeFlag(address user) public view returns (bool) { - return userNotFirstTradeFlag[user]; - } - - /** - * @notice Setter to toggle on the not-first-trade flag of a user. - * @param user The address of a given user. - */ - function setUserNotFirstTradeFlag(address user) external onlyCallableByLoanPools whenNotPaused { - if (!userNotFirstTradeFlag[user]) { - userNotFirstTradeFlag[user] = true; - emit SetUserNotFirstTradeFlag(user); - } - } - - /** - * @notice Internal getter to query the fee share for affiliate program. - * @dev It returns a value defined at protocol storage (State.sol) - * @return The percentage of fee share w/ 18 decimals. - */ - function _getAffiliatesTradingFeePercentForSOV() internal view returns (uint256) { - return affiliateFeePercent; - } - - /** - * @notice Internal to calculate the affiliates trading token fee amount. - * Affiliates program has 2 kind of rewards: - * 1. x% based on the fee of the token that is traded (in form of the token itself). - * 2. x% based on the fee of the token that is traded (in form of SOV). - * This _getReferrerTradingFeeForToken calculates the first one - * by applying a custom percentage multiplier. - * @param feeTokenAmount The trading token fee amount. - * @return The affiliates share of the trading token fee amount. - */ - function _getReferrerTradingFeeForToken(uint256 feeTokenAmount) internal view returns (uint256) { - return feeTokenAmount.mul(getAffiliateTradingTokenFeePercent()).div(10**20); - } - - /** - * @notice Getter to query the fee share of trading token fee for affiliate program. - * @dev It returns a value defined at protocol storage (State.sol) - * @return The percentage of fee share w/ 18 decimals. - */ - function getAffiliateTradingTokenFeePercent() public view returns (uint256) { - return affiliateTradingTokenFeePercent; - } - - /** - * @notice Getter to query referral threshold for paying out to the referrer. - * @dev It returns a value defined at protocol storage (State.sol) - * @return The minimum number of referrals set by Protocol. - */ - function getMinReferralsToPayout() public view returns (uint256) { - return minReferralsToPayout; - } - - /** - * @notice Get the sovToken reward of a trade. - * @dev The reward is worth x% of the trading fee. - * @param feeToken The address of the token in which the trading/borrowing fee was paid. - * @param feeAmount The height of the fee. - * @return The reward amount. - * */ - function _getSovBonusAmount(address feeToken, uint256 feeAmount) internal view returns (uint256) { - uint256 rewardAmount; - address _priceFeeds = priceFeeds; - - /// @dev Calculate the reward amount, querying the price feed. - (bool success, bytes memory data) = - _priceFeeds.staticcall( - abi.encodeWithSelector( - IPriceFeeds(_priceFeeds).queryReturn.selector, - feeToken, - sovTokenAddress, /// dest token = SOV - feeAmount.mul(_getAffiliatesTradingFeePercentForSOV()).div(1e20) - ) - ); - // solhint-disable-next-line no-inline-assembly - assembly { - if eq(success, 1) { - rewardAmount := mload(add(data, 32)) - } - } - - return rewardAmount; - } - - /** - * @notice Protocol calls this function to pay the affiliates rewards to a user (referrer). - * - * @dev Affiliates program has 2 kind of rewards: - * 1. x% based on the fee of the token that is traded (in form of the token itself). - * 2. x% based on the fee of the token that is traded (in form of SOV). - * Both are paid in this function. - * - * @dev Actually they are not paid, but just holded by protocol until user claims them by - * actively calling withdrawAffiliatesReferrerTokenFees() function, - * and/or when unvesting lockedSOV. - * - * @dev To be precise, what this function does is updating the registers of the rewards - * for the referrer including the assignment of the SOV tokens as rewards to the - * referrer's vesting contract. - * - * @param referrer The address of the referrer. - * @param trader The address of the trader. - * @param token The address of the token in which the trading/borrowing fee was paid. - * @param tradingFeeTokenBaseAmount Total trading fee amount, the base for calculating referrer's fees. - * - * @return referrerBonusSovAmount The amount of SOV tokens paid to the referrer (through a vesting contract, lockedSOV). - * @return referrerBonusTokenAmount The amount of trading tokens paid directly to the referrer. - */ - function payTradingFeeToAffiliatesReferrer( - address referrer, - address trader, - address token, - uint256 tradingFeeTokenBaseAmount - ) external onlyCallableInternal whenNotPaused returns (uint256 referrerBonusSovAmount, uint256 referrerBonusTokenAmount) { - bool isHeld = referralsList[referrer].length() < getMinReferralsToPayout(); - bool bonusPaymentIsSuccess = true; - uint256 paidReferrerBonusSovAmount; - - /// Process token fee rewards first. - referrerBonusTokenAmount = _getReferrerTradingFeeForToken(tradingFeeTokenBaseAmount); - if (!affiliatesReferrerTokensList[referrer].contains(token)) affiliatesReferrerTokensList[referrer].add(token); - affiliatesReferrerBalances[referrer][token] = affiliatesReferrerBalances[referrer][token].add(referrerBonusTokenAmount); - - /// Then process SOV rewards. - referrerBonusSovAmount = _getSovBonusAmount(token, tradingFeeTokenBaseAmount); - uint256 rewardsHeldByProtocol = affiliateRewardsHeld[referrer]; - - if (isHeld) { - /// If referrals less than minimum, temp the rewards SOV to the storage - affiliateRewardsHeld[referrer] = rewardsHeldByProtocol.add(referrerBonusSovAmount); - } else { - /// If referrals >= minimum, directly send all of the remain rewards to locked sov - /// Call depositSOV() in LockedSov contract - /// Set the affiliaterewardsheld = 0 - if (affiliateRewardsHeld[referrer] > 0) { - affiliateRewardsHeld[referrer] = 0; - } - - paidReferrerBonusSovAmount = referrerBonusSovAmount.add(rewardsHeldByProtocol); - IERC20(sovTokenAddress).approve(lockedSOVAddress, paidReferrerBonusSovAmount); - - (bool success, ) = - lockedSOVAddress.call(abi.encodeWithSignature("depositSOV(address,uint256)", referrer, paidReferrerBonusSovAmount)); - - if (!success) { - bonusPaymentIsSuccess = false; - } - } - - if (bonusPaymentIsSuccess) { - emit PayTradingFeeToAffiliate( - referrer, - trader, // trader - token, - isHeld, - tradingFeeTokenBaseAmount, - referrerBonusTokenAmount, - referrerBonusSovAmount, - paidReferrerBonusSovAmount - ); - } else { - emit PayTradingFeeToAffiliateFail( - referrer, - trader, // trader - token, - tradingFeeTokenBaseAmount, - referrerBonusTokenAmount, - referrerBonusSovAmount, - paidReferrerBonusSovAmount - ); - } - - return (referrerBonusSovAmount, referrerBonusTokenAmount); - } - - /** - * @notice Referrer calls this function to receive its reward in a given token. - * It will send the other (non-SOV) reward tokens from trading protocol fees, - * to the referrer’s wallet. - * @dev Rewards are held by protocol in different tokens coming from trading fees. - * Referrer has to claim them one by one for every token with accumulated balance. - * @param token The address of the token to withdraw. - * @param receiver The address of the withdrawal beneficiary. - * @param amount The amount of tokens to claim. If greater than balance, just sends balance. - */ - function withdrawAffiliatesReferrerTokenFees( - address token, - address receiver, - uint256 amount - ) public whenNotPaused { - require(receiver != address(0), "Affiliates: cannot withdraw to zero address"); - address referrer = msg.sender; - uint256 referrerTokenBalance = affiliatesReferrerBalances[referrer][token]; - uint256 withdrawAmount = referrerTokenBalance > amount ? amount : referrerTokenBalance; - - require(withdrawAmount > 0, "Affiliates: cannot withdraw zero amount"); - - require(referralsList[referrer].length() >= getMinReferralsToPayout(), "Your referrals has not reached the minimum request"); - - uint256 newReferrerTokenBalance = referrerTokenBalance.sub(withdrawAmount); - - if (newReferrerTokenBalance == 0) { - _removeAffiliatesReferrerToken(referrer, token); - } else { - affiliatesReferrerBalances[referrer][token] = newReferrerTokenBalance; - } - - IERC20(token).safeTransfer(receiver, withdrawAmount); - - emit WithdrawAffiliatesReferrerTokenFees(referrer, receiver, token, withdrawAmount); - } - - /** - * @notice Withdraw to msg.sender all token fees for a referrer. - * @dev It's done by looping through its available tokens. - * @param receiver The address of the withdrawal beneficiary. - */ - function withdrawAllAffiliatesReferrerTokenFees(address receiver) external whenNotPaused { - require(receiver != address(0), "Affiliates: cannot withdraw to zero address"); - address referrer = msg.sender; - - require(referralsList[referrer].length() >= getMinReferralsToPayout(), "Your referrals has not reached the minimum request"); - - (address[] memory tokenAddresses, uint256[] memory tokenBalances) = getAffiliatesReferrerBalances(referrer); - for (uint256 i; i < tokenAddresses.length; i++) { - withdrawAffiliatesReferrerTokenFees(tokenAddresses[i], receiver, tokenBalances[i]); - } - } - - /** - * @notice Internal function to delete a referrer's token balance. - * @param referrer The address of the referrer. - * @param token The address of the token specifying the balance to remove. - */ - function _removeAffiliatesReferrerToken(address referrer, address token) internal { - delete affiliatesReferrerBalances[referrer][token]; - affiliatesReferrerTokensList[referrer].remove(token); - } - - /** - * @notice Get all token balances of a referrer. - * @param referrer The address of the referrer. - * @return referrerTokensList The array of available tokens (keys). - * @return referrerTokensBalances The array of token balances (values). - */ - function getAffiliatesReferrerBalances(address referrer) - public - view - returns (address[] memory referrerTokensList, uint256[] memory referrerTokensBalances) - { - referrerTokensList = getAffiliatesReferrerTokensList(referrer); - referrerTokensBalances = new uint256[](referrerTokensList.length); - for (uint256 i; i < referrerTokensList.length; i++) { - referrerTokensBalances[i] = getAffiliatesReferrerTokenBalance(referrer, referrerTokensList[i]); - } - return (referrerTokensList, referrerTokensBalances); - } - - /** - * @dev Get all token rewards estimation value in rbtc. - * - * @param referrer Address of referrer. - * - * @return The value estimation in rbtc. - */ - function getAffiliatesTokenRewardsValueInRbtc(address referrer) external view returns (uint256 rbtcTotalAmount) { - address[] memory tokensList = getAffiliatesReferrerTokensList(referrer); - address _priceFeeds = priceFeeds; - - for (uint256 i; i < tokensList.length; i++) { - // Get the value of each token in rbtc - - (bool success, bytes memory data) = - _priceFeeds.staticcall( - abi.encodeWithSelector( - IPriceFeeds(_priceFeeds).queryReturn.selector, - tokensList[i], // source token - address(wrbtcToken), // dest token = SOV - affiliatesReferrerBalances[referrer][tokensList[i]] // total token rewards - ) - ); - - assembly { - if eq(success, 1) { - rbtcTotalAmount := add(rbtcTotalAmount, mload(add(data, 32))) - } - } - } - } - - /** - * @notice Get all available tokens at the affiliates program for a given referrer. - * @param referrer The address of a given referrer. - * @return tokensList The list of available tokens. - */ - function getAffiliatesReferrerTokensList(address referrer) public view returns (address[] memory tokensList) { - tokensList = affiliatesReferrerTokensList[referrer].enumerate(); - return tokensList; - } - - /** - * @notice Getter to query the affiliate balance for a given referrer and token. - * @param referrer The address of the referrer. - * @param token The address of the token to get balance for. - * @return The affiliatesReferrerBalances mapping value by referrer and token keys. - */ - function getAffiliatesReferrerTokenBalance(address referrer, address token) public view returns (uint256) { - return affiliatesReferrerBalances[referrer][token]; - } - - /** - * @notice Getter to query the address of referrer for a given user. - * @param user The address of the user. - * @return The address on affiliatesUserReferrer mapping value by user key. - */ - function getAffiliatesUserReferrer(address user) public view returns (address) { - return affiliatesUserReferrer[user]; - } - - /** - * @notice Getter to query the reward amount held for a given referrer. - * @param referrer The address of the referrer. - * @return The affiliateRewardsHeld mapping value by referrer key. - */ - function getAffiliateRewardsHeld(address referrer) public view returns (uint256) { - return affiliateRewardsHeld[referrer]; - } + using SafeERC20 for IERC20; + + /** + * @notice Void constructor. + */ + // solhint-disable-next-line no-empty-blocks + constructor() public {} + + /** + * @notice Avoid calls to this contract except for those explicitly declared. + */ + function() external { + revert("Affiliates - fallback not allowed"); + } + + /** + * @notice Set delegate callable functions by proxy contract. + * @dev This contract is designed as a module, this way logic can be + * expanded and upgraded w/o losing storage that is kept in the protocol (State.sol) + * initialize() is used to register in the proxy external (module) functions + * to be called via the proxy. + * @param target The address of a new logic implementation. + */ + function initialize(address target) external onlyOwner { + address prevModuleContractAddress = logicTargets[this.setAffiliatesReferrer.selector]; + _setTarget(this.setAffiliatesReferrer.selector, target); + _setTarget(this.getUserNotFirstTradeFlag.selector, target); + _setTarget(this.getReferralsList.selector, target); + _setTarget(this.setUserNotFirstTradeFlag.selector, target); + _setTarget(this.payTradingFeeToAffiliatesReferrer.selector, target); + _setTarget(this.getAffiliatesReferrerBalances.selector, target); + _setTarget(this.getAffiliatesReferrerTokenBalance.selector, target); + _setTarget(this.getAffiliatesReferrerTokensList.selector, target); + _setTarget(this.withdrawAffiliatesReferrerTokenFees.selector, target); + _setTarget(this.withdrawAllAffiliatesReferrerTokenFees.selector, target); + _setTarget(this.getMinReferralsToPayout.selector, target); + _setTarget(this.getAffiliatesUserReferrer.selector, target); + _setTarget(this.getAffiliateRewardsHeld.selector, target); + _setTarget(this.getAffiliateTradingTokenFeePercent.selector, target); + _setTarget(this.getAffiliatesTokenRewardsValueInRbtc.selector, target); + emit ProtocolModuleContractReplaced(prevModuleContractAddress, target, "Affiliates"); + } + + /** + * @notice Function modifier to avoid any other calls not coming from loan pools. + */ + modifier onlyCallableByLoanPools() { + require(loanPoolToUnderlying[msg.sender] != address(0), "Affiliates: not authorized"); + _; + } + + /** + * @notice Function modifier to avoid any other calls not coming from within protocol functions. + */ + modifier onlyCallableInternal() { + require(msg.sender == protocolAddress, "Affiliates: not authorized"); + _; + } + + /** + * @notice Data structure comprised of 3 flags to compute the result of setting a referrer. + */ + struct SetAffiliatesReferrerResult { + bool success; + bool alreadySet; + bool userNotFirstTradeFlag; + } + + /** + * @notice Loan pool calls this function to tell affiliates + * a user coming from a referrer is trading and should be registered if not yet. + * Taking into account some user status flags may lead to the user and referrer + * become added or not to the affiliates record. + * + * @param user The address of the user that is trading on loan pools. + * @param referrer The address of the referrer the user is coming from. + */ + function setAffiliatesReferrer(address user, address referrer) + external + onlyCallableByLoanPools + whenNotPaused + { + SetAffiliatesReferrerResult memory result; + + result.userNotFirstTradeFlag = getUserNotFirstTradeFlag(user); + result.alreadySet = affiliatesUserReferrer[user] != address(0); + result.success = !(result.userNotFirstTradeFlag || result.alreadySet || user == referrer); + if (result.success) { + affiliatesUserReferrer[user] = referrer; + referralsList[referrer].add(user); + emit SetAffiliatesReferrer(user, referrer); + } else { + emit SetAffiliatesReferrerFail( + user, + referrer, + result.alreadySet, + result.userNotFirstTradeFlag + ); + } + } + + /** + * @notice Getter to query the referrals coming from a referrer. + * @param referrer The address of a given referrer. + * @return The referralsList mapping value by referrer. + */ + function getReferralsList(address referrer) external view returns (address[] memory refList) { + refList = referralsList[referrer].enumerate(); + return refList; + } + + /** + * @notice Getter to query the not-first-trade flag of a user. + * @param user The address of a given user. + * @return The userNotFirstTradeFlag mapping value by user. + */ + function getUserNotFirstTradeFlag(address user) public view returns (bool) { + return userNotFirstTradeFlag[user]; + } + + /** + * @notice Setter to toggle on the not-first-trade flag of a user. + * @param user The address of a given user. + */ + function setUserNotFirstTradeFlag(address user) + external + onlyCallableByLoanPools + whenNotPaused + { + if (!userNotFirstTradeFlag[user]) { + userNotFirstTradeFlag[user] = true; + emit SetUserNotFirstTradeFlag(user); + } + } + + /** + * @notice Internal getter to query the fee share for affiliate program. + * @dev It returns a value defined at protocol storage (State.sol) + * @return The percentage of fee share w/ 18 decimals. + */ + function _getAffiliatesTradingFeePercentForSOV() internal view returns (uint256) { + return affiliateFeePercent; + } + + /** + * @notice Internal to calculate the affiliates trading token fee amount. + * Affiliates program has 2 kind of rewards: + * 1. x% based on the fee of the token that is traded (in form of the token itself). + * 2. x% based on the fee of the token that is traded (in form of SOV). + * This _getReferrerTradingFeeForToken calculates the first one + * by applying a custom percentage multiplier. + * @param feeTokenAmount The trading token fee amount. + * @return The affiliates share of the trading token fee amount. + */ + function _getReferrerTradingFeeForToken(uint256 feeTokenAmount) + internal + view + returns (uint256) + { + return feeTokenAmount.mul(getAffiliateTradingTokenFeePercent()).div(10**20); + } + + /** + * @notice Getter to query the fee share of trading token fee for affiliate program. + * @dev It returns a value defined at protocol storage (State.sol) + * @return The percentage of fee share w/ 18 decimals. + */ + function getAffiliateTradingTokenFeePercent() public view returns (uint256) { + return affiliateTradingTokenFeePercent; + } + + /** + * @notice Getter to query referral threshold for paying out to the referrer. + * @dev It returns a value defined at protocol storage (State.sol) + * @return The minimum number of referrals set by Protocol. + */ + function getMinReferralsToPayout() public view returns (uint256) { + return minReferralsToPayout; + } + + /** + * @notice Get the sovToken reward of a trade. + * @dev The reward is worth x% of the trading fee. + * @param feeToken The address of the token in which the trading/borrowing fee was paid. + * @param feeAmount The height of the fee. + * @return The reward amount. + * */ + function _getSovBonusAmount(address feeToken, uint256 feeAmount) + internal + view + returns (uint256) + { + uint256 rewardAmount; + address _priceFeeds = priceFeeds; + + /// @dev Calculate the reward amount, querying the price feed. + (bool success, bytes memory data) = + _priceFeeds.staticcall( + abi.encodeWithSelector( + IPriceFeeds(_priceFeeds).queryReturn.selector, + feeToken, + sovTokenAddress, /// dest token = SOV + feeAmount.mul(_getAffiliatesTradingFeePercentForSOV()).div(1e20) + ) + ); + // solhint-disable-next-line no-inline-assembly + assembly { + if eq(success, 1) { + rewardAmount := mload(add(data, 32)) + } + } + + return rewardAmount; + } + + /** + * @notice Protocol calls this function to pay the affiliates rewards to a user (referrer). + * + * @dev Affiliates program has 2 kind of rewards: + * 1. x% based on the fee of the token that is traded (in form of the token itself). + * 2. x% based on the fee of the token that is traded (in form of SOV). + * Both are paid in this function. + * + * @dev Actually they are not paid, but just holded by protocol until user claims them by + * actively calling withdrawAffiliatesReferrerTokenFees() function, + * and/or when unvesting lockedSOV. + * + * @dev To be precise, what this function does is updating the registers of the rewards + * for the referrer including the assignment of the SOV tokens as rewards to the + * referrer's vesting contract. + * + * @param referrer The address of the referrer. + * @param trader The address of the trader. + * @param token The address of the token in which the trading/borrowing fee was paid. + * @param tradingFeeTokenBaseAmount Total trading fee amount, the base for calculating referrer's fees. + * + * @return referrerBonusSovAmount The amount of SOV tokens paid to the referrer (through a vesting contract, lockedSOV). + * @return referrerBonusTokenAmount The amount of trading tokens paid directly to the referrer. + */ + function payTradingFeeToAffiliatesReferrer( + address referrer, + address trader, + address token, + uint256 tradingFeeTokenBaseAmount + ) + external + onlyCallableInternal + whenNotPaused + returns (uint256 referrerBonusSovAmount, uint256 referrerBonusTokenAmount) + { + bool isHeld = referralsList[referrer].length() < getMinReferralsToPayout(); + bool bonusPaymentIsSuccess = true; + uint256 paidReferrerBonusSovAmount; + + /// Process token fee rewards first. + referrerBonusTokenAmount = _getReferrerTradingFeeForToken(tradingFeeTokenBaseAmount); + if (!affiliatesReferrerTokensList[referrer].contains(token)) + affiliatesReferrerTokensList[referrer].add(token); + affiliatesReferrerBalances[referrer][token] = affiliatesReferrerBalances[referrer][token] + .add(referrerBonusTokenAmount); + + /// Then process SOV rewards. + referrerBonusSovAmount = _getSovBonusAmount(token, tradingFeeTokenBaseAmount); + uint256 rewardsHeldByProtocol = affiliateRewardsHeld[referrer]; + + if (isHeld) { + /// If referrals less than minimum, temp the rewards SOV to the storage + affiliateRewardsHeld[referrer] = rewardsHeldByProtocol.add(referrerBonusSovAmount); + } else { + /// If referrals >= minimum, directly send all of the remain rewards to locked sov + /// Call depositSOV() in LockedSov contract + /// Set the affiliaterewardsheld = 0 + if (affiliateRewardsHeld[referrer] > 0) { + affiliateRewardsHeld[referrer] = 0; + } + + paidReferrerBonusSovAmount = referrerBonusSovAmount.add(rewardsHeldByProtocol); + IERC20(sovTokenAddress).approve(lockedSOVAddress, paidReferrerBonusSovAmount); + + (bool success, ) = + lockedSOVAddress.call( + abi.encodeWithSignature( + "depositSOV(address,uint256)", + referrer, + paidReferrerBonusSovAmount + ) + ); + + if (!success) { + bonusPaymentIsSuccess = false; + } + } + + if (bonusPaymentIsSuccess) { + emit PayTradingFeeToAffiliate( + referrer, + trader, // trader + token, + isHeld, + tradingFeeTokenBaseAmount, + referrerBonusTokenAmount, + referrerBonusSovAmount, + paidReferrerBonusSovAmount + ); + } else { + emit PayTradingFeeToAffiliateFail( + referrer, + trader, // trader + token, + tradingFeeTokenBaseAmount, + referrerBonusTokenAmount, + referrerBonusSovAmount, + paidReferrerBonusSovAmount + ); + } + + return (referrerBonusSovAmount, referrerBonusTokenAmount); + } + + /** + * @notice Referrer calls this function to receive its reward in a given token. + * It will send the other (non-SOV) reward tokens from trading protocol fees, + * to the referrer’s wallet. + * @dev Rewards are held by protocol in different tokens coming from trading fees. + * Referrer has to claim them one by one for every token with accumulated balance. + * @param token The address of the token to withdraw. + * @param receiver The address of the withdrawal beneficiary. + * @param amount The amount of tokens to claim. If greater than balance, just sends balance. + */ + function withdrawAffiliatesReferrerTokenFees( + address token, + address receiver, + uint256 amount + ) public whenNotPaused { + require(receiver != address(0), "Affiliates: cannot withdraw to zero address"); + address referrer = msg.sender; + uint256 referrerTokenBalance = affiliatesReferrerBalances[referrer][token]; + uint256 withdrawAmount = referrerTokenBalance > amount ? amount : referrerTokenBalance; + + require(withdrawAmount > 0, "Affiliates: cannot withdraw zero amount"); + + require( + referralsList[referrer].length() >= getMinReferralsToPayout(), + "Your referrals has not reached the minimum request" + ); + + uint256 newReferrerTokenBalance = referrerTokenBalance.sub(withdrawAmount); + + if (newReferrerTokenBalance == 0) { + _removeAffiliatesReferrerToken(referrer, token); + } else { + affiliatesReferrerBalances[referrer][token] = newReferrerTokenBalance; + } + + IERC20(token).safeTransfer(receiver, withdrawAmount); + + emit WithdrawAffiliatesReferrerTokenFees(referrer, receiver, token, withdrawAmount); + } + + /** + * @notice Withdraw to msg.sender all token fees for a referrer. + * @dev It's done by looping through its available tokens. + * @param receiver The address of the withdrawal beneficiary. + */ + function withdrawAllAffiliatesReferrerTokenFees(address receiver) external whenNotPaused { + require(receiver != address(0), "Affiliates: cannot withdraw to zero address"); + address referrer = msg.sender; + + require( + referralsList[referrer].length() >= getMinReferralsToPayout(), + "Your referrals has not reached the minimum request" + ); + + (address[] memory tokenAddresses, uint256[] memory tokenBalances) = + getAffiliatesReferrerBalances(referrer); + for (uint256 i; i < tokenAddresses.length; i++) { + withdrawAffiliatesReferrerTokenFees(tokenAddresses[i], receiver, tokenBalances[i]); + } + } + + /** + * @notice Internal function to delete a referrer's token balance. + * @param referrer The address of the referrer. + * @param token The address of the token specifying the balance to remove. + */ + function _removeAffiliatesReferrerToken(address referrer, address token) internal { + delete affiliatesReferrerBalances[referrer][token]; + affiliatesReferrerTokensList[referrer].remove(token); + } + + /** + * @notice Get all token balances of a referrer. + * @param referrer The address of the referrer. + * @return referrerTokensList The array of available tokens (keys). + * @return referrerTokensBalances The array of token balances (values). + */ + function getAffiliatesReferrerBalances(address referrer) + public + view + returns (address[] memory referrerTokensList, uint256[] memory referrerTokensBalances) + { + referrerTokensList = getAffiliatesReferrerTokensList(referrer); + referrerTokensBalances = new uint256[](referrerTokensList.length); + for (uint256 i; i < referrerTokensList.length; i++) { + referrerTokensBalances[i] = getAffiliatesReferrerTokenBalance( + referrer, + referrerTokensList[i] + ); + } + return (referrerTokensList, referrerTokensBalances); + } + + /** + * @dev Get all token rewards estimation value in rbtc. + * + * @param referrer Address of referrer. + * + * @return The value estimation in rbtc. + */ + function getAffiliatesTokenRewardsValueInRbtc(address referrer) + external + view + returns (uint256 rbtcTotalAmount) + { + address[] memory tokensList = getAffiliatesReferrerTokensList(referrer); + address _priceFeeds = priceFeeds; + + for (uint256 i; i < tokensList.length; i++) { + // Get the value of each token in rbtc + + (bool success, bytes memory data) = + _priceFeeds.staticcall( + abi.encodeWithSelector( + IPriceFeeds(_priceFeeds).queryReturn.selector, + tokensList[i], // source token + address(wrbtcToken), // dest token = SOV + affiliatesReferrerBalances[referrer][tokensList[i]] // total token rewards + ) + ); + + assembly { + if eq(success, 1) { + rbtcTotalAmount := add(rbtcTotalAmount, mload(add(data, 32))) + } + } + } + } + + /** + * @notice Get all available tokens at the affiliates program for a given referrer. + * @param referrer The address of a given referrer. + * @return tokensList The list of available tokens. + */ + function getAffiliatesReferrerTokensList(address referrer) + public + view + returns (address[] memory tokensList) + { + tokensList = affiliatesReferrerTokensList[referrer].enumerate(); + return tokensList; + } + + /** + * @notice Getter to query the affiliate balance for a given referrer and token. + * @param referrer The address of the referrer. + * @param token The address of the token to get balance for. + * @return The affiliatesReferrerBalances mapping value by referrer and token keys. + */ + function getAffiliatesReferrerTokenBalance(address referrer, address token) + public + view + returns (uint256) + { + return affiliatesReferrerBalances[referrer][token]; + } + + /** + * @notice Getter to query the address of referrer for a given user. + * @param user The address of the user. + * @return The address on affiliatesUserReferrer mapping value by user key. + */ + function getAffiliatesUserReferrer(address user) public view returns (address) { + return affiliatesUserReferrer[user]; + } + + /** + * @notice Getter to query the reward amount held for a given referrer. + * @param referrer The address of the referrer. + * @return The affiliateRewardsHeld mapping value by referrer key. + */ + function getAffiliateRewardsHeld(address referrer) public view returns (uint256) { + return affiliateRewardsHeld[referrer]; + } } diff --git a/contracts/modules/LoanClosingsLiquidation.sol b/contracts/modules/LoanClosingsLiquidation.sol index 6bea601cf..6c1ebb471 100644 --- a/contracts/modules/LoanClosingsLiquidation.sol +++ b/contracts/modules/LoanClosingsLiquidation.sol @@ -18,211 +18,229 @@ import "./LoanClosingsShared.sol"; * Loans are liquidated if the position goes below margin maintenance. * */ contract LoanClosingsLiquidation is LoanClosingsShared, LiquidationHelper { - uint256 internal constant MONTH = 365 days / 12; - - constructor() public {} - - function() external { - revert("fallback not allowed"); - } - - function initialize(address target) external onlyOwner { - address prevModuleContractAddress = logicTargets[this.liquidate.selector]; - _setTarget(this.liquidate.selector, target); - emit ProtocolModuleContractReplaced(prevModuleContractAddress, target, "LoanClosingsLiquidation"); - } - - /** - * @notice Liquidate an unhealty loan. - * - * @dev Public wrapper for _liquidate internal function. - * - * The caller needs to approve the closeAmount prior to calling. Will - * not liquidate more than is needed to restore the desired margin - * (maintenance +5%). - * - * Whenever the current margin of a loan falls below maintenance margin, - * it needs to be liquidated. Anybody can initiate a liquidation and buy - * the collateral tokens at a discounted rate (5%). - * - * @param loanId The ID of the loan to liquidate. - * loanId is the ID of the loan, which is created on loan opening. - * It can be obtained either by parsing the Trade event or by reading - * the open loans from the contract by calling getActiveLoans or getUserLoans. - * @param receiver The receiver of the seized amount. - * @param closeAmount The amount to close in loanTokens. - * - * @return loanCloseAmount The amount of the collateral token of the loan. - * @return seizedAmount The seized amount in the collateral token. - * @return seizedToken The loan token address. - * */ - function liquidate( - bytes32 loanId, - address receiver, - uint256 closeAmount // denominated in loanToken - ) - external - payable - nonReentrant - whenNotPaused - returns ( - uint256 loanCloseAmount, - uint256 seizedAmount, - address seizedToken - ) - { - return _liquidate(loanId, receiver, closeAmount); - } - - /** - * @notice Internal function for liquidating an unhealthy loan. - * - * The caller needs to approve the closeAmount prior to calling. Will - * not liquidate more than is needed to restore the desired margin - * (maintenance +5%). - * - * Whenever the current margin of a loan falls below maintenance margin, - * it needs to be liquidated. Anybody can initiate a liquidation and buy - * the collateral tokens at a discounted rate (5%). - * - * @param loanId The ID of the loan to liquidate. - * @param receiver The receiver of the seized amount. - * @param closeAmount The amount to close in loanTokens. - * - * @return loanCloseAmount The amount of the collateral token of the loan. - * @return seizedAmount The seized amount in the collateral token. - * @return seizedToken The loan token address. - * */ - function _liquidate( - bytes32 loanId, - address receiver, - uint256 closeAmount - ) - internal - returns ( - uint256 loanCloseAmount, - uint256 seizedAmount, - address seizedToken - ) - { - (Loan storage loanLocal, LoanParams storage loanParamsLocal) = _checkLoan(loanId); - - (uint256 currentMargin, uint256 collateralToLoanRate) = - IPriceFeeds(priceFeeds).getCurrentMargin( - loanParamsLocal.loanToken, - loanParamsLocal.collateralToken, - loanLocal.principal, - loanLocal.collateral - ); - require(currentMargin <= loanParamsLocal.maintenanceMargin, "healthy position"); - - loanCloseAmount = closeAmount; - - //amounts to restore the desired margin (maintencance + 5%) - (uint256 maxLiquidatable, uint256 maxSeizable, ) = - _getLiquidationAmounts( - loanLocal.principal, - loanLocal.collateral, - currentMargin, - loanParamsLocal.maintenanceMargin, - collateralToLoanRate - ); - - if (loanCloseAmount < maxLiquidatable) { - //close maxLiquidatable if tiny position will remain - uint256 remainingAmount = maxLiquidatable - loanCloseAmount; - remainingAmount = _getAmountInRbtc(loanParamsLocal.loanToken, remainingAmount); - if (remainingAmount <= TINY_AMOUNT) { - loanCloseAmount = maxLiquidatable; - seizedAmount = maxSeizable; - } else { - seizedAmount = maxSeizable.mul(loanCloseAmount).div(maxLiquidatable); - } - } else if (loanCloseAmount > maxLiquidatable) { - // adjust down the close amount to the max - loanCloseAmount = maxLiquidatable; - seizedAmount = maxSeizable; - } else { - seizedAmount = maxSeizable; - } - - require(loanCloseAmount != 0, "nothing to liquidate"); - - // liquidator deposits the principal being closed - _returnPrincipalWithDeposit(loanParamsLocal.loanToken, address(this), loanCloseAmount); - - // a portion of the principal is repaid to the lender out of interest refunded - uint256 loanCloseAmountLessInterest = _settleInterestToPrincipal(loanLocal, loanParamsLocal, loanCloseAmount, loanLocal.borrower); - - if (loanCloseAmount > loanCloseAmountLessInterest) { - // full interest refund goes to the borrower - _withdrawAsset(loanParamsLocal.loanToken, loanLocal.borrower, loanCloseAmount - loanCloseAmountLessInterest); - } - - if (loanCloseAmountLessInterest != 0) { - // The lender always gets back an ERC20 (even wrbtc), so we call withdraw directly rather than - // use the _withdrawAsset helper function - vaultWithdraw(loanParamsLocal.loanToken, loanLocal.lender, loanCloseAmountLessInterest); - } - - seizedToken = loanParamsLocal.collateralToken; - - if (seizedAmount != 0) { - loanLocal.collateral = loanLocal.collateral.sub(seizedAmount); - - _withdrawAsset(seizedToken, receiver, seizedAmount); - } - - _closeLoan(loanLocal, loanCloseAmount); - - _emitClosingEvents( - loanParamsLocal, - loanLocal, - loanCloseAmount, - seizedAmount, - collateralToLoanRate, - 0, - currentMargin, - CloseTypes.Liquidation - ); - } - - /** - * @notice Swap back excessive loan tokens to collateral tokens. - * - * @param loanLocal The loan object. - * @param loanParamsLocal The loan parameters. - * @param swapAmount The amount to be swapped. - * @param loanDataBytes Additional loan data (not in use for token swaps). - * - * @return destTokenAmountReceived The amount of destiny tokens received. - * @return sourceTokenAmountUsed The amount of source tokens used. - * @return collateralToLoanSwapRate The swap rate of collateral. - * */ - function _swapBackExcess( - Loan memory loanLocal, - LoanParams memory loanParamsLocal, - uint256 swapAmount, - bytes memory loanDataBytes - ) - internal - returns ( - uint256 destTokenAmountReceived, - uint256 sourceTokenAmountUsed, - uint256 collateralToLoanSwapRate - ) - { - (destTokenAmountReceived, sourceTokenAmountUsed, collateralToLoanSwapRate) = _loanSwap( - loanLocal.id, - loanParamsLocal.loanToken, - loanParamsLocal.collateralToken, - loanLocal.borrower, - swapAmount, // minSourceTokenAmount - swapAmount, // maxSourceTokenAmount - 0, // requiredDestTokenAmount - false, // bypassFee - loanDataBytes - ); - require(sourceTokenAmountUsed <= swapAmount, "excessive source amount"); - } + uint256 internal constant MONTH = 365 days / 12; + + constructor() public {} + + function() external { + revert("fallback not allowed"); + } + + function initialize(address target) external onlyOwner { + address prevModuleContractAddress = logicTargets[this.liquidate.selector]; + _setTarget(this.liquidate.selector, target); + emit ProtocolModuleContractReplaced( + prevModuleContractAddress, + target, + "LoanClosingsLiquidation" + ); + } + + /** + * @notice Liquidate an unhealty loan. + * + * @dev Public wrapper for _liquidate internal function. + * + * The caller needs to approve the closeAmount prior to calling. Will + * not liquidate more than is needed to restore the desired margin + * (maintenance +5%). + * + * Whenever the current margin of a loan falls below maintenance margin, + * it needs to be liquidated. Anybody can initiate a liquidation and buy + * the collateral tokens at a discounted rate (5%). + * + * @param loanId The ID of the loan to liquidate. + * loanId is the ID of the loan, which is created on loan opening. + * It can be obtained either by parsing the Trade event or by reading + * the open loans from the contract by calling getActiveLoans or getUserLoans. + * @param receiver The receiver of the seized amount. + * @param closeAmount The amount to close in loanTokens. + * + * @return loanCloseAmount The amount of the collateral token of the loan. + * @return seizedAmount The seized amount in the collateral token. + * @return seizedToken The loan token address. + * */ + function liquidate( + bytes32 loanId, + address receiver, + uint256 closeAmount // denominated in loanToken + ) + external + payable + nonReentrant + whenNotPaused + returns ( + uint256 loanCloseAmount, + uint256 seizedAmount, + address seizedToken + ) + { + return _liquidate(loanId, receiver, closeAmount); + } + + /** + * @notice Internal function for liquidating an unhealthy loan. + * + * The caller needs to approve the closeAmount prior to calling. Will + * not liquidate more than is needed to restore the desired margin + * (maintenance +5%). + * + * Whenever the current margin of a loan falls below maintenance margin, + * it needs to be liquidated. Anybody can initiate a liquidation and buy + * the collateral tokens at a discounted rate (5%). + * + * @param loanId The ID of the loan to liquidate. + * @param receiver The receiver of the seized amount. + * @param closeAmount The amount to close in loanTokens. + * + * @return loanCloseAmount The amount of the collateral token of the loan. + * @return seizedAmount The seized amount in the collateral token. + * @return seizedToken The loan token address. + * */ + function _liquidate( + bytes32 loanId, + address receiver, + uint256 closeAmount + ) + internal + returns ( + uint256 loanCloseAmount, + uint256 seizedAmount, + address seizedToken + ) + { + (Loan storage loanLocal, LoanParams storage loanParamsLocal) = _checkLoan(loanId); + + (uint256 currentMargin, uint256 collateralToLoanRate) = + IPriceFeeds(priceFeeds).getCurrentMargin( + loanParamsLocal.loanToken, + loanParamsLocal.collateralToken, + loanLocal.principal, + loanLocal.collateral + ); + require(currentMargin <= loanParamsLocal.maintenanceMargin, "healthy position"); + + loanCloseAmount = closeAmount; + + //amounts to restore the desired margin (maintencance + 5%) + (uint256 maxLiquidatable, uint256 maxSeizable, ) = + _getLiquidationAmounts( + loanLocal.principal, + loanLocal.collateral, + currentMargin, + loanParamsLocal.maintenanceMargin, + collateralToLoanRate + ); + + if (loanCloseAmount < maxLiquidatable) { + //close maxLiquidatable if tiny position will remain + uint256 remainingAmount = maxLiquidatable - loanCloseAmount; + remainingAmount = _getAmountInRbtc(loanParamsLocal.loanToken, remainingAmount); + if (remainingAmount <= TINY_AMOUNT) { + loanCloseAmount = maxLiquidatable; + seizedAmount = maxSeizable; + } else { + seizedAmount = maxSeizable.mul(loanCloseAmount).div(maxLiquidatable); + } + } else if (loanCloseAmount > maxLiquidatable) { + // adjust down the close amount to the max + loanCloseAmount = maxLiquidatable; + seizedAmount = maxSeizable; + } else { + seizedAmount = maxSeizable; + } + + require(loanCloseAmount != 0, "nothing to liquidate"); + + // liquidator deposits the principal being closed + _returnPrincipalWithDeposit(loanParamsLocal.loanToken, address(this), loanCloseAmount); + + // a portion of the principal is repaid to the lender out of interest refunded + uint256 loanCloseAmountLessInterest = + _settleInterestToPrincipal( + loanLocal, + loanParamsLocal, + loanCloseAmount, + loanLocal.borrower + ); + + if (loanCloseAmount > loanCloseAmountLessInterest) { + // full interest refund goes to the borrower + _withdrawAsset( + loanParamsLocal.loanToken, + loanLocal.borrower, + loanCloseAmount - loanCloseAmountLessInterest + ); + } + + if (loanCloseAmountLessInterest != 0) { + // The lender always gets back an ERC20 (even wrbtc), so we call withdraw directly rather than + // use the _withdrawAsset helper function + vaultWithdraw( + loanParamsLocal.loanToken, + loanLocal.lender, + loanCloseAmountLessInterest + ); + } + + seizedToken = loanParamsLocal.collateralToken; + + if (seizedAmount != 0) { + loanLocal.collateral = loanLocal.collateral.sub(seizedAmount); + + _withdrawAsset(seizedToken, receiver, seizedAmount); + } + + _closeLoan(loanLocal, loanCloseAmount); + + _emitClosingEvents( + loanParamsLocal, + loanLocal, + loanCloseAmount, + seizedAmount, + collateralToLoanRate, + 0, + currentMargin, + CloseTypes.Liquidation + ); + } + + /** + * @notice Swap back excessive loan tokens to collateral tokens. + * + * @param loanLocal The loan object. + * @param loanParamsLocal The loan parameters. + * @param swapAmount The amount to be swapped. + * @param loanDataBytes Additional loan data (not in use for token swaps). + * + * @return destTokenAmountReceived The amount of destiny tokens received. + * @return sourceTokenAmountUsed The amount of source tokens used. + * @return collateralToLoanSwapRate The swap rate of collateral. + * */ + function _swapBackExcess( + Loan memory loanLocal, + LoanParams memory loanParamsLocal, + uint256 swapAmount, + bytes memory loanDataBytes + ) + internal + returns ( + uint256 destTokenAmountReceived, + uint256 sourceTokenAmountUsed, + uint256 collateralToLoanSwapRate + ) + { + (destTokenAmountReceived, sourceTokenAmountUsed, collateralToLoanSwapRate) = _loanSwap( + loanLocal.id, + loanParamsLocal.loanToken, + loanParamsLocal.collateralToken, + loanLocal.borrower, + swapAmount, // minSourceTokenAmount + swapAmount, // maxSourceTokenAmount + 0, // requiredDestTokenAmount + false, // bypassFee + loanDataBytes + ); + require(sourceTokenAmountUsed <= swapAmount, "excessive source amount"); + } } diff --git a/contracts/modules/LoanClosingsRollover.sol b/contracts/modules/LoanClosingsRollover.sol index 3cb2cbe1e..193c086b8 100644 --- a/contracts/modules/LoanClosingsRollover.sol +++ b/contracts/modules/LoanClosingsRollover.sol @@ -17,276 +17,302 @@ import "./LoanClosingsShared.sol"; * * */ contract LoanClosingsRollover is LoanClosingsShared, LiquidationHelper { - uint256 internal constant MONTH = 365 days / 12; - - constructor() public {} - - function() external { - revert("fallback not allowed"); - } - - function initialize(address target) external onlyOwner { - address prevModuleContractAddress = logicTargets[this.rollover.selector]; - _setTarget(this.rollover.selector, target); - emit ProtocolModuleContractReplaced(prevModuleContractAddress, target, "LoanClosingsRollover"); - } - - /** - * @notice Roll over a loan. - * - * @dev Public wrapper for _rollover internal function. - * - * Each loan has a duration. In case of a margin trade it is set to 28 - * days, in case of borrowing, it can be set by the user. On loan - * openning, the user pays the interest for this duration in advance. - * If closing early, he gets the excess refunded. If it is not closed - * before the end date, it needs to be rolled over. On rollover the - * interest is paid for the next period. In case of margin trading - * it's 28 days, in case of borrowing it's a month. - * - * The function rollover on the protocol contract extends the loan - * duration by the maximum term (28 days for margin trades at the moment - * of writing), pays the interest to the lender and refunds the caller - * for the gas cost by sending 2 * the gas cost using the fast gas price - * as base for the calculation. - * - * @param loanId The ID of the loan to roll over. - * // param calldata The payload for the call. These loan DataBytes are additional loan data (not in use for token swaps). - * */ - function rollover( - bytes32 loanId, - bytes calldata // for future use /*loanDataBytes*/ - ) external nonReentrant whenNotPaused { - // restrict to EOAs to prevent griefing attacks, during interest rate recalculation - require(msg.sender == tx.origin, "EOAs call"); - - return - _rollover( - loanId, - "" // loanDataBytes - ); - } - - /** - * @notice Internal function for roll over a loan. - * - * Each loan has a duration. In case of a margin trade it is set to 28 - * days, in case of borrowing, it can be set by the user. On loan - * openning, the user pays the interest for this duration in advance. - * If closing early, he gets the excess refunded. If it is not closed - * before the end date, it needs to be rolled over. On rollover the - * interest is paid for the next period. In case of margin trading - * it's 28 days, in case of borrowing it's a month. - * - * @param loanId The ID of the loan to roll over. - * @param loanDataBytes The payload for the call. These loan DataBytes are - * additional loan data (not in use for token swaps). - * */ - function _rollover(bytes32 loanId, bytes memory loanDataBytes) internal { - (Loan storage loanLocal, LoanParams storage loanParamsLocal) = _checkLoan(loanId); - require(block.timestamp > loanLocal.endTimestamp.sub(3600), "healthy position"); - require(loanPoolToUnderlying[loanLocal.lender] != address(0), "invalid lender"); - - // pay outstanding interest to lender - _payInterest(loanLocal.lender, loanParamsLocal.loanToken); - - LoanInterest storage loanInterestLocal = loanInterest[loanLocal.id]; - LenderInterest storage lenderInterestLocal = lenderInterest[loanLocal.lender][loanParamsLocal.loanToken]; - - _settleFeeRewardForInterestExpense( - loanInterestLocal, - loanLocal.id, - loanParamsLocal.loanToken, /// fee token - loanParamsLocal.collateralToken, /// pairToken (used to check if there is any special rebates or not) -- to pay fee reward - loanLocal.borrower, - block.timestamp - ); - - // Handle back interest: calculates interest owned since the loan endtime passed but the loan remained open - uint256 backInterestTime; - uint256 backInterestOwed; - if (block.timestamp > loanLocal.endTimestamp) { - backInterestTime = block.timestamp.sub(loanLocal.endTimestamp); - backInterestOwed = backInterestTime.mul(loanInterestLocal.owedPerDay); - backInterestOwed = backInterestOwed.div(1 days); - } - - //note: to avoid code duplication, it would be nicer to store loanParamsLocal.maxLoanTerm in a local variable - //however, we've got stack too deep issues if we do so. - if (loanParamsLocal.maxLoanTerm != 0) { - // fixed-term loan, so need to query iToken for latest variable rate - uint256 owedPerDay = loanLocal.principal.mul(ILoanPool(loanLocal.lender).borrowInterestRate()).div(365 * 10**20); - - lenderInterestLocal.owedPerDay = lenderInterestLocal.owedPerDay.add(owedPerDay); - lenderInterestLocal.owedPerDay = lenderInterestLocal.owedPerDay.sub(loanInterestLocal.owedPerDay); - - loanInterestLocal.owedPerDay = owedPerDay; - - //if the loan has been open for longer than an additional period, add at least 1 additional day - if (backInterestTime >= loanParamsLocal.maxLoanTerm) { - loanLocal.endTimestamp = loanLocal.endTimestamp.add(backInterestTime).add(1 days); - } - //extend by the max loan term - else { - loanLocal.endTimestamp = loanLocal.endTimestamp.add(loanParamsLocal.maxLoanTerm); - } - } else { - // loanInterestLocal.owedPerDay doesn't change - if (backInterestTime >= MONTH) { - loanLocal.endTimestamp = loanLocal.endTimestamp.add(backInterestTime).add(1 days); - } else { - loanLocal.endTimestamp = loanLocal.endTimestamp.add(MONTH); - } - } - - uint256 interestAmountRequired = loanLocal.endTimestamp.sub(block.timestamp); - interestAmountRequired = interestAmountRequired.mul(loanInterestLocal.owedPerDay); - interestAmountRequired = interestAmountRequired.div(1 days); - - loanInterestLocal.depositTotal = loanInterestLocal.depositTotal.add(interestAmountRequired); - - lenderInterestLocal.owedTotal = lenderInterestLocal.owedTotal.add(interestAmountRequired); - - // add backInterestOwed - interestAmountRequired = interestAmountRequired.add(backInterestOwed); - - // collect interest (needs to be converted from the collateral) - (uint256 destTokenAmountReceived, uint256 sourceTokenAmountUsed, ) = - _doCollateralSwap( - loanLocal, - loanParamsLocal, - 0, //min swap 0 -> swap connector estimates the amount of source tokens to use - interestAmountRequired, //required destination tokens - true, // returnTokenIsCollateral - loanDataBytes - ); - - //received more tokens than needed to pay the interest - if (destTokenAmountReceived > interestAmountRequired) { - // swap rest back to collateral, if the amount is big enough to cover gas cost - if (worthTheTransfer(loanParamsLocal.loanToken, destTokenAmountReceived - interestAmountRequired)) { - (destTokenAmountReceived, , ) = _swapBackExcess( - loanLocal, - loanParamsLocal, - destTokenAmountReceived - interestAmountRequired, //amount to be swapped - loanDataBytes - ); - sourceTokenAmountUsed = sourceTokenAmountUsed.sub(destTokenAmountReceived); - } - //else give it to the protocol as a lending fee - else { - _payLendingFee(loanLocal.borrower, loanParamsLocal.loanToken, destTokenAmountReceived - interestAmountRequired); - } - } - - //subtract the interest from the collateral - loanLocal.collateral = loanLocal.collateral.sub(sourceTokenAmountUsed); - - if (backInterestOwed != 0) { - // pay out backInterestOwed - - _payInterestTransfer(loanLocal.lender, loanParamsLocal.loanToken, backInterestOwed); - } - - uint256 rolloverReward = _getRolloverReward(loanParamsLocal.collateralToken, loanParamsLocal.loanToken, loanLocal.principal); - - if (rolloverReward != 0) { - // if the reward > collateral: - if (rolloverReward > loanLocal.collateral) { - // 1. pay back the remaining loan to the lender - // 2. pay the remaining collateral to msg.sender - // 3. close the position & emit close event - _closeWithSwap( - loanLocal.id, - msg.sender, - loanLocal.collateral, - false, - "" // loanDataBytes - ); - } else { - // pay out reward to caller - loanLocal.collateral = loanLocal.collateral.sub(rolloverReward); - - _withdrawAsset(loanParamsLocal.collateralToken, msg.sender, rolloverReward); - } - } - - if (loanLocal.collateral > 0) { - //close whole loan if tiny position will remain - if (_getAmountInRbtc(loanParamsLocal.loanToken, loanLocal.principal) <= TINY_AMOUNT) { - _closeWithSwap( - loanLocal.id, - loanLocal.borrower, - loanLocal.collateral, // swap all collaterals - false, - "" /// loanDataBytes - ); - } else { - (uint256 currentMargin, ) = - IPriceFeeds(priceFeeds).getCurrentMargin( - loanParamsLocal.loanToken, - loanParamsLocal.collateralToken, - loanLocal.principal, - loanLocal.collateral - ); - - require( - currentMargin > 3 ether, // ensure there's more than 3% margin remaining - "unhealthy position" - ); - } - } - - if (loanLocal.active) { - emit Rollover( - loanLocal.borrower, // user (borrower) - loanLocal.lender, // lender - loanLocal.id, // loanId - loanLocal.principal, // principal - loanLocal.collateral, // collateral - loanLocal.endTimestamp, // endTimestamp - msg.sender, // rewardReceiver - rolloverReward // reward - ); - } - } - - /** - * @notice Swap back excessive loan tokens to collateral tokens. - * - * @param loanLocal The loan object. - * @param loanParamsLocal The loan parameters. - * @param swapAmount The amount to be swapped. - * @param loanDataBytes Additional loan data (not in use for token swaps). - * - * @return destTokenAmountReceived The amount of destiny tokens received. - * @return sourceTokenAmountUsed The amount of source tokens used. - * @return collateralToLoanSwapRate The swap rate of collateral. - * */ - function _swapBackExcess( - Loan memory loanLocal, - LoanParams memory loanParamsLocal, - uint256 swapAmount, - bytes memory loanDataBytes - ) - internal - returns ( - uint256 destTokenAmountReceived, - uint256 sourceTokenAmountUsed, - uint256 collateralToLoanSwapRate - ) - { - (destTokenAmountReceived, sourceTokenAmountUsed, collateralToLoanSwapRate) = _loanSwap( - loanLocal.id, - loanParamsLocal.loanToken, - loanParamsLocal.collateralToken, - loanLocal.borrower, - swapAmount, // minSourceTokenAmount - swapAmount, // maxSourceTokenAmount - 0, // requiredDestTokenAmount - false, // bypassFee - loanDataBytes - ); - require(sourceTokenAmountUsed <= swapAmount, "excessive source amount"); - } + uint256 internal constant MONTH = 365 days / 12; + + constructor() public {} + + function() external { + revert("fallback not allowed"); + } + + function initialize(address target) external onlyOwner { + address prevModuleContractAddress = logicTargets[this.rollover.selector]; + _setTarget(this.rollover.selector, target); + emit ProtocolModuleContractReplaced( + prevModuleContractAddress, + target, + "LoanClosingsRollover" + ); + } + + /** + * @notice Roll over a loan. + * + * @dev Public wrapper for _rollover internal function. + * + * Each loan has a duration. In case of a margin trade it is set to 28 + * days, in case of borrowing, it can be set by the user. On loan + * openning, the user pays the interest for this duration in advance. + * If closing early, he gets the excess refunded. If it is not closed + * before the end date, it needs to be rolled over. On rollover the + * interest is paid for the next period. In case of margin trading + * it's 28 days, in case of borrowing it's a month. + * + * The function rollover on the protocol contract extends the loan + * duration by the maximum term (28 days for margin trades at the moment + * of writing), pays the interest to the lender and refunds the caller + * for the gas cost by sending 2 * the gas cost using the fast gas price + * as base for the calculation. + * + * @param loanId The ID of the loan to roll over. + * // param calldata The payload for the call. These loan DataBytes are additional loan data (not in use for token swaps). + * */ + function rollover( + bytes32 loanId, + bytes calldata // for future use /*loanDataBytes*/ + ) external nonReentrant whenNotPaused { + // restrict to EOAs to prevent griefing attacks, during interest rate recalculation + require(msg.sender == tx.origin, "EOAs call"); + + return + _rollover( + loanId, + "" // loanDataBytes + ); + } + + /** + * @notice Internal function for roll over a loan. + * + * Each loan has a duration. In case of a margin trade it is set to 28 + * days, in case of borrowing, it can be set by the user. On loan + * openning, the user pays the interest for this duration in advance. + * If closing early, he gets the excess refunded. If it is not closed + * before the end date, it needs to be rolled over. On rollover the + * interest is paid for the next period. In case of margin trading + * it's 28 days, in case of borrowing it's a month. + * + * @param loanId The ID of the loan to roll over. + * @param loanDataBytes The payload for the call. These loan DataBytes are + * additional loan data (not in use for token swaps). + * */ + function _rollover(bytes32 loanId, bytes memory loanDataBytes) internal { + (Loan storage loanLocal, LoanParams storage loanParamsLocal) = _checkLoan(loanId); + require(block.timestamp > loanLocal.endTimestamp.sub(3600), "healthy position"); + require(loanPoolToUnderlying[loanLocal.lender] != address(0), "invalid lender"); + + // pay outstanding interest to lender + _payInterest(loanLocal.lender, loanParamsLocal.loanToken); + + LoanInterest storage loanInterestLocal = loanInterest[loanLocal.id]; + LenderInterest storage lenderInterestLocal = + lenderInterest[loanLocal.lender][loanParamsLocal.loanToken]; + + _settleFeeRewardForInterestExpense( + loanInterestLocal, + loanLocal.id, + loanParamsLocal.loanToken, /// fee token + loanParamsLocal.collateralToken, /// pairToken (used to check if there is any special rebates or not) -- to pay fee reward + loanLocal.borrower, + block.timestamp + ); + + // Handle back interest: calculates interest owned since the loan endtime passed but the loan remained open + uint256 backInterestTime; + uint256 backInterestOwed; + if (block.timestamp > loanLocal.endTimestamp) { + backInterestTime = block.timestamp.sub(loanLocal.endTimestamp); + backInterestOwed = backInterestTime.mul(loanInterestLocal.owedPerDay); + backInterestOwed = backInterestOwed.div(1 days); + } + + //note: to avoid code duplication, it would be nicer to store loanParamsLocal.maxLoanTerm in a local variable + //however, we've got stack too deep issues if we do so. + if (loanParamsLocal.maxLoanTerm != 0) { + // fixed-term loan, so need to query iToken for latest variable rate + uint256 owedPerDay = + loanLocal.principal.mul(ILoanPool(loanLocal.lender).borrowInterestRate()).div( + 365 * 10**20 + ); + + lenderInterestLocal.owedPerDay = lenderInterestLocal.owedPerDay.add(owedPerDay); + lenderInterestLocal.owedPerDay = lenderInterestLocal.owedPerDay.sub( + loanInterestLocal.owedPerDay + ); + + loanInterestLocal.owedPerDay = owedPerDay; + + //if the loan has been open for longer than an additional period, add at least 1 additional day + if (backInterestTime >= loanParamsLocal.maxLoanTerm) { + loanLocal.endTimestamp = loanLocal.endTimestamp.add(backInterestTime).add(1 days); + } + //extend by the max loan term + else { + loanLocal.endTimestamp = loanLocal.endTimestamp.add(loanParamsLocal.maxLoanTerm); + } + } else { + // loanInterestLocal.owedPerDay doesn't change + if (backInterestTime >= MONTH) { + loanLocal.endTimestamp = loanLocal.endTimestamp.add(backInterestTime).add(1 days); + } else { + loanLocal.endTimestamp = loanLocal.endTimestamp.add(MONTH); + } + } + + uint256 interestAmountRequired = loanLocal.endTimestamp.sub(block.timestamp); + interestAmountRequired = interestAmountRequired.mul(loanInterestLocal.owedPerDay); + interestAmountRequired = interestAmountRequired.div(1 days); + + loanInterestLocal.depositTotal = loanInterestLocal.depositTotal.add( + interestAmountRequired + ); + + lenderInterestLocal.owedTotal = lenderInterestLocal.owedTotal.add(interestAmountRequired); + + // add backInterestOwed + interestAmountRequired = interestAmountRequired.add(backInterestOwed); + + // collect interest (needs to be converted from the collateral) + (uint256 destTokenAmountReceived, uint256 sourceTokenAmountUsed, ) = + _doCollateralSwap( + loanLocal, + loanParamsLocal, + 0, //min swap 0 -> swap connector estimates the amount of source tokens to use + interestAmountRequired, //required destination tokens + true, // returnTokenIsCollateral + loanDataBytes + ); + + //received more tokens than needed to pay the interest + if (destTokenAmountReceived > interestAmountRequired) { + // swap rest back to collateral, if the amount is big enough to cover gas cost + if ( + worthTheTransfer( + loanParamsLocal.loanToken, + destTokenAmountReceived - interestAmountRequired + ) + ) { + (destTokenAmountReceived, , ) = _swapBackExcess( + loanLocal, + loanParamsLocal, + destTokenAmountReceived - interestAmountRequired, //amount to be swapped + loanDataBytes + ); + sourceTokenAmountUsed = sourceTokenAmountUsed.sub(destTokenAmountReceived); + } + //else give it to the protocol as a lending fee + else { + _payLendingFee( + loanLocal.borrower, + loanParamsLocal.loanToken, + destTokenAmountReceived - interestAmountRequired + ); + } + } + + //subtract the interest from the collateral + loanLocal.collateral = loanLocal.collateral.sub(sourceTokenAmountUsed); + + if (backInterestOwed != 0) { + // pay out backInterestOwed + + _payInterestTransfer(loanLocal.lender, loanParamsLocal.loanToken, backInterestOwed); + } + + uint256 rolloverReward = + _getRolloverReward( + loanParamsLocal.collateralToken, + loanParamsLocal.loanToken, + loanLocal.principal + ); + + if (rolloverReward != 0) { + // if the reward > collateral: + if (rolloverReward > loanLocal.collateral) { + // 1. pay back the remaining loan to the lender + // 2. pay the remaining collateral to msg.sender + // 3. close the position & emit close event + _closeWithSwap( + loanLocal.id, + msg.sender, + loanLocal.collateral, + false, + "" // loanDataBytes + ); + } else { + // pay out reward to caller + loanLocal.collateral = loanLocal.collateral.sub(rolloverReward); + + _withdrawAsset(loanParamsLocal.collateralToken, msg.sender, rolloverReward); + } + } + + if (loanLocal.collateral > 0) { + //close whole loan if tiny position will remain + if (_getAmountInRbtc(loanParamsLocal.loanToken, loanLocal.principal) <= TINY_AMOUNT) { + _closeWithSwap( + loanLocal.id, + loanLocal.borrower, + loanLocal.collateral, // swap all collaterals + false, + "" /// loanDataBytes + ); + } else { + (uint256 currentMargin, ) = + IPriceFeeds(priceFeeds).getCurrentMargin( + loanParamsLocal.loanToken, + loanParamsLocal.collateralToken, + loanLocal.principal, + loanLocal.collateral + ); + + require( + currentMargin > 3 ether, // ensure there's more than 3% margin remaining + "unhealthy position" + ); + } + } + + if (loanLocal.active) { + emit Rollover( + loanLocal.borrower, // user (borrower) + loanLocal.lender, // lender + loanLocal.id, // loanId + loanLocal.principal, // principal + loanLocal.collateral, // collateral + loanLocal.endTimestamp, // endTimestamp + msg.sender, // rewardReceiver + rolloverReward // reward + ); + } + } + + /** + * @notice Swap back excessive loan tokens to collateral tokens. + * + * @param loanLocal The loan object. + * @param loanParamsLocal The loan parameters. + * @param swapAmount The amount to be swapped. + * @param loanDataBytes Additional loan data (not in use for token swaps). + * + * @return destTokenAmountReceived The amount of destiny tokens received. + * @return sourceTokenAmountUsed The amount of source tokens used. + * @return collateralToLoanSwapRate The swap rate of collateral. + * */ + function _swapBackExcess( + Loan memory loanLocal, + LoanParams memory loanParamsLocal, + uint256 swapAmount, + bytes memory loanDataBytes + ) + internal + returns ( + uint256 destTokenAmountReceived, + uint256 sourceTokenAmountUsed, + uint256 collateralToLoanSwapRate + ) + { + (destTokenAmountReceived, sourceTokenAmountUsed, collateralToLoanSwapRate) = _loanSwap( + loanLocal.id, + loanParamsLocal.loanToken, + loanParamsLocal.collateralToken, + loanLocal.borrower, + swapAmount, // minSourceTokenAmount + swapAmount, // maxSourceTokenAmount + 0, // requiredDestTokenAmount + false, // bypassFee + loanDataBytes + ); + require(sourceTokenAmountUsed <= swapAmount, "excessive source amount"); + } } diff --git a/contracts/modules/LoanClosingsShared.sol b/contracts/modules/LoanClosingsShared.sol index 1d6b62702..54be2acc5 100644 --- a/contracts/modules/LoanClosingsShared.sol +++ b/contracts/modules/LoanClosingsShared.sol @@ -20,643 +20,705 @@ import "../mixins/ModuleCommonFunctionalities.sol"; * LoanClosingsLiquidation, LoanClosingsRollover & LoanClosingsWith contract * * */ -contract LoanClosingsShared is LoanClosingsEvents, VaultController, InterestUser, SwapsUser, RewardHelper, ModuleCommonFunctionalities { - uint256 internal constant MONTH = 365 days / 12; - //0.00001 BTC, would be nicer in State.sol, but would require a redeploy of the complete protocol, so adding it here instead - //because it's not shared state anyway and only used by this contract - uint256 public constant paySwapExcessToBorrowerThreshold = 10000000000000; - - uint256 public constant TINY_AMOUNT = 25e13; - - enum CloseTypes { Deposit, Swap, Liquidation } - - /** - * @dev computes the interest which needs to be refunded to the borrower based on the amount he's closing and either - * subtracts it from the amount which still needs to be paid back (in case outstanding amount > interest) or withdraws the - * excess to the borrower (in case interest > outstanding). - * @param loanLocal the loan - * @param loanParamsLocal the loan params - * @param loanCloseAmount the amount to be closed (base for the computation) - * @param receiver the address of the receiver (usually the borrower) - * */ - function _settleInterestToPrincipal( - Loan memory loanLocal, - LoanParams memory loanParamsLocal, - uint256 loanCloseAmount, - address receiver - ) internal returns (uint256) { - uint256 loanCloseAmountLessInterest = loanCloseAmount; - - //compute the interest which neeeds to be refunded to the borrower (because full interest is paid on loan ) - uint256 interestRefundToBorrower = _settleInterest(loanParamsLocal, loanLocal, loanCloseAmountLessInterest); - - uint256 interestAppliedToPrincipal; - //if the outstanding loan is bigger than the interest to be refunded, reduce the amount to be paid back / closed by the interest - if (loanCloseAmountLessInterest >= interestRefundToBorrower) { - // apply all of borrower interest refund torwards principal - interestAppliedToPrincipal = interestRefundToBorrower; - - // principal needed is reduced by this amount - loanCloseAmountLessInterest -= interestRefundToBorrower; - - // no interest refund remaining - interestRefundToBorrower = 0; - } else { - //if the interest refund is bigger than the outstanding loan, the user needs to get back the interest - // principal fully covered by excess interest - interestAppliedToPrincipal = loanCloseAmountLessInterest; - - // amount refunded is reduced by this amount - interestRefundToBorrower -= loanCloseAmountLessInterest; - - // principal fully covered by excess interest - loanCloseAmountLessInterest = 0; - - if (interestRefundToBorrower != 0) { - // refund overage - _withdrawAsset(loanParamsLocal.loanToken, receiver, interestRefundToBorrower); - } - } - - //pay the interest to the lender - //note: this is a waste of gas, because the loanCloseAmountLessInterest is withdrawn to the lender, too. It could be done at once. - if (interestAppliedToPrincipal != 0) { - // The lender always gets back an ERC20 (even wrbtc), so we call withdraw directly rather than - // use the _withdrawAsset helper function - vaultWithdraw(loanParamsLocal.loanToken, loanLocal.lender, interestAppliedToPrincipal); - } - - return loanCloseAmountLessInterest; - } - - // The receiver always gets back an ERC20 (even wrbtc) - function _returnPrincipalWithDeposit( - address loanToken, - address receiver, - uint256 principalNeeded - ) internal { - if (principalNeeded != 0) { - if (msg.value == 0) { - vaultTransfer(loanToken, msg.sender, receiver, principalNeeded); - } else { - require(loanToken == address(wrbtcToken), "wrong asset sent"); - require(msg.value >= principalNeeded, "not enough ether"); - wrbtcToken.deposit.value(principalNeeded)(); - if (receiver != address(this)) { - vaultTransfer(loanToken, address(this), receiver, principalNeeded); - } - if (msg.value > principalNeeded) { - // refund overage - Address.sendValue(msg.sender, msg.value - principalNeeded); - } - } - } else { - require(msg.value == 0, "wrong asset sent"); - } - } - - /** - * @dev checks if the amount of the asset to be transfered is worth the transfer fee - * @param asset the asset to be transfered - * @param amount the amount to be transfered - * @return True if the amount is bigger than the threshold - * */ - function worthTheTransfer(address asset, uint256 amount) internal returns (bool) { - uint256 amountInRbtc = _getAmountInRbtc(asset, amount); - emit swapExcess(amountInRbtc > paySwapExcessToBorrowerThreshold, amount, amountInRbtc, paySwapExcessToBorrowerThreshold); - return amountInRbtc > paySwapExcessToBorrowerThreshold; - } - - /** - * swaps collateral tokens for loan tokens - * @param loanLocal the loan object - * @param loanParamsLocal the loan parameters - * @param swapAmount the amount to be swapped - * @param principalNeeded the required destination token amount - * @param returnTokenIsCollateral if true -> required destination token amount will be passed on, else not - * note: quite dirty. should be refactored. - * @param loanDataBytes additional loan data (not in use for token swaps) - * */ - function _doCollateralSwap( - Loan memory loanLocal, - LoanParams memory loanParamsLocal, - uint256 swapAmount, - uint256 principalNeeded, - bool returnTokenIsCollateral, - bytes memory loanDataBytes - ) - internal - returns ( - uint256 destTokenAmountReceived, - uint256 sourceTokenAmountUsed, - uint256 collateralToLoanSwapRate - ) - { - (destTokenAmountReceived, sourceTokenAmountUsed, collateralToLoanSwapRate) = _loanSwap( - loanLocal.id, - loanParamsLocal.collateralToken, - loanParamsLocal.loanToken, - loanLocal.borrower, - swapAmount, // minSourceTokenAmount - loanLocal.collateral, // maxSourceTokenAmount - returnTokenIsCollateral - ? principalNeeded // requiredDestTokenAmount - : 0, - false, // bypassFee - loanDataBytes - ); - require(destTokenAmountReceived >= principalNeeded, "insufficient dest amount"); - require(sourceTokenAmountUsed <= loanLocal.collateral, "excessive source amount"); - } - - /** - * @notice Withdraw asset to receiver. - * - * @param assetToken The loan token. - * @param receiver The address of the receiver. - * @param assetAmount The loan token amount. - * */ - function _withdrawAsset( - address assetToken, - address receiver, - uint256 assetAmount - ) internal { - if (assetAmount != 0) { - if (assetToken == address(wrbtcToken)) { - vaultEtherWithdraw(receiver, assetAmount); - } else { - vaultWithdraw(assetToken, receiver, assetAmount); - } - } - } - - /** - * @notice Internal function to close a loan. - * - * @param loanLocal The loan object. - * @param loanCloseAmount The amount to close: principal or lower. - * - * */ - function _closeLoan(Loan storage loanLocal, uint256 loanCloseAmount) internal { - require(loanCloseAmount != 0, "nothing to close"); - - if (loanCloseAmount == loanLocal.principal) { - loanLocal.principal = 0; - loanLocal.active = false; - loanLocal.endTimestamp = block.timestamp; - loanLocal.pendingTradesId = 0; - activeLoansSet.removeBytes32(loanLocal.id); - lenderLoanSets[loanLocal.lender].removeBytes32(loanLocal.id); - borrowerLoanSets[loanLocal.borrower].removeBytes32(loanLocal.id); - } else { - loanLocal.principal = loanLocal.principal.sub(loanCloseAmount); - } - } - - function _settleInterest( - LoanParams memory loanParamsLocal, - Loan memory loanLocal, - uint256 closePrincipal - ) internal returns (uint256) { - // pay outstanding interest to lender - _payInterest(loanLocal.lender, loanParamsLocal.loanToken); - - LoanInterest storage loanInterestLocal = loanInterest[loanLocal.id]; - LenderInterest storage lenderInterestLocal = lenderInterest[loanLocal.lender][loanParamsLocal.loanToken]; - - uint256 interestTime = block.timestamp; - if (interestTime > loanLocal.endTimestamp) { - interestTime = loanLocal.endTimestamp; - } - - _settleFeeRewardForInterestExpense( - loanInterestLocal, - loanLocal.id, - loanParamsLocal.loanToken, /// fee token - loanParamsLocal.collateralToken, /// pairToken (used to check if there is any special rebates or not) -- to pay fee reward - loanLocal.borrower, - interestTime - ); - - uint256 owedPerDayRefund; - if (closePrincipal < loanLocal.principal) { - owedPerDayRefund = loanInterestLocal.owedPerDay.mul(closePrincipal).div(loanLocal.principal); - } else { - owedPerDayRefund = loanInterestLocal.owedPerDay; - } - - // update stored owedPerDay - loanInterestLocal.owedPerDay = loanInterestLocal.owedPerDay.sub(owedPerDayRefund); - lenderInterestLocal.owedPerDay = lenderInterestLocal.owedPerDay.sub(owedPerDayRefund); - - // update borrower interest - uint256 interestRefundToBorrower = loanLocal.endTimestamp.sub(interestTime); - interestRefundToBorrower = interestRefundToBorrower.mul(owedPerDayRefund); - interestRefundToBorrower = interestRefundToBorrower.div(1 days); - - if (closePrincipal < loanLocal.principal) { - loanInterestLocal.depositTotal = loanInterestLocal.depositTotal.sub(interestRefundToBorrower); - } else { - loanInterestLocal.depositTotal = 0; - } - - // update remaining lender interest values - lenderInterestLocal.principalTotal = lenderInterestLocal.principalTotal.sub(closePrincipal); - - uint256 owedTotal = lenderInterestLocal.owedTotal; - lenderInterestLocal.owedTotal = owedTotal > interestRefundToBorrower ? owedTotal - interestRefundToBorrower : 0; - - return interestRefundToBorrower; - } - - /** - * @notice Check sender is borrower or delegatee and loan id exists. - * - * @param loanId byte32 of the loan id. - * */ - function _checkAuthorized(bytes32 loanId) internal view { - Loan storage loanLocal = loans[loanId]; - require(msg.sender == loanLocal.borrower || delegatedManagers[loanLocal.id][msg.sender], "unauthorized"); - } - - /** - * @notice Internal function for closing a position by swapping the - * collateral back to loan tokens, paying the lender and withdrawing - * the remainder. - * - * @param loanId The id of the loan. - * @param receiver The receiver of the remainder (unused collatral + profit). - * @param swapAmount Defines how much of the position should be closed and - * is denominated in collateral tokens. - * If swapAmount >= collateral, the complete position will be closed. - * Else if returnTokenIsCollateral, (swapAmount/collateral) * principal will be swapped (partial closure). - * Else coveredPrincipal - * @param returnTokenIsCollateral Defines if the remainder should be paid - * out in collateral tokens or underlying loan tokens. - * - * @return loanCloseAmount The amount of the collateral token of the loan. - * @return withdrawAmount The withdraw amount in the collateral token. - * @return withdrawToken The loan token address. - * */ - function _closeWithSwap( - bytes32 loanId, - address receiver, - uint256 swapAmount, - bool returnTokenIsCollateral, - bytes memory loanDataBytes - ) - internal - returns ( - uint256 loanCloseAmount, - uint256 withdrawAmount, - address withdrawToken - ) - { - require(swapAmount != 0, "swapAmount == 0"); - - (Loan storage loanLocal, LoanParams storage loanParamsLocal) = _checkLoan(loanId); - - /// Can't swap more than collateral. - swapAmount = swapAmount > loanLocal.collateral ? loanLocal.collateral : swapAmount; - - //close whole loan if tiny position will remain - if (loanLocal.collateral - swapAmount > 0) { - if (_getAmountInRbtc(loanParamsLocal.collateralToken, loanLocal.collateral - swapAmount) <= TINY_AMOUNT) { - swapAmount = loanLocal.collateral; - } - } - - uint256 loanCloseAmountLessInterest; - if (swapAmount == loanLocal.collateral || returnTokenIsCollateral) { - /// loanCloseAmountLessInterest will be passed as required amount amount of destination tokens. - /// this means, the actual swapAmount passed to the swap contract does not matter at all. - /// the source token amount will be computed depending on the required amount amount of destination tokens. - loanCloseAmount = swapAmount == loanLocal.collateral - ? loanLocal.principal - : loanLocal.principal.mul(swapAmount).div(loanLocal.collateral); - require(loanCloseAmount != 0, "loanCloseAmount == 0"); - - /// Computes the interest refund for the borrower and sends it to the lender to cover part of the principal. - loanCloseAmountLessInterest = _settleInterestToPrincipal(loanLocal, loanParamsLocal, loanCloseAmount, receiver); - } else { - /// loanCloseAmount is calculated after swap; for this case we want to swap the entire source amount - /// and determine the loanCloseAmount and withdraw amount based on that. - loanCloseAmountLessInterest = 0; - } - - uint256 coveredPrincipal; - uint256 usedCollateral; - /// swapAmount repurposed for collateralToLoanSwapRate to avoid stack too deep error. - (coveredPrincipal, usedCollateral, withdrawAmount, swapAmount) = _coverPrincipalWithSwap( - loanLocal, - loanParamsLocal, - swapAmount, /// The amount of source tokens to swap (only matters if !returnTokenIsCollateral or loanCloseAmountLessInterest = 0) - loanCloseAmountLessInterest, /// This is the amount of destination tokens we want to receive (only matters if returnTokenIsCollateral) - returnTokenIsCollateral, - loanDataBytes - ); - - if (loanCloseAmountLessInterest == 0) { - /// Condition prior to swap: swapAmount != loanLocal.collateral && !returnTokenIsCollateral - - /// Amounts that is closed. - loanCloseAmount = coveredPrincipal; - if (coveredPrincipal != loanLocal.principal) { - loanCloseAmount = loanCloseAmount.mul(usedCollateral).div(loanLocal.collateral); - } - require(loanCloseAmount != 0, "loanCloseAmount == 0"); - - /// Amount that is returned to the lender. - loanCloseAmountLessInterest = _settleInterestToPrincipal(loanLocal, loanParamsLocal, loanCloseAmount, receiver); - - /// Remaining amount withdrawn to the receiver. - withdrawAmount = withdrawAmount.add(coveredPrincipal).sub(loanCloseAmountLessInterest); - } else { - /// Pay back the amount which was covered by the swap. - loanCloseAmountLessInterest = coveredPrincipal; - } - - require(loanCloseAmountLessInterest != 0, "closeAmount is 0 after swap"); - - /// Reduce the collateral by the amount which was swapped for the closure. - if (usedCollateral != 0) { - loanLocal.collateral = loanLocal.collateral.sub(usedCollateral); - } - - /// Repays principal to lender. - /// The lender always gets back an ERC20 (even wrbtc), so we call - /// withdraw directly rather than use the _withdrawAsset helper function. - vaultWithdraw(loanParamsLocal.loanToken, loanLocal.lender, loanCloseAmountLessInterest); - - withdrawToken = returnTokenIsCollateral ? loanParamsLocal.collateralToken : loanParamsLocal.loanToken; - - if (withdrawAmount != 0) { - _withdrawAsset(withdrawToken, receiver, withdrawAmount); - } - - _finalizeClose( - loanLocal, - loanParamsLocal, - loanCloseAmount, - usedCollateral, - swapAmount, /// collateralToLoanSwapRate - CloseTypes.Swap - ); - } - - /** - * @notice Close a loan. - * - * @dev Wrapper for _closeLoan internal function. - * - * @param loanLocal The loan object. - * @param loanParamsLocal The loan params. - * @param loanCloseAmount The amount to close: principal or lower. - * @param collateralCloseAmount The amount of collateral to close. - * @param collateralToLoanSwapRate The price rate collateral/loan token. - * @param closeType The type of loan close. - * */ - function _finalizeClose( - Loan storage loanLocal, - LoanParams storage loanParamsLocal, - uint256 loanCloseAmount, - uint256 collateralCloseAmount, - uint256 collateralToLoanSwapRate, - CloseTypes closeType - ) internal { - _closeLoan(loanLocal, loanCloseAmount); - - address _priceFeeds = priceFeeds; - uint256 currentMargin; - uint256 collateralToLoanRate; - - /// This is still called even with full loan close to return collateralToLoanRate - (bool success, bytes memory data) = - _priceFeeds.staticcall( - abi.encodeWithSelector( - IPriceFeeds(_priceFeeds).getCurrentMargin.selector, - loanParamsLocal.loanToken, - loanParamsLocal.collateralToken, - loanLocal.principal, - loanLocal.collateral - ) - ); - assembly { - if eq(success, 1) { - currentMargin := mload(add(data, 32)) - collateralToLoanRate := mload(add(data, 64)) - } - } - /// Note: We can safely skip the margin check if closing - /// via closeWithDeposit or if closing the loan in full by any method. - require( - closeType == CloseTypes.Deposit || - loanLocal.principal == 0 || /// loan fully closed - currentMargin > loanParamsLocal.maintenanceMargin, - "unhealthy position" - ); - - _emitClosingEvents( - loanParamsLocal, - loanLocal, - loanCloseAmount, - collateralCloseAmount, - collateralToLoanRate, - collateralToLoanSwapRate, - currentMargin, - closeType - ); - } - - /** - * swaps a share of a loan's collateral or the complete collateral in order to cover the principle. - * @param loanLocal the loan - * @param loanParamsLocal the loan parameters - * @param swapAmount in case principalNeeded == 0 or !returnTokenIsCollateral, this is the amount which is going to be swapped. - * Else, swapAmount doesn't matter, because the amount of source tokens needed for the swap is estimated by the connector. - * @param principalNeeded the required amount of destination tokens in order to cover the principle (only used if returnTokenIsCollateral) - * @param returnTokenIsCollateral tells if the user wants to withdraw his remaining collateral + profit in collateral tokens - * @notice Swaps a share of a loan's collateral or the complete collateral - * in order to cover the principle. - * - * @param loanLocal The loan object. - * @param loanParamsLocal The loan parameters. - * @param swapAmount In case principalNeeded == 0 or !returnTokenIsCollateral, - * this is the amount which is going to be swapped. - * Else, swapAmount doesn't matter, because the amount of source tokens - * needed for the swap is estimated by the connector. - * @param principalNeeded The required amount of destination tokens in order to - * cover the principle (only used if returnTokenIsCollateral). - * @param returnTokenIsCollateral Tells if the user wants to withdraw his - * remaining collateral + profit in collateral tokens. - * - * @return coveredPrincipal The amount of principal that is covered. - * @return usedCollateral The amount of collateral used. - * @return withdrawAmount The withdraw amount in the collateral token. - * @return collateralToLoanSwapRate The swap rate of collateral. - * */ - function _coverPrincipalWithSwap( - Loan memory loanLocal, - LoanParams memory loanParamsLocal, - uint256 swapAmount, - uint256 principalNeeded, - bool returnTokenIsCollateral, - bytes memory loanDataBytes - ) - internal - returns ( - uint256 coveredPrincipal, - uint256 usedCollateral, - uint256 withdrawAmount, - uint256 collateralToLoanSwapRate - ) - { - uint256 destTokenAmountReceived; - uint256 sourceTokenAmountUsed; - (destTokenAmountReceived, sourceTokenAmountUsed, collateralToLoanSwapRate) = _doCollateralSwap( - loanLocal, - loanParamsLocal, - swapAmount, - principalNeeded, - returnTokenIsCollateral, - loanDataBytes - ); - - if (returnTokenIsCollateral) { - coveredPrincipal = principalNeeded; - - /// Better fill than expected. - if (destTokenAmountReceived > coveredPrincipal) { - /// Send excess to borrower if the amount is big enough to be - /// worth the gas fees. - if (worthTheTransfer(loanParamsLocal.loanToken, destTokenAmountReceived - coveredPrincipal)) { - _withdrawAsset(loanParamsLocal.loanToken, loanLocal.borrower, destTokenAmountReceived - coveredPrincipal); - } - /// Else, give the excess to the lender (if it goes to the - /// borrower, they're very confused. causes more trouble than it's worth) - else { - coveredPrincipal = destTokenAmountReceived; - } - } - withdrawAmount = swapAmount > sourceTokenAmountUsed ? swapAmount - sourceTokenAmountUsed : 0; - } else { - require(sourceTokenAmountUsed == swapAmount, "swap error"); - - if (swapAmount == loanLocal.collateral) { - /// sourceTokenAmountUsed == swapAmount == loanLocal.collateral - - coveredPrincipal = principalNeeded; - withdrawAmount = destTokenAmountReceived - principalNeeded; - } else { - /// sourceTokenAmountUsed == swapAmount < loanLocal.collateral - - if (destTokenAmountReceived >= loanLocal.principal) { - /// Edge case where swap covers full principal. - - coveredPrincipal = loanLocal.principal; - withdrawAmount = destTokenAmountReceived - loanLocal.principal; - - /// Excess collateral refunds to the borrower. - _withdrawAsset(loanParamsLocal.collateralToken, loanLocal.borrower, loanLocal.collateral - sourceTokenAmountUsed); - sourceTokenAmountUsed = loanLocal.collateral; - } else { - coveredPrincipal = destTokenAmountReceived; - withdrawAmount = 0; - } - } - } - - usedCollateral = sourceTokenAmountUsed > swapAmount ? sourceTokenAmountUsed : swapAmount; - } - - function _emitClosingEvents( - LoanParams memory loanParamsLocal, - Loan memory loanLocal, - uint256 loanCloseAmount, - uint256 collateralCloseAmount, - uint256 collateralToLoanRate, - uint256 collateralToLoanSwapRate, - uint256 currentMargin, - CloseTypes closeType - ) internal { - if (closeType == CloseTypes.Deposit) { - emit CloseWithDeposit( - loanLocal.borrower, /// user (borrower) - loanLocal.lender, /// lender - loanLocal.id, /// loanId - msg.sender, /// closer - loanParamsLocal.loanToken, /// loanToken - loanParamsLocal.collateralToken, /// collateralToken - loanCloseAmount, /// loanCloseAmount - collateralCloseAmount, /// collateralCloseAmount - collateralToLoanRate, /// collateralToLoanRate - currentMargin /// currentMargin - ); - } else if (closeType == CloseTypes.Swap) { - /// exitPrice = 1 / collateralToLoanSwapRate - if (collateralToLoanSwapRate != 0) { - collateralToLoanSwapRate = SafeMath.div(10**36, collateralToLoanSwapRate); - } - - /// currentLeverage = 100 / currentMargin - if (currentMargin != 0) { - currentMargin = SafeMath.div(10**38, currentMargin); - } - - emit CloseWithSwap( - loanLocal.borrower, /// user (trader) - loanLocal.lender, /// lender - loanLocal.id, /// loanId - loanParamsLocal.collateralToken, /// collateralToken - loanParamsLocal.loanToken, /// loanToken - msg.sender, /// closer - collateralCloseAmount, /// positionCloseSize - loanCloseAmount, /// loanCloseAmount - collateralToLoanSwapRate, /// exitPrice (1 / collateralToLoanSwapRate) - currentMargin /// currentLeverage - ); - } else if (closeType == CloseTypes.Liquidation) { - emit Liquidate( - loanLocal.borrower, // user (borrower) - msg.sender, // liquidator - loanLocal.id, // loanId - loanLocal.lender, // lender - loanParamsLocal.loanToken, // loanToken - loanParamsLocal.collateralToken, // collateralToken - loanCloseAmount, // loanCloseAmount - collateralCloseAmount, // collateralCloseAmount - collateralToLoanRate, // collateralToLoanRate - currentMargin // currentMargin - ); - } - } - - /** - * @dev returns amount of the asset converted to RBTC - * @param asset the asset to be transferred - * @param amount the amount to be transferred - * @return amount in RBTC - * */ - function _getAmountInRbtc(address asset, uint256 amount) internal returns (uint256) { - (uint256 rbtcRate, uint256 rbtcPrecision) = IPriceFeeds(priceFeeds).queryRate(asset, address(wrbtcToken)); - return amount.mul(rbtcRate).div(rbtcPrecision); - } - - /** - * @dev private function which check the loanLocal & loanParamsLocal does exist - * - * @param loanId bytes32 of loanId - * - * @return Loan storage - * @return LoanParams storage - */ - function _checkLoan(bytes32 loanId) internal view returns (Loan storage, LoanParams storage) { - Loan storage loanLocal = loans[loanId]; - LoanParams storage loanParamsLocal = loanParams[loanLocal.loanParamsId]; - - require(loanLocal.active, "loan is closed"); - require(loanParamsLocal.id != 0, "loanParams not exists"); - - return (loanLocal, loanParamsLocal); - } +contract LoanClosingsShared is + LoanClosingsEvents, + VaultController, + InterestUser, + SwapsUser, + RewardHelper, + ModuleCommonFunctionalities +{ + uint256 internal constant MONTH = 365 days / 12; + //0.00001 BTC, would be nicer in State.sol, but would require a redeploy of the complete protocol, so adding it here instead + //because it's not shared state anyway and only used by this contract + uint256 public constant paySwapExcessToBorrowerThreshold = 10000000000000; + + uint256 public constant TINY_AMOUNT = 25e13; + + enum CloseTypes { Deposit, Swap, Liquidation } + + /** + * @dev computes the interest which needs to be refunded to the borrower based on the amount he's closing and either + * subtracts it from the amount which still needs to be paid back (in case outstanding amount > interest) or withdraws the + * excess to the borrower (in case interest > outstanding). + * @param loanLocal the loan + * @param loanParamsLocal the loan params + * @param loanCloseAmount the amount to be closed (base for the computation) + * @param receiver the address of the receiver (usually the borrower) + * */ + function _settleInterestToPrincipal( + Loan memory loanLocal, + LoanParams memory loanParamsLocal, + uint256 loanCloseAmount, + address receiver + ) internal returns (uint256) { + uint256 loanCloseAmountLessInterest = loanCloseAmount; + + //compute the interest which neeeds to be refunded to the borrower (because full interest is paid on loan ) + uint256 interestRefundToBorrower = + _settleInterest(loanParamsLocal, loanLocal, loanCloseAmountLessInterest); + + uint256 interestAppliedToPrincipal; + //if the outstanding loan is bigger than the interest to be refunded, reduce the amount to be paid back / closed by the interest + if (loanCloseAmountLessInterest >= interestRefundToBorrower) { + // apply all of borrower interest refund torwards principal + interestAppliedToPrincipal = interestRefundToBorrower; + + // principal needed is reduced by this amount + loanCloseAmountLessInterest -= interestRefundToBorrower; + + // no interest refund remaining + interestRefundToBorrower = 0; + } else { + //if the interest refund is bigger than the outstanding loan, the user needs to get back the interest + // principal fully covered by excess interest + interestAppliedToPrincipal = loanCloseAmountLessInterest; + + // amount refunded is reduced by this amount + interestRefundToBorrower -= loanCloseAmountLessInterest; + + // principal fully covered by excess interest + loanCloseAmountLessInterest = 0; + + if (interestRefundToBorrower != 0) { + // refund overage + _withdrawAsset(loanParamsLocal.loanToken, receiver, interestRefundToBorrower); + } + } + + //pay the interest to the lender + //note: this is a waste of gas, because the loanCloseAmountLessInterest is withdrawn to the lender, too. It could be done at once. + if (interestAppliedToPrincipal != 0) { + // The lender always gets back an ERC20 (even wrbtc), so we call withdraw directly rather than + // use the _withdrawAsset helper function + vaultWithdraw(loanParamsLocal.loanToken, loanLocal.lender, interestAppliedToPrincipal); + } + + return loanCloseAmountLessInterest; + } + + // The receiver always gets back an ERC20 (even wrbtc) + function _returnPrincipalWithDeposit( + address loanToken, + address receiver, + uint256 principalNeeded + ) internal { + if (principalNeeded != 0) { + if (msg.value == 0) { + vaultTransfer(loanToken, msg.sender, receiver, principalNeeded); + } else { + require(loanToken == address(wrbtcToken), "wrong asset sent"); + require(msg.value >= principalNeeded, "not enough ether"); + wrbtcToken.deposit.value(principalNeeded)(); + if (receiver != address(this)) { + vaultTransfer(loanToken, address(this), receiver, principalNeeded); + } + if (msg.value > principalNeeded) { + // refund overage + Address.sendValue(msg.sender, msg.value - principalNeeded); + } + } + } else { + require(msg.value == 0, "wrong asset sent"); + } + } + + /** + * @dev checks if the amount of the asset to be transfered is worth the transfer fee + * @param asset the asset to be transfered + * @param amount the amount to be transfered + * @return True if the amount is bigger than the threshold + * */ + function worthTheTransfer(address asset, uint256 amount) internal returns (bool) { + uint256 amountInRbtc = _getAmountInRbtc(asset, amount); + emit swapExcess( + amountInRbtc > paySwapExcessToBorrowerThreshold, + amount, + amountInRbtc, + paySwapExcessToBorrowerThreshold + ); + return amountInRbtc > paySwapExcessToBorrowerThreshold; + } + + /** + * swaps collateral tokens for loan tokens + * @param loanLocal the loan object + * @param loanParamsLocal the loan parameters + * @param swapAmount the amount to be swapped + * @param principalNeeded the required destination token amount + * @param returnTokenIsCollateral if true -> required destination token amount will be passed on, else not + * note: quite dirty. should be refactored. + * @param loanDataBytes additional loan data (not in use for token swaps) + * */ + function _doCollateralSwap( + Loan memory loanLocal, + LoanParams memory loanParamsLocal, + uint256 swapAmount, + uint256 principalNeeded, + bool returnTokenIsCollateral, + bytes memory loanDataBytes + ) + internal + returns ( + uint256 destTokenAmountReceived, + uint256 sourceTokenAmountUsed, + uint256 collateralToLoanSwapRate + ) + { + (destTokenAmountReceived, sourceTokenAmountUsed, collateralToLoanSwapRate) = _loanSwap( + loanLocal.id, + loanParamsLocal.collateralToken, + loanParamsLocal.loanToken, + loanLocal.borrower, + swapAmount, // minSourceTokenAmount + loanLocal.collateral, // maxSourceTokenAmount + returnTokenIsCollateral + ? principalNeeded // requiredDestTokenAmount + : 0, + false, // bypassFee + loanDataBytes + ); + require(destTokenAmountReceived >= principalNeeded, "insufficient dest amount"); + require(sourceTokenAmountUsed <= loanLocal.collateral, "excessive source amount"); + } + + /** + * @notice Withdraw asset to receiver. + * + * @param assetToken The loan token. + * @param receiver The address of the receiver. + * @param assetAmount The loan token amount. + * */ + function _withdrawAsset( + address assetToken, + address receiver, + uint256 assetAmount + ) internal { + if (assetAmount != 0) { + if (assetToken == address(wrbtcToken)) { + vaultEtherWithdraw(receiver, assetAmount); + } else { + vaultWithdraw(assetToken, receiver, assetAmount); + } + } + } + + /** + * @notice Internal function to close a loan. + * + * @param loanLocal The loan object. + * @param loanCloseAmount The amount to close: principal or lower. + * + * */ + function _closeLoan(Loan storage loanLocal, uint256 loanCloseAmount) internal { + require(loanCloseAmount != 0, "nothing to close"); + + if (loanCloseAmount == loanLocal.principal) { + loanLocal.principal = 0; + loanLocal.active = false; + loanLocal.endTimestamp = block.timestamp; + loanLocal.pendingTradesId = 0; + activeLoansSet.removeBytes32(loanLocal.id); + lenderLoanSets[loanLocal.lender].removeBytes32(loanLocal.id); + borrowerLoanSets[loanLocal.borrower].removeBytes32(loanLocal.id); + } else { + loanLocal.principal = loanLocal.principal.sub(loanCloseAmount); + } + } + + function _settleInterest( + LoanParams memory loanParamsLocal, + Loan memory loanLocal, + uint256 closePrincipal + ) internal returns (uint256) { + // pay outstanding interest to lender + _payInterest(loanLocal.lender, loanParamsLocal.loanToken); + + LoanInterest storage loanInterestLocal = loanInterest[loanLocal.id]; + LenderInterest storage lenderInterestLocal = + lenderInterest[loanLocal.lender][loanParamsLocal.loanToken]; + + uint256 interestTime = block.timestamp; + if (interestTime > loanLocal.endTimestamp) { + interestTime = loanLocal.endTimestamp; + } + + _settleFeeRewardForInterestExpense( + loanInterestLocal, + loanLocal.id, + loanParamsLocal.loanToken, /// fee token + loanParamsLocal.collateralToken, /// pairToken (used to check if there is any special rebates or not) -- to pay fee reward + loanLocal.borrower, + interestTime + ); + + uint256 owedPerDayRefund; + if (closePrincipal < loanLocal.principal) { + owedPerDayRefund = loanInterestLocal.owedPerDay.mul(closePrincipal).div( + loanLocal.principal + ); + } else { + owedPerDayRefund = loanInterestLocal.owedPerDay; + } + + // update stored owedPerDay + loanInterestLocal.owedPerDay = loanInterestLocal.owedPerDay.sub(owedPerDayRefund); + lenderInterestLocal.owedPerDay = lenderInterestLocal.owedPerDay.sub(owedPerDayRefund); + + // update borrower interest + uint256 interestRefundToBorrower = loanLocal.endTimestamp.sub(interestTime); + interestRefundToBorrower = interestRefundToBorrower.mul(owedPerDayRefund); + interestRefundToBorrower = interestRefundToBorrower.div(1 days); + + if (closePrincipal < loanLocal.principal) { + loanInterestLocal.depositTotal = loanInterestLocal.depositTotal.sub( + interestRefundToBorrower + ); + } else { + loanInterestLocal.depositTotal = 0; + } + + // update remaining lender interest values + lenderInterestLocal.principalTotal = lenderInterestLocal.principalTotal.sub( + closePrincipal + ); + + uint256 owedTotal = lenderInterestLocal.owedTotal; + lenderInterestLocal.owedTotal = owedTotal > interestRefundToBorrower + ? owedTotal - interestRefundToBorrower + : 0; + + return interestRefundToBorrower; + } + + /** + * @notice Check sender is borrower or delegatee and loan id exists. + * + * @param loanId byte32 of the loan id. + * */ + function _checkAuthorized(bytes32 loanId) internal view { + Loan storage loanLocal = loans[loanId]; + require( + msg.sender == loanLocal.borrower || delegatedManagers[loanLocal.id][msg.sender], + "unauthorized" + ); + } + + /** + * @notice Internal function for closing a position by swapping the + * collateral back to loan tokens, paying the lender and withdrawing + * the remainder. + * + * @param loanId The id of the loan. + * @param receiver The receiver of the remainder (unused collatral + profit). + * @param swapAmount Defines how much of the position should be closed and + * is denominated in collateral tokens. + * If swapAmount >= collateral, the complete position will be closed. + * Else if returnTokenIsCollateral, (swapAmount/collateral) * principal will be swapped (partial closure). + * Else coveredPrincipal + * @param returnTokenIsCollateral Defines if the remainder should be paid + * out in collateral tokens or underlying loan tokens. + * + * @return loanCloseAmount The amount of the collateral token of the loan. + * @return withdrawAmount The withdraw amount in the collateral token. + * @return withdrawToken The loan token address. + * */ + function _closeWithSwap( + bytes32 loanId, + address receiver, + uint256 swapAmount, + bool returnTokenIsCollateral, + bytes memory loanDataBytes + ) + internal + returns ( + uint256 loanCloseAmount, + uint256 withdrawAmount, + address withdrawToken + ) + { + require(swapAmount != 0, "swapAmount == 0"); + + (Loan storage loanLocal, LoanParams storage loanParamsLocal) = _checkLoan(loanId); + + /// Can't swap more than collateral. + swapAmount = swapAmount > loanLocal.collateral ? loanLocal.collateral : swapAmount; + + //close whole loan if tiny position will remain + if (loanLocal.collateral - swapAmount > 0) { + if ( + _getAmountInRbtc( + loanParamsLocal.collateralToken, + loanLocal.collateral - swapAmount + ) <= TINY_AMOUNT + ) { + swapAmount = loanLocal.collateral; + } + } + + uint256 loanCloseAmountLessInterest; + if (swapAmount == loanLocal.collateral || returnTokenIsCollateral) { + /// loanCloseAmountLessInterest will be passed as required amount amount of destination tokens. + /// this means, the actual swapAmount passed to the swap contract does not matter at all. + /// the source token amount will be computed depending on the required amount amount of destination tokens. + loanCloseAmount = swapAmount == loanLocal.collateral + ? loanLocal.principal + : loanLocal.principal.mul(swapAmount).div(loanLocal.collateral); + require(loanCloseAmount != 0, "loanCloseAmount == 0"); + + /// Computes the interest refund for the borrower and sends it to the lender to cover part of the principal. + loanCloseAmountLessInterest = _settleInterestToPrincipal( + loanLocal, + loanParamsLocal, + loanCloseAmount, + receiver + ); + } else { + /// loanCloseAmount is calculated after swap; for this case we want to swap the entire source amount + /// and determine the loanCloseAmount and withdraw amount based on that. + loanCloseAmountLessInterest = 0; + } + + uint256 coveredPrincipal; + uint256 usedCollateral; + /// swapAmount repurposed for collateralToLoanSwapRate to avoid stack too deep error. + (coveredPrincipal, usedCollateral, withdrawAmount, swapAmount) = _coverPrincipalWithSwap( + loanLocal, + loanParamsLocal, + swapAmount, /// The amount of source tokens to swap (only matters if !returnTokenIsCollateral or loanCloseAmountLessInterest = 0) + loanCloseAmountLessInterest, /// This is the amount of destination tokens we want to receive (only matters if returnTokenIsCollateral) + returnTokenIsCollateral, + loanDataBytes + ); + + if (loanCloseAmountLessInterest == 0) { + /// Condition prior to swap: swapAmount != loanLocal.collateral && !returnTokenIsCollateral + + /// Amounts that is closed. + loanCloseAmount = coveredPrincipal; + if (coveredPrincipal != loanLocal.principal) { + loanCloseAmount = loanCloseAmount.mul(usedCollateral).div(loanLocal.collateral); + } + require(loanCloseAmount != 0, "loanCloseAmount == 0"); + + /// Amount that is returned to the lender. + loanCloseAmountLessInterest = _settleInterestToPrincipal( + loanLocal, + loanParamsLocal, + loanCloseAmount, + receiver + ); + + /// Remaining amount withdrawn to the receiver. + withdrawAmount = withdrawAmount.add(coveredPrincipal).sub(loanCloseAmountLessInterest); + } else { + /// Pay back the amount which was covered by the swap. + loanCloseAmountLessInterest = coveredPrincipal; + } + + require(loanCloseAmountLessInterest != 0, "closeAmount is 0 after swap"); + + /// Reduce the collateral by the amount which was swapped for the closure. + if (usedCollateral != 0) { + loanLocal.collateral = loanLocal.collateral.sub(usedCollateral); + } + + /// Repays principal to lender. + /// The lender always gets back an ERC20 (even wrbtc), so we call + /// withdraw directly rather than use the _withdrawAsset helper function. + vaultWithdraw(loanParamsLocal.loanToken, loanLocal.lender, loanCloseAmountLessInterest); + + withdrawToken = returnTokenIsCollateral + ? loanParamsLocal.collateralToken + : loanParamsLocal.loanToken; + + if (withdrawAmount != 0) { + _withdrawAsset(withdrawToken, receiver, withdrawAmount); + } + + _finalizeClose( + loanLocal, + loanParamsLocal, + loanCloseAmount, + usedCollateral, + swapAmount, /// collateralToLoanSwapRate + CloseTypes.Swap + ); + } + + /** + * @notice Close a loan. + * + * @dev Wrapper for _closeLoan internal function. + * + * @param loanLocal The loan object. + * @param loanParamsLocal The loan params. + * @param loanCloseAmount The amount to close: principal or lower. + * @param collateralCloseAmount The amount of collateral to close. + * @param collateralToLoanSwapRate The price rate collateral/loan token. + * @param closeType The type of loan close. + * */ + function _finalizeClose( + Loan storage loanLocal, + LoanParams storage loanParamsLocal, + uint256 loanCloseAmount, + uint256 collateralCloseAmount, + uint256 collateralToLoanSwapRate, + CloseTypes closeType + ) internal { + _closeLoan(loanLocal, loanCloseAmount); + + address _priceFeeds = priceFeeds; + uint256 currentMargin; + uint256 collateralToLoanRate; + + /// This is still called even with full loan close to return collateralToLoanRate + (bool success, bytes memory data) = + _priceFeeds.staticcall( + abi.encodeWithSelector( + IPriceFeeds(_priceFeeds).getCurrentMargin.selector, + loanParamsLocal.loanToken, + loanParamsLocal.collateralToken, + loanLocal.principal, + loanLocal.collateral + ) + ); + assembly { + if eq(success, 1) { + currentMargin := mload(add(data, 32)) + collateralToLoanRate := mload(add(data, 64)) + } + } + /// Note: We can safely skip the margin check if closing + /// via closeWithDeposit or if closing the loan in full by any method. + require( + closeType == CloseTypes.Deposit || + loanLocal.principal == 0 || /// loan fully closed + currentMargin > loanParamsLocal.maintenanceMargin, + "unhealthy position" + ); + + _emitClosingEvents( + loanParamsLocal, + loanLocal, + loanCloseAmount, + collateralCloseAmount, + collateralToLoanRate, + collateralToLoanSwapRate, + currentMargin, + closeType + ); + } + + /** + * swaps a share of a loan's collateral or the complete collateral in order to cover the principle. + * @param loanLocal the loan + * @param loanParamsLocal the loan parameters + * @param swapAmount in case principalNeeded == 0 or !returnTokenIsCollateral, this is the amount which is going to be swapped. + * Else, swapAmount doesn't matter, because the amount of source tokens needed for the swap is estimated by the connector. + * @param principalNeeded the required amount of destination tokens in order to cover the principle (only used if returnTokenIsCollateral) + * @param returnTokenIsCollateral tells if the user wants to withdraw his remaining collateral + profit in collateral tokens + * @notice Swaps a share of a loan's collateral or the complete collateral + * in order to cover the principle. + * + * @param loanLocal The loan object. + * @param loanParamsLocal The loan parameters. + * @param swapAmount In case principalNeeded == 0 or !returnTokenIsCollateral, + * this is the amount which is going to be swapped. + * Else, swapAmount doesn't matter, because the amount of source tokens + * needed for the swap is estimated by the connector. + * @param principalNeeded The required amount of destination tokens in order to + * cover the principle (only used if returnTokenIsCollateral). + * @param returnTokenIsCollateral Tells if the user wants to withdraw his + * remaining collateral + profit in collateral tokens. + * + * @return coveredPrincipal The amount of principal that is covered. + * @return usedCollateral The amount of collateral used. + * @return withdrawAmount The withdraw amount in the collateral token. + * @return collateralToLoanSwapRate The swap rate of collateral. + * */ + function _coverPrincipalWithSwap( + Loan memory loanLocal, + LoanParams memory loanParamsLocal, + uint256 swapAmount, + uint256 principalNeeded, + bool returnTokenIsCollateral, + bytes memory loanDataBytes + ) + internal + returns ( + uint256 coveredPrincipal, + uint256 usedCollateral, + uint256 withdrawAmount, + uint256 collateralToLoanSwapRate + ) + { + uint256 destTokenAmountReceived; + uint256 sourceTokenAmountUsed; + ( + destTokenAmountReceived, + sourceTokenAmountUsed, + collateralToLoanSwapRate + ) = _doCollateralSwap( + loanLocal, + loanParamsLocal, + swapAmount, + principalNeeded, + returnTokenIsCollateral, + loanDataBytes + ); + + if (returnTokenIsCollateral) { + coveredPrincipal = principalNeeded; + + /// Better fill than expected. + if (destTokenAmountReceived > coveredPrincipal) { + /// Send excess to borrower if the amount is big enough to be + /// worth the gas fees. + if ( + worthTheTransfer( + loanParamsLocal.loanToken, + destTokenAmountReceived - coveredPrincipal + ) + ) { + _withdrawAsset( + loanParamsLocal.loanToken, + loanLocal.borrower, + destTokenAmountReceived - coveredPrincipal + ); + } + /// Else, give the excess to the lender (if it goes to the + /// borrower, they're very confused. causes more trouble than it's worth) + else { + coveredPrincipal = destTokenAmountReceived; + } + } + withdrawAmount = swapAmount > sourceTokenAmountUsed + ? swapAmount - sourceTokenAmountUsed + : 0; + } else { + require(sourceTokenAmountUsed == swapAmount, "swap error"); + + if (swapAmount == loanLocal.collateral) { + /// sourceTokenAmountUsed == swapAmount == loanLocal.collateral + + coveredPrincipal = principalNeeded; + withdrawAmount = destTokenAmountReceived - principalNeeded; + } else { + /// sourceTokenAmountUsed == swapAmount < loanLocal.collateral + + if (destTokenAmountReceived >= loanLocal.principal) { + /// Edge case where swap covers full principal. + + coveredPrincipal = loanLocal.principal; + withdrawAmount = destTokenAmountReceived - loanLocal.principal; + + /// Excess collateral refunds to the borrower. + _withdrawAsset( + loanParamsLocal.collateralToken, + loanLocal.borrower, + loanLocal.collateral - sourceTokenAmountUsed + ); + sourceTokenAmountUsed = loanLocal.collateral; + } else { + coveredPrincipal = destTokenAmountReceived; + withdrawAmount = 0; + } + } + } + + usedCollateral = sourceTokenAmountUsed > swapAmount ? sourceTokenAmountUsed : swapAmount; + } + + function _emitClosingEvents( + LoanParams memory loanParamsLocal, + Loan memory loanLocal, + uint256 loanCloseAmount, + uint256 collateralCloseAmount, + uint256 collateralToLoanRate, + uint256 collateralToLoanSwapRate, + uint256 currentMargin, + CloseTypes closeType + ) internal { + if (closeType == CloseTypes.Deposit) { + emit CloseWithDeposit( + loanLocal.borrower, /// user (borrower) + loanLocal.lender, /// lender + loanLocal.id, /// loanId + msg.sender, /// closer + loanParamsLocal.loanToken, /// loanToken + loanParamsLocal.collateralToken, /// collateralToken + loanCloseAmount, /// loanCloseAmount + collateralCloseAmount, /// collateralCloseAmount + collateralToLoanRate, /// collateralToLoanRate + currentMargin /// currentMargin + ); + } else if (closeType == CloseTypes.Swap) { + /// exitPrice = 1 / collateralToLoanSwapRate + if (collateralToLoanSwapRate != 0) { + collateralToLoanSwapRate = SafeMath.div(10**36, collateralToLoanSwapRate); + } + + /// currentLeverage = 100 / currentMargin + if (currentMargin != 0) { + currentMargin = SafeMath.div(10**38, currentMargin); + } + + emit CloseWithSwap( + loanLocal.borrower, /// user (trader) + loanLocal.lender, /// lender + loanLocal.id, /// loanId + loanParamsLocal.collateralToken, /// collateralToken + loanParamsLocal.loanToken, /// loanToken + msg.sender, /// closer + collateralCloseAmount, /// positionCloseSize + loanCloseAmount, /// loanCloseAmount + collateralToLoanSwapRate, /// exitPrice (1 / collateralToLoanSwapRate) + currentMargin /// currentLeverage + ); + } else if (closeType == CloseTypes.Liquidation) { + emit Liquidate( + loanLocal.borrower, // user (borrower) + msg.sender, // liquidator + loanLocal.id, // loanId + loanLocal.lender, // lender + loanParamsLocal.loanToken, // loanToken + loanParamsLocal.collateralToken, // collateralToken + loanCloseAmount, // loanCloseAmount + collateralCloseAmount, // collateralCloseAmount + collateralToLoanRate, // collateralToLoanRate + currentMargin // currentMargin + ); + } + } + + /** + * @dev returns amount of the asset converted to RBTC + * @param asset the asset to be transferred + * @param amount the amount to be transferred + * @return amount in RBTC + * */ + function _getAmountInRbtc(address asset, uint256 amount) internal returns (uint256) { + (uint256 rbtcRate, uint256 rbtcPrecision) = + IPriceFeeds(priceFeeds).queryRate(asset, address(wrbtcToken)); + return amount.mul(rbtcRate).div(rbtcPrecision); + } + + /** + * @dev private function which check the loanLocal & loanParamsLocal does exist + * + * @param loanId bytes32 of loanId + * + * @return Loan storage + * @return LoanParams storage + */ + function _checkLoan(bytes32 loanId) internal view returns (Loan storage, LoanParams storage) { + Loan storage loanLocal = loans[loanId]; + LoanParams storage loanParamsLocal = loanParams[loanLocal.loanParamsId]; + + require(loanLocal.active, "loan is closed"); + require(loanParamsLocal.id != 0, "loanParams not exists"); + + return (loanLocal, loanParamsLocal); + } } diff --git a/contracts/modules/LoanClosingsWith.sol b/contracts/modules/LoanClosingsWith.sol index b26df3886..d1deb97a4 100644 --- a/contracts/modules/LoanClosingsWith.sol +++ b/contracts/modules/LoanClosingsWith.sol @@ -18,170 +18,177 @@ import "./LoanClosingsShared.sol"; * Loans are liquidated if the position goes below margin maintenance. * */ contract LoanClosingsWith is LoanClosingsShared { - constructor() public {} - - function() external { - revert("fallback not allowed"); - } - - function initialize(address target) external onlyOwner { - address prevModuleContractAddress = logicTargets[this.closeWithDeposit.selector]; - _setTarget(this.closeWithDeposit.selector, target); - _setTarget(this.closeWithSwap.selector, target); - emit ProtocolModuleContractReplaced(prevModuleContractAddress, target, "LoanClosingsWith"); - } - - /** - * @notice Closes a loan by doing a deposit. - * - * @dev Public wrapper for _closeWithDeposit internal function. - * - * @param loanId The id of the loan. - * @param receiver The receiver of the remainder. - * @param depositAmount Defines how much of the position should be closed. - * It is denominated in loan tokens. (e.g. rBTC on a iSUSD contract). - * If depositAmount > principal, the complete loan will be closed - * else deposit amount (partial closure). - * - * @return loanCloseAmount The amount of the collateral token of the loan. - * @return withdrawAmount The withdraw amount in the collateral token. - * @return withdrawToken The loan token address. - * */ - function closeWithDeposit( - bytes32 loanId, - address receiver, - uint256 depositAmount /// Denominated in loanToken. - ) - public - payable - nonReentrant - whenNotPaused - returns ( - uint256 loanCloseAmount, - uint256 withdrawAmount, - address withdrawToken - ) - { - _checkAuthorized(loanId); - return _closeWithDeposit(loanId, receiver, depositAmount); - } - - /** - * @notice Close a position by swapping the collateral back to loan tokens - * paying the lender and withdrawing the remainder. - * - * @dev Public wrapper for _closeWithSwap internal function. - * - * @param loanId The id of the loan. - * @param receiver The receiver of the remainder (unused collateral + profit). - * @param swapAmount Defines how much of the position should be closed and - * is denominated in collateral tokens. - * If swapAmount >= collateral, the complete position will be closed. - * Else if returnTokenIsCollateral, (swapAmount/collateral) * principal will be swapped (partial closure). - * Else coveredPrincipal - * @param returnTokenIsCollateral Defines if the remainder should be paid out - * in collateral tokens or underlying loan tokens. - * - * @return loanCloseAmount The amount of the collateral token of the loan. - * @return withdrawAmount The withdraw amount in the collateral token. - * @return withdrawToken The loan token address. - * */ - function closeWithSwap( - bytes32 loanId, - address receiver, - uint256 swapAmount, // denominated in collateralToken - bool returnTokenIsCollateral, // true: withdraws collateralToken, false: withdraws loanToken - bytes memory // for future use /*loanDataBytes*/ - ) - public - nonReentrant - whenNotPaused - returns ( - uint256 loanCloseAmount, - uint256 withdrawAmount, - address withdrawToken - ) - { - _checkAuthorized(loanId); - return - _closeWithSwap( - loanId, - receiver, - swapAmount, - returnTokenIsCollateral, - "" /// loanDataBytes - ); - } - - /** - * @notice Internal function for closing a loan by doing a deposit. - * - * @param loanId The id of the loan. - * @param receiver The receiver of the remainder. - * @param depositAmount Defines how much of the position should be closed. - * It is denominated in loan tokens. - * If depositAmount > principal, the complete loan will be closed - * else deposit amount (partial closure). - * - * @return loanCloseAmount The amount of the collateral token of the loan. - * @return withdrawAmount The withdraw amount in the collateral token. - * @return withdrawToken The loan token address. - * */ - function _closeWithDeposit( - bytes32 loanId, - address receiver, - uint256 depositAmount /// Denominated in loanToken. - ) - internal - returns ( - uint256 loanCloseAmount, - uint256 withdrawAmount, - address withdrawToken - ) - { - require(depositAmount != 0, "depositAmount == 0"); - - //TODO should we skip this check if invoked from rollover ? - (Loan storage loanLocal, LoanParams storage loanParamsLocal) = _checkLoan(loanId); - - /// Can't close more than the full principal. - loanCloseAmount = depositAmount > loanLocal.principal ? loanLocal.principal : depositAmount; - - //close whole loan if tiny position will remain - uint256 remainingAmount = loanLocal.principal - loanCloseAmount; - if (remainingAmount > 0) { - remainingAmount = _getAmountInRbtc(loanParamsLocal.loanToken, remainingAmount); - if (remainingAmount <= TINY_AMOUNT) { - loanCloseAmount = loanLocal.principal; - } - } - - uint256 loanCloseAmountLessInterest = _settleInterestToPrincipal(loanLocal, loanParamsLocal, loanCloseAmount, receiver); - - if (loanCloseAmountLessInterest != 0) { - _returnPrincipalWithDeposit(loanParamsLocal.loanToken, loanLocal.lender, loanCloseAmountLessInterest); - } - - if (loanCloseAmount == loanLocal.principal) { - withdrawAmount = loanLocal.collateral; - } else { - withdrawAmount = loanLocal.collateral.mul(loanCloseAmount).div(loanLocal.principal); - } - - withdrawToken = loanParamsLocal.collateralToken; - - if (withdrawAmount != 0) { - loanLocal.collateral = loanLocal.collateral.sub(withdrawAmount); - _withdrawAsset(withdrawToken, receiver, withdrawAmount); - } - - _finalizeClose( - loanLocal, - loanParamsLocal, - loanCloseAmount, - withdrawAmount, /// collateralCloseAmount - 0, /// collateralToLoanSwapRate - CloseTypes.Deposit - ); - } + constructor() public {} + + function() external { + revert("fallback not allowed"); + } + + function initialize(address target) external onlyOwner { + address prevModuleContractAddress = logicTargets[this.closeWithDeposit.selector]; + _setTarget(this.closeWithDeposit.selector, target); + _setTarget(this.closeWithSwap.selector, target); + emit ProtocolModuleContractReplaced(prevModuleContractAddress, target, "LoanClosingsWith"); + } + + /** + * @notice Closes a loan by doing a deposit. + * + * @dev Public wrapper for _closeWithDeposit internal function. + * + * @param loanId The id of the loan. + * @param receiver The receiver of the remainder. + * @param depositAmount Defines how much of the position should be closed. + * It is denominated in loan tokens. (e.g. rBTC on a iSUSD contract). + * If depositAmount > principal, the complete loan will be closed + * else deposit amount (partial closure). + * + * @return loanCloseAmount The amount of the collateral token of the loan. + * @return withdrawAmount The withdraw amount in the collateral token. + * @return withdrawToken The loan token address. + * */ + function closeWithDeposit( + bytes32 loanId, + address receiver, + uint256 depositAmount /// Denominated in loanToken. + ) + public + payable + nonReentrant + whenNotPaused + returns ( + uint256 loanCloseAmount, + uint256 withdrawAmount, + address withdrawToken + ) + { + _checkAuthorized(loanId); + return _closeWithDeposit(loanId, receiver, depositAmount); + } + + /** + * @notice Close a position by swapping the collateral back to loan tokens + * paying the lender and withdrawing the remainder. + * + * @dev Public wrapper for _closeWithSwap internal function. + * + * @param loanId The id of the loan. + * @param receiver The receiver of the remainder (unused collateral + profit). + * @param swapAmount Defines how much of the position should be closed and + * is denominated in collateral tokens. + * If swapAmount >= collateral, the complete position will be closed. + * Else if returnTokenIsCollateral, (swapAmount/collateral) * principal will be swapped (partial closure). + * Else coveredPrincipal + * @param returnTokenIsCollateral Defines if the remainder should be paid out + * in collateral tokens or underlying loan tokens. + * + * @return loanCloseAmount The amount of the collateral token of the loan. + * @return withdrawAmount The withdraw amount in the collateral token. + * @return withdrawToken The loan token address. + * */ + function closeWithSwap( + bytes32 loanId, + address receiver, + uint256 swapAmount, // denominated in collateralToken + bool returnTokenIsCollateral, // true: withdraws collateralToken, false: withdraws loanToken + bytes memory // for future use /*loanDataBytes*/ + ) + public + nonReentrant + whenNotPaused + returns ( + uint256 loanCloseAmount, + uint256 withdrawAmount, + address withdrawToken + ) + { + _checkAuthorized(loanId); + return + _closeWithSwap( + loanId, + receiver, + swapAmount, + returnTokenIsCollateral, + "" /// loanDataBytes + ); + } + + /** + * @notice Internal function for closing a loan by doing a deposit. + * + * @param loanId The id of the loan. + * @param receiver The receiver of the remainder. + * @param depositAmount Defines how much of the position should be closed. + * It is denominated in loan tokens. + * If depositAmount > principal, the complete loan will be closed + * else deposit amount (partial closure). + * + * @return loanCloseAmount The amount of the collateral token of the loan. + * @return withdrawAmount The withdraw amount in the collateral token. + * @return withdrawToken The loan token address. + * */ + function _closeWithDeposit( + bytes32 loanId, + address receiver, + uint256 depositAmount /// Denominated in loanToken. + ) + internal + returns ( + uint256 loanCloseAmount, + uint256 withdrawAmount, + address withdrawToken + ) + { + require(depositAmount != 0, "depositAmount == 0"); + + //TODO should we skip this check if invoked from rollover ? + (Loan storage loanLocal, LoanParams storage loanParamsLocal) = _checkLoan(loanId); + + /// Can't close more than the full principal. + loanCloseAmount = depositAmount > loanLocal.principal + ? loanLocal.principal + : depositAmount; + + //close whole loan if tiny position will remain + uint256 remainingAmount = loanLocal.principal - loanCloseAmount; + if (remainingAmount > 0) { + remainingAmount = _getAmountInRbtc(loanParamsLocal.loanToken, remainingAmount); + if (remainingAmount <= TINY_AMOUNT) { + loanCloseAmount = loanLocal.principal; + } + } + + uint256 loanCloseAmountLessInterest = + _settleInterestToPrincipal(loanLocal, loanParamsLocal, loanCloseAmount, receiver); + + if (loanCloseAmountLessInterest != 0) { + _returnPrincipalWithDeposit( + loanParamsLocal.loanToken, + loanLocal.lender, + loanCloseAmountLessInterest + ); + } + + if (loanCloseAmount == loanLocal.principal) { + withdrawAmount = loanLocal.collateral; + } else { + withdrawAmount = loanLocal.collateral.mul(loanCloseAmount).div(loanLocal.principal); + } + + withdrawToken = loanParamsLocal.collateralToken; + + if (withdrawAmount != 0) { + loanLocal.collateral = loanLocal.collateral.sub(withdrawAmount); + _withdrawAsset(withdrawToken, receiver, withdrawAmount); + } + + _finalizeClose( + loanLocal, + loanParamsLocal, + loanCloseAmount, + withdrawAmount, /// collateralCloseAmount + 0, /// collateralToLoanSwapRate + CloseTypes.Deposit + ); + } } diff --git a/contracts/modules/LoanMaintenance.sol b/contracts/modules/LoanMaintenance.sol index 8784611fd..492279605 100644 --- a/contracts/modules/LoanMaintenance.sol +++ b/contracts/modules/LoanMaintenance.sol @@ -25,829 +25,880 @@ import "../mixins/ModuleCommonFunctionalities.sol"; * by withdrawing or depositing collateral. * */ contract LoanMaintenance is - LoanOpeningsEvents, - LoanMaintenanceEvents, - VaultController, - InterestUser, - SwapsUser, - LiquidationHelper, - ModuleCommonFunctionalities + LoanOpeningsEvents, + LoanMaintenanceEvents, + VaultController, + InterestUser, + SwapsUser, + LiquidationHelper, + ModuleCommonFunctionalities { - // Keep the old LoanReturnData for backward compatibility (especially for the watcher) - struct LoanReturnData { - bytes32 loanId; - address loanToken; - address collateralToken; - uint256 principal; - uint256 collateral; - uint256 interestOwedPerDay; - uint256 interestDepositRemaining; - uint256 startRate; /// collateralToLoanRate - uint256 startMargin; - uint256 maintenanceMargin; - uint256 currentMargin; - uint256 maxLoanTerm; - uint256 endTimestamp; - uint256 maxLiquidatable; - uint256 maxSeizable; - } - - // The new struct which contained borrower & creation time of a loan - struct LoanReturnDataV2 { - bytes32 loanId; - address loanToken; - address collateralToken; - address borrower; - uint256 principal; - uint256 collateral; - uint256 interestOwedPerDay; - uint256 interestDepositRemaining; - uint256 startRate; /// collateralToLoanRate - uint256 startMargin; - uint256 maintenanceMargin; - uint256 currentMargin; - uint256 maxLoanTerm; - uint256 endTimestamp; - uint256 maxLiquidatable; - uint256 maxSeizable; - uint256 creationTimestamp; - } - - /** - * @notice Empty public constructor. - * */ - constructor() public {} - - /** - * @notice Fallback function is to react to receiving value (rBTC). - * */ - function() external { - revert("fallback not allowed"); - } - - /** - * @notice Set initial values of proxy targets. - * - * @param target The address of the logic contract instance. - * */ - function initialize(address target) external onlyOwner { - address prevModuleContractAddress = logicTargets[this.depositCollateral.selector]; - _setTarget(this.depositCollateral.selector, target); - _setTarget(this.withdrawCollateral.selector, target); - _setTarget(this.withdrawAccruedInterest.selector, target); - _setTarget(this.extendLoanDuration.selector, target); - _setTarget(this.reduceLoanDuration.selector, target); - _setTarget(this.getLenderInterestData.selector, target); - _setTarget(this.getLoanInterestData.selector, target); - _setTarget(this.getUserLoans.selector, target); - _setTarget(this.getUserLoansV2.selector, target); - _setTarget(this.getLoan.selector, target); - _setTarget(this.getLoanV2.selector, target); - _setTarget(this.getActiveLoans.selector, target); - _setTarget(this.getActiveLoansV2.selector, target); - emit ProtocolModuleContractReplaced(prevModuleContractAddress, target, "LoanMaintenance"); - } - - /** - * @notice Increase the margin of a position by depositing additional collateral. - * - * @param loanId A unique ID representing the loan. - * @param depositAmount The amount to be deposited in collateral tokens. - * - * @return actualWithdrawAmount The amount withdrawn taking into account drawdowns. - * */ - function depositCollateral( - bytes32 loanId, - uint256 depositAmount /// must match msg.value if ether is sent - ) external payable nonReentrant whenNotPaused { - require(depositAmount != 0, "depositAmount is 0"); - Loan storage loanLocal = loans[loanId]; - LoanParams storage loanParamsLocal = loanParams[loanLocal.loanParamsId]; - - require(loanLocal.active, "loan is closed"); - require(msg.value == 0 || loanParamsLocal.collateralToken == address(wrbtcToken), "wrong asset sent"); - - loanLocal.collateral = loanLocal.collateral.add(depositAmount); - - if (msg.value == 0) { - vaultDeposit(loanParamsLocal.collateralToken, msg.sender, depositAmount); - } else { - require(msg.value == depositAmount, "ether deposit mismatch"); - vaultEtherDeposit(msg.sender, msg.value); - } - - (uint256 collateralToLoanRate, ) = IPriceFeeds(priceFeeds).queryRate(loanParamsLocal.collateralToken, loanParamsLocal.loanToken); - - emit DepositCollateral(loanId, depositAmount, collateralToLoanRate); - } - - /** - * @notice Withdraw from the collateral. This reduces the margin of a position. - * - * @param loanId A unique ID representing the loan. - * @param receiver The account getting the withdrawal. - * @param withdrawAmount The amount to be withdrawn in collateral tokens. - * - * @return actualWithdrawAmount The amount withdrawn taking into account drawdowns. - * */ - function withdrawCollateral( - bytes32 loanId, - address receiver, - uint256 withdrawAmount - ) external nonReentrant whenNotPaused returns (uint256 actualWithdrawAmount) { - require(withdrawAmount != 0, "withdrawAmount is 0"); - Loan storage loanLocal = loans[loanId]; - LoanParams storage loanParamsLocal = loanParams[loanLocal.loanParamsId]; - - require(loanLocal.active, "loan is closed"); - require(msg.sender == loanLocal.borrower || delegatedManagers[loanLocal.id][msg.sender], "unauthorized"); - - uint256 maxDrawdown = - IPriceFeeds(priceFeeds).getMaxDrawdown( - loanParamsLocal.loanToken, - loanParamsLocal.collateralToken, - loanLocal.principal, - loanLocal.collateral, - loanParamsLocal.maintenanceMargin - ); - - if (withdrawAmount > maxDrawdown) { - actualWithdrawAmount = maxDrawdown; - } else { - actualWithdrawAmount = withdrawAmount; - } - - loanLocal.collateral = loanLocal.collateral.sub(actualWithdrawAmount); - - if (loanParamsLocal.collateralToken == address(wrbtcToken)) { - vaultEtherWithdraw(receiver, actualWithdrawAmount); - } else { - vaultWithdraw(loanParamsLocal.collateralToken, receiver, actualWithdrawAmount); - } - } - - /** - * @notice Withdraw accrued loan interest. - * - * @dev Wrapper for _payInterest internal function. - * - * @param loanToken The loan token address. - * */ - function withdrawAccruedInterest(address loanToken) external whenNotPaused { - /// Pay outstanding interest to lender. - _payInterest( - msg.sender, /// Lender. - loanToken - ); - } - - /** - * @notice Extend the loan duration by as much time as depositAmount can buy. - * - * @param loanId A unique ID representing the loan. - * @param depositAmount The amount to be deposited in loan tokens. Used to pay the interest for the new duration. - * @param useCollateral Whether pay interests w/ the collateral. If true, depositAmount of loan tokens - * will be purchased with the collateral. - * // param calldata The payload for the call. These loan DataBytes are additional loan data (not in use for token swaps). - * - * @return secondsExtended The amount of time in seconds the loan is extended. - * */ - function extendLoanDuration( - bytes32 loanId, - uint256 depositAmount, - bool useCollateral, - bytes calldata /// loanDataBytes, for future use. - ) external payable nonReentrant whenNotPaused returns (uint256 secondsExtended) { - require(depositAmount != 0, "depositAmount is 0"); - Loan storage loanLocal = loans[loanId]; - LoanParams storage loanParamsLocal = loanParams[loanLocal.loanParamsId]; - - require(loanLocal.active, "loan is closed"); - require(!useCollateral || msg.sender == loanLocal.borrower || delegatedManagers[loanLocal.id][msg.sender], "unauthorized"); - require(loanParamsLocal.maxLoanTerm == 0, "indefinite-term only"); - require(msg.value == 0 || (!useCollateral && loanParamsLocal.loanToken == address(wrbtcToken)), "wrong asset sent"); - - /// Pay outstanding interest to lender. - _payInterest(loanLocal.lender, loanParamsLocal.loanToken); - - LoanInterest storage loanInterestLocal = loanInterest[loanLocal.id]; - - _settleFeeRewardForInterestExpense( - loanInterestLocal, - loanLocal.id, - loanParamsLocal.loanToken, /// fee token - loanParamsLocal.collateralToken, /// pairToken (used to check if there is any special rebates or not) -- to pay fee reward - loanLocal.borrower, - block.timestamp - ); - - /// Handle back interest: calculates interest owned since the loan - /// endtime passed but the loan remained open. - uint256 backInterestOwed; - if (block.timestamp > loanLocal.endTimestamp) { - backInterestOwed = block.timestamp.sub(loanLocal.endTimestamp); - backInterestOwed = backInterestOwed.mul(loanInterestLocal.owedPerDay); - backInterestOwed = backInterestOwed.div(86400); - - require(depositAmount > backInterestOwed, "deposit cannot cover back interest"); - } - - /// Deposit interest. - if (useCollateral) { - _doCollateralSwap(loanLocal, loanParamsLocal, depositAmount); - } else { - if (msg.value == 0) { - vaultDeposit(loanParamsLocal.loanToken, msg.sender, depositAmount); - } else { - require(msg.value == depositAmount, "ether deposit mismatch"); - vaultEtherDeposit(msg.sender, msg.value); - } - } - - if (backInterestOwed != 0) { - depositAmount = depositAmount.sub(backInterestOwed); - - /// Pay out backInterestOwed - _payInterestTransfer(loanLocal.lender, loanParamsLocal.loanToken, backInterestOwed); - } - - secondsExtended = depositAmount.mul(86400).div(loanInterestLocal.owedPerDay); - - loanLocal.endTimestamp = loanLocal.endTimestamp.add(secondsExtended); - - require(loanLocal.endTimestamp > block.timestamp, "loan too short"); - - uint256 maxDuration = loanLocal.endTimestamp.sub(block.timestamp); - - /// Loan term has to at least be greater than one hour. - require(maxDuration > 3600, "loan too short"); - - loanInterestLocal.depositTotal = loanInterestLocal.depositTotal.add(depositAmount); - - lenderInterest[loanLocal.lender][loanParamsLocal.loanToken].owedTotal = lenderInterest[loanLocal.lender][loanParamsLocal.loanToken] - .owedTotal - .add(depositAmount); - } - - /** - * @notice Reduce the loan duration by withdrawing from the deposited interest. - * - * @param loanId A unique ID representing the loan. - * @param receiver The account getting the withdrawal. - * @param withdrawAmount The amount to be withdrawn in loan tokens. - * - * @return secondsReduced The amount of time in seconds the loan is reduced. - * */ - function reduceLoanDuration( - bytes32 loanId, - address receiver, - uint256 withdrawAmount - ) external nonReentrant whenNotPaused returns (uint256 secondsReduced) { - require(withdrawAmount != 0, "withdrawAmount is 0"); - Loan storage loanLocal = loans[loanId]; - LoanParams storage loanParamsLocal = loanParams[loanLocal.loanParamsId]; - - require(loanLocal.active, "loan is closed"); - require(msg.sender == loanLocal.borrower || delegatedManagers[loanLocal.id][msg.sender], "unauthorized"); - require(loanParamsLocal.maxLoanTerm == 0, "indefinite-term only"); - require(loanLocal.endTimestamp > block.timestamp, "loan term has ended"); - - /// Pay outstanding interest to lender. - _payInterest(loanLocal.lender, loanParamsLocal.loanToken); - - LoanInterest storage loanInterestLocal = loanInterest[loanLocal.id]; - - _settleFeeRewardForInterestExpense( - loanInterestLocal, - loanLocal.id, - loanParamsLocal.loanToken, /// fee token - loanParamsLocal.collateralToken, /// pairToken (used to check if there is any special rebates or not) -- to pay fee reward - loanLocal.borrower, - block.timestamp - ); - - uint256 interestDepositRemaining = loanLocal.endTimestamp.sub(block.timestamp).mul(loanInterestLocal.owedPerDay).div(86400); - require(withdrawAmount < interestDepositRemaining, "withdraw amount too high"); - - /// Withdraw interest. - if (loanParamsLocal.loanToken == address(wrbtcToken)) { - vaultEtherWithdraw(receiver, withdrawAmount); - } else { - vaultWithdraw(loanParamsLocal.loanToken, receiver, withdrawAmount); - } - - secondsReduced = withdrawAmount.mul(86400).div(loanInterestLocal.owedPerDay); - - require(loanLocal.endTimestamp > secondsReduced, "loan too short"); - - loanLocal.endTimestamp = loanLocal.endTimestamp.sub(secondsReduced); - - require(loanLocal.endTimestamp > block.timestamp, "loan too short"); - - uint256 maxDuration = loanLocal.endTimestamp.sub(block.timestamp); - - /// Loan term has to at least be greater than one hour. - require(maxDuration > 3600, "loan too short"); - - loanInterestLocal.depositTotal = loanInterestLocal.depositTotal.sub(withdrawAmount); - - lenderInterest[loanLocal.lender][loanParamsLocal.loanToken].owedTotal = lenderInterest[loanLocal.lender][loanParamsLocal.loanToken] - .owedTotal - .sub(withdrawAmount); - } - - /** - * @notice Get current lender interest data totals for all loans - * with a specific oracle and interest token. - * - * @param lender The lender address. - * @param loanToken The loan token address. - * - * @return interestPaid The total amount of interest that has been paid to a lender so far. - * @return interestPaidDate The date of the last interest pay out, or 0 if no interest has been withdrawn yet. - * @return interestOwedPerDay The amount of interest the lender is earning per day. - * @return interestUnPaid The total amount of interest the lender is owned and not yet withdrawn. - * @return interestFeePercent The fee retained by the protocol before interest is paid to the lender. - * @return principalTotal The total amount of outstanding principal the lender has loaned. - * */ - function getLenderInterestData(address lender, address loanToken) - external - view - returns ( - uint256 interestPaid, - uint256 interestPaidDate, - uint256 interestOwedPerDay, - uint256 interestUnPaid, - uint256 interestFeePercent, - uint256 principalTotal - ) - { - LenderInterest memory lenderInterestLocal = lenderInterest[lender][loanToken]; - - interestUnPaid = block.timestamp.sub(lenderInterestLocal.updatedTimestamp).mul(lenderInterestLocal.owedPerDay).div(86400); - if (interestUnPaid > lenderInterestLocal.owedTotal) interestUnPaid = lenderInterestLocal.owedTotal; - - return ( - lenderInterestLocal.paidTotal, - lenderInterestLocal.paidTotal != 0 ? lenderInterestLocal.updatedTimestamp : 0, - lenderInterestLocal.owedPerDay, - lenderInterestLocal.updatedTimestamp != 0 ? interestUnPaid : 0, - lendingFeePercent, - lenderInterestLocal.principalTotal - ); - } - - /** - * @notice Get current interest data for a loan. - * - * @param loanId A unique ID representing the loan. - * - * @return loanToken The loan token that interest is paid in. - * @return interestOwedPerDay The amount of interest the borrower is paying per day. - * @return interestDepositTotal The total amount of interest the borrower has deposited. - * @return interestDepositRemaining The amount of deposited interest that is not yet owed to a lender. - * */ - function getLoanInterestData(bytes32 loanId) - external - view - returns ( - address loanToken, - uint256 interestOwedPerDay, - uint256 interestDepositTotal, - uint256 interestDepositRemaining - ) - { - loanToken = loanParams[loans[loanId].loanParamsId].loanToken; - interestOwedPerDay = loanInterest[loanId].owedPerDay; - interestDepositTotal = loanInterest[loanId].depositTotal; - - uint256 endTimestamp = loans[loanId].endTimestamp; - uint256 interestTime = block.timestamp > endTimestamp ? endTimestamp : block.timestamp; - interestDepositRemaining = endTimestamp > interestTime ? endTimestamp.sub(interestTime).mul(interestOwedPerDay).div(86400) : 0; - } - - /** - * @notice Get all user loans. - * - * Only returns data for loans that are active. - * - * @param user The user address. - * @param start The lower loan ID to start with. - * @param count The maximum number of results. - * @param loanType The type of loan. - * loanType 0: all loans. - * loanType 1: margin trade loans. - * loanType 2: non-margin trade loans. - * @param isLender Whether the user is lender or borrower. - * @param unsafeOnly The safe filter (True/False). - * - * @return loansData The array of loans as query result. - * */ - function getUserLoans( - address user, - uint256 start, - uint256 count, - uint256 loanType, - bool isLender, - bool unsafeOnly - ) external view returns (LoanReturnData[] memory loansData) { - EnumerableBytes32Set.Bytes32Set storage set = isLender ? lenderLoanSets[user] : borrowerLoanSets[user]; - - uint256 end = start.add(count).min256(set.length()); - if (start >= end) { - return loansData; - } - - loansData = new LoanReturnData[](count); - uint256 itemCount; - for (uint256 i = end - start; i > 0; i--) { - if (itemCount == count) { - break; - } - LoanReturnData memory loanData = - _getLoan( - set.get(i + start - 1), /// loanId - loanType, - unsafeOnly - ); - if (loanData.loanId == 0) continue; - - loansData[itemCount] = loanData; - itemCount++; - } - - if (itemCount < count) { - assembly { - mstore(loansData, itemCount) - } - } - } - - /** - * @notice Get all user loans. - * - * Only returns data for loans that are active. - * - * @param user The user address. - * @param start The lower loan ID to start with. - * @param count The maximum number of results. - * @param loanType The type of loan. - * loanType 0: all loans. - * loanType 1: margin trade loans. - * loanType 2: non-margin trade loans. - * @param isLender Whether the user is lender or borrower. - * @param unsafeOnly The safe filter (True/False). - * - * @return loansData The array of loans as query result. - * */ - function getUserLoansV2( - address user, - uint256 start, - uint256 count, - uint256 loanType, - bool isLender, - bool unsafeOnly - ) external view returns (LoanReturnDataV2[] memory loansDataV2) { - EnumerableBytes32Set.Bytes32Set storage set = isLender ? lenderLoanSets[user] : borrowerLoanSets[user]; - - uint256 end = start.add(count).min256(set.length()); - if (start >= end) { - return loansDataV2; - } - - loansDataV2 = new LoanReturnDataV2[](count); - uint256 itemCount; - for (uint256 i = end - start; i > 0; i--) { - if (itemCount == count) { - break; - } - LoanReturnDataV2 memory loanDataV2 = - _getLoanV2( - set.get(i + start - 1), /// loanId - loanType, - unsafeOnly - ); - if (loanDataV2.loanId == 0) continue; - - loansDataV2[itemCount] = loanDataV2; - itemCount++; - } - - if (itemCount < count) { - assembly { - mstore(loansDataV2, itemCount) - } - } - } - - /** - * @notice Get one loan data structure by matching ID. - * - * Wrapper to internal _getLoan call. - * - * @param loanId A unique ID representing the loan. - * - * @return loansData The data structure w/ loan information. - * */ - function getLoan(bytes32 loanId) external view returns (LoanReturnData memory loanData) { - return - _getLoan( - loanId, - 0, /// loanType - false /// unsafeOnly - ); - } - - /** - * @notice Get one loan data structure by matching ID. - * - * Wrapper to internal _getLoan call. - * - * @param loanId A unique ID representing the loan. - * - * @return loansData The data structure w/ loan information. - * */ - function getLoanV2(bytes32 loanId) external view returns (LoanReturnDataV2 memory loanDataV2) { - return - _getLoanV2( - loanId, - 0, /// loanType - false /// unsafeOnly - ); - } - - /** - * @notice Get all active loans. - * - * @param start The lower loan ID to start with. - * @param count The maximum number of results. - * @param unsafeOnly The safe filter (True/False). - * - * @return loansData The data structure w/ loan information. - * */ - function getActiveLoans( - uint256 start, - uint256 count, - bool unsafeOnly - ) external view returns (LoanReturnData[] memory loansData) { - uint256 end = start.add(count).min256(activeLoansSet.length()); - if (start >= end) { - return loansData; - } - - loansData = new LoanReturnData[](count); - uint256 itemCount; - for (uint256 i = end - start; i > 0; i--) { - if (itemCount == count) { - break; - } - LoanReturnData memory loanData = - _getLoan( - activeLoansSet.get(i + start - 1), /// loanId - 0, /// loanType - unsafeOnly - ); - if (loanData.loanId == 0) continue; - - loansData[itemCount] = loanData; - itemCount++; - } - - if (itemCount < count) { - assembly { - mstore(loansData, itemCount) - } - } - } - - /** - * @dev New view function which will return the loan data. - * @dev This function was created to support backward compatibility - * @dev As in we the old getActiveLoans function is not expected to be changed by the wathcers. - * - * @param start The lower loan ID to start with. - * @param count The maximum number of results. - * @param unsafeOnly The safe filter (True/False). - * - * @return loanData The data structure - * @return extendedLoanData The data structure which contained (borrower & creation time) - */ - function getActiveLoansV2( - uint256 start, - uint256 count, - bool unsafeOnly - ) external view returns (LoanReturnDataV2[] memory loansDataV2) { - uint256 end = start.add(count).min256(activeLoansSet.length()); - if (start >= end) { - return loansDataV2; - } - - loansDataV2 = new LoanReturnDataV2[](count); - uint256 itemCount; - for (uint256 i = end - start; i > 0; i--) { - if (itemCount == count) { - break; - } - LoanReturnDataV2 memory loanDataV2 = - _getLoanV2( - activeLoansSet.get(i + start - 1), /// loanId - 0, /// loanType - unsafeOnly - ); - if (loanDataV2.loanId == 0) continue; - - loansDataV2[itemCount] = loanDataV2; - itemCount++; - } - - if (itemCount < count) { - assembly { - mstore(loansDataV2, itemCount) - } - } - } - - /** - * @notice Internal function to get one loan data structure. - * - * @param loanId A unique ID representing the loan. - * @param loanType The type of loan. - * loanType 0: all loans. - * loanType 1: margin trade loans. - * loanType 2: non-margin trade loans. - * @param unsafeOnly The safe filter (True/False). - * - * @return loansData The data structure w/ the loan information. - * */ - function _getLoan( - bytes32 loanId, - uint256 loanType, - bool unsafeOnly - ) internal view returns (LoanReturnData memory loanData) { - Loan memory loanLocal = loans[loanId]; - LoanParams memory loanParamsLocal = loanParams[loanLocal.loanParamsId]; - - if (loanType != 0) { - if (!((loanType == 1 && loanParamsLocal.maxLoanTerm != 0) || (loanType == 2 && loanParamsLocal.maxLoanTerm == 0))) { - return loanData; - } - } - - LoanInterest memory loanInterestLocal = loanInterest[loanId]; - - (uint256 currentMargin, uint256 collateralToLoanRate) = - IPriceFeeds(priceFeeds).getCurrentMargin( - loanParamsLocal.loanToken, - loanParamsLocal.collateralToken, - loanLocal.principal, - loanLocal.collateral - ); - - uint256 maxLiquidatable; - uint256 maxSeizable; - if (currentMargin <= loanParamsLocal.maintenanceMargin) { - (maxLiquidatable, maxSeizable, ) = _getLiquidationAmounts( - loanLocal.principal, - loanLocal.collateral, - currentMargin, - loanParamsLocal.maintenanceMargin, - collateralToLoanRate - ); - } else if (unsafeOnly) { - return loanData; - } - - return - LoanReturnData({ - loanId: loanId, - loanToken: loanParamsLocal.loanToken, - collateralToken: loanParamsLocal.collateralToken, - principal: loanLocal.principal, - collateral: loanLocal.collateral, - interestOwedPerDay: loanInterestLocal.owedPerDay, - interestDepositRemaining: loanLocal.endTimestamp >= block.timestamp - ? loanLocal.endTimestamp.sub(block.timestamp).mul(loanInterestLocal.owedPerDay).div(86400) - : 0, - startRate: loanLocal.startRate, - startMargin: loanLocal.startMargin, - maintenanceMargin: loanParamsLocal.maintenanceMargin, - currentMargin: currentMargin, - maxLoanTerm: loanParamsLocal.maxLoanTerm, - endTimestamp: loanLocal.endTimestamp, - maxLiquidatable: maxLiquidatable, - maxSeizable: maxSeizable - }); - } - - /** - * @notice Internal function to get one loan data structure v2. - * - * @param loanId A unique ID representing the loan. - * @param loanType The type of loan. - * loanType 0: all loans. - * loanType 1: margin trade loans. - * loanType 2: non-margin trade loans. - * @param unsafeOnly The safe filter (True/False). - * - * @return loansData The data v2 structure w/ the loan information. - * */ - function _getLoanV2( - bytes32 loanId, - uint256 loanType, - bool unsafeOnly - ) internal view returns (LoanReturnDataV2 memory loanDataV2) { - Loan memory loanLocal = loans[loanId]; - LoanParams memory loanParamsLocal = loanParams[loanLocal.loanParamsId]; - - if (loanType != 0) { - if (!((loanType == 1 && loanParamsLocal.maxLoanTerm != 0) || (loanType == 2 && loanParamsLocal.maxLoanTerm == 0))) { - return loanDataV2; - } - } - - LoanInterest memory loanInterestLocal = loanInterest[loanId]; - - (uint256 currentMargin, uint256 collateralToLoanRate) = - IPriceFeeds(priceFeeds).getCurrentMargin( - loanParamsLocal.loanToken, - loanParamsLocal.collateralToken, - loanLocal.principal, - loanLocal.collateral - ); - - uint256 maxLiquidatable; - uint256 maxSeizable; - if (currentMargin <= loanParamsLocal.maintenanceMargin) { - (maxLiquidatable, maxSeizable, ) = _getLiquidationAmounts( - loanLocal.principal, - loanLocal.collateral, - currentMargin, - loanParamsLocal.maintenanceMargin, - collateralToLoanRate - ); - } else if (unsafeOnly) { - return loanDataV2; - } - - return - LoanReturnDataV2({ - loanId: loanId, - loanToken: loanParamsLocal.loanToken, - collateralToken: loanParamsLocal.collateralToken, - borrower: loanLocal.borrower, - principal: loanLocal.principal, - collateral: loanLocal.collateral, - interestOwedPerDay: loanInterestLocal.owedPerDay, - interestDepositRemaining: loanLocal.endTimestamp >= block.timestamp - ? loanLocal.endTimestamp.sub(block.timestamp).mul(loanInterestLocal.owedPerDay).div(86400) - : 0, - startRate: loanLocal.startRate, - startMargin: loanLocal.startMargin, - maintenanceMargin: loanParamsLocal.maintenanceMargin, - currentMargin: currentMargin, - maxLoanTerm: loanParamsLocal.maxLoanTerm, - endTimestamp: loanLocal.endTimestamp, - maxLiquidatable: maxLiquidatable, - maxSeizable: maxSeizable, - creationTimestamp: loanLocal.startTimestamp - }); - } - - /** - * @notice Internal function to collect interest from the collateral. - * - * @param loanLocal The loan object. - * @param loanParamsLocal The loan parameters. - * @param depositAmount The amount of underlying tokens provided on the loan. - * */ - function _doCollateralSwap( - Loan storage loanLocal, - LoanParams memory loanParamsLocal, - uint256 depositAmount - ) internal { - /// Reverts in _loanSwap if amountNeeded can't be bought. - (, uint256 sourceTokenAmountUsed, ) = - _loanSwap( - loanLocal.id, - loanParamsLocal.collateralToken, - loanParamsLocal.loanToken, - loanLocal.borrower, - loanLocal.collateral, /// minSourceTokenAmount - 0, /// maxSourceTokenAmount (0 means minSourceTokenAmount) - depositAmount, /// requiredDestTokenAmount (partial spend of loanLocal.collateral to fill this amount) - true, /// bypassFee - "" /// loanDataBytes - ); - loanLocal.collateral = loanLocal.collateral.sub(sourceTokenAmountUsed); - - /// Ensure the loan is still healthy. - (uint256 currentMargin, ) = - IPriceFeeds(priceFeeds).getCurrentMargin( - loanParamsLocal.loanToken, - loanParamsLocal.collateralToken, - loanLocal.principal, - loanLocal.collateral - ); - require(currentMargin > loanParamsLocal.maintenanceMargin, "unhealthy position"); - } + // Keep the old LoanReturnData for backward compatibility (especially for the watcher) + struct LoanReturnData { + bytes32 loanId; + address loanToken; + address collateralToken; + uint256 principal; + uint256 collateral; + uint256 interestOwedPerDay; + uint256 interestDepositRemaining; + uint256 startRate; /// collateralToLoanRate + uint256 startMargin; + uint256 maintenanceMargin; + uint256 currentMargin; + uint256 maxLoanTerm; + uint256 endTimestamp; + uint256 maxLiquidatable; + uint256 maxSeizable; + } + + // The new struct which contained borrower & creation time of a loan + struct LoanReturnDataV2 { + bytes32 loanId; + address loanToken; + address collateralToken; + address borrower; + uint256 principal; + uint256 collateral; + uint256 interestOwedPerDay; + uint256 interestDepositRemaining; + uint256 startRate; /// collateralToLoanRate + uint256 startMargin; + uint256 maintenanceMargin; + uint256 currentMargin; + uint256 maxLoanTerm; + uint256 endTimestamp; + uint256 maxLiquidatable; + uint256 maxSeizable; + uint256 creationTimestamp; + } + + /** + * @notice Empty public constructor. + * */ + constructor() public {} + + /** + * @notice Fallback function is to react to receiving value (rBTC). + * */ + function() external { + revert("fallback not allowed"); + } + + /** + * @notice Set initial values of proxy targets. + * + * @param target The address of the logic contract instance. + * */ + function initialize(address target) external onlyOwner { + address prevModuleContractAddress = logicTargets[this.depositCollateral.selector]; + _setTarget(this.depositCollateral.selector, target); + _setTarget(this.withdrawCollateral.selector, target); + _setTarget(this.withdrawAccruedInterest.selector, target); + _setTarget(this.extendLoanDuration.selector, target); + _setTarget(this.reduceLoanDuration.selector, target); + _setTarget(this.getLenderInterestData.selector, target); + _setTarget(this.getLoanInterestData.selector, target); + _setTarget(this.getUserLoans.selector, target); + _setTarget(this.getUserLoansV2.selector, target); + _setTarget(this.getLoan.selector, target); + _setTarget(this.getLoanV2.selector, target); + _setTarget(this.getActiveLoans.selector, target); + _setTarget(this.getActiveLoansV2.selector, target); + emit ProtocolModuleContractReplaced(prevModuleContractAddress, target, "LoanMaintenance"); + } + + /** + * @notice Increase the margin of a position by depositing additional collateral. + * + * @param loanId A unique ID representing the loan. + * @param depositAmount The amount to be deposited in collateral tokens. + * + * @return actualWithdrawAmount The amount withdrawn taking into account drawdowns. + * */ + function depositCollateral( + bytes32 loanId, + uint256 depositAmount /// must match msg.value if ether is sent + ) external payable nonReentrant whenNotPaused { + require(depositAmount != 0, "depositAmount is 0"); + Loan storage loanLocal = loans[loanId]; + LoanParams storage loanParamsLocal = loanParams[loanLocal.loanParamsId]; + + require(loanLocal.active, "loan is closed"); + require( + msg.value == 0 || loanParamsLocal.collateralToken == address(wrbtcToken), + "wrong asset sent" + ); + + loanLocal.collateral = loanLocal.collateral.add(depositAmount); + + if (msg.value == 0) { + vaultDeposit(loanParamsLocal.collateralToken, msg.sender, depositAmount); + } else { + require(msg.value == depositAmount, "ether deposit mismatch"); + vaultEtherDeposit(msg.sender, msg.value); + } + + (uint256 collateralToLoanRate, ) = + IPriceFeeds(priceFeeds).queryRate( + loanParamsLocal.collateralToken, + loanParamsLocal.loanToken + ); + + emit DepositCollateral(loanId, depositAmount, collateralToLoanRate); + } + + /** + * @notice Withdraw from the collateral. This reduces the margin of a position. + * + * @param loanId A unique ID representing the loan. + * @param receiver The account getting the withdrawal. + * @param withdrawAmount The amount to be withdrawn in collateral tokens. + * + * @return actualWithdrawAmount The amount withdrawn taking into account drawdowns. + * */ + function withdrawCollateral( + bytes32 loanId, + address receiver, + uint256 withdrawAmount + ) external nonReentrant whenNotPaused returns (uint256 actualWithdrawAmount) { + require(withdrawAmount != 0, "withdrawAmount is 0"); + Loan storage loanLocal = loans[loanId]; + LoanParams storage loanParamsLocal = loanParams[loanLocal.loanParamsId]; + + require(loanLocal.active, "loan is closed"); + require( + msg.sender == loanLocal.borrower || delegatedManagers[loanLocal.id][msg.sender], + "unauthorized" + ); + + uint256 maxDrawdown = + IPriceFeeds(priceFeeds).getMaxDrawdown( + loanParamsLocal.loanToken, + loanParamsLocal.collateralToken, + loanLocal.principal, + loanLocal.collateral, + loanParamsLocal.maintenanceMargin + ); + + if (withdrawAmount > maxDrawdown) { + actualWithdrawAmount = maxDrawdown; + } else { + actualWithdrawAmount = withdrawAmount; + } + + loanLocal.collateral = loanLocal.collateral.sub(actualWithdrawAmount); + + if (loanParamsLocal.collateralToken == address(wrbtcToken)) { + vaultEtherWithdraw(receiver, actualWithdrawAmount); + } else { + vaultWithdraw(loanParamsLocal.collateralToken, receiver, actualWithdrawAmount); + } + } + + /** + * @notice Withdraw accrued loan interest. + * + * @dev Wrapper for _payInterest internal function. + * + * @param loanToken The loan token address. + * */ + function withdrawAccruedInterest(address loanToken) external whenNotPaused { + /// Pay outstanding interest to lender. + _payInterest( + msg.sender, /// Lender. + loanToken + ); + } + + /** + * @notice Extend the loan duration by as much time as depositAmount can buy. + * + * @param loanId A unique ID representing the loan. + * @param depositAmount The amount to be deposited in loan tokens. Used to pay the interest for the new duration. + * @param useCollateral Whether pay interests w/ the collateral. If true, depositAmount of loan tokens + * will be purchased with the collateral. + * // param calldata The payload for the call. These loan DataBytes are additional loan data (not in use for token swaps). + * + * @return secondsExtended The amount of time in seconds the loan is extended. + * */ + function extendLoanDuration( + bytes32 loanId, + uint256 depositAmount, + bool useCollateral, + bytes calldata /// loanDataBytes, for future use. + ) external payable nonReentrant whenNotPaused returns (uint256 secondsExtended) { + require(depositAmount != 0, "depositAmount is 0"); + Loan storage loanLocal = loans[loanId]; + LoanParams storage loanParamsLocal = loanParams[loanLocal.loanParamsId]; + + require(loanLocal.active, "loan is closed"); + require( + !useCollateral || + msg.sender == loanLocal.borrower || + delegatedManagers[loanLocal.id][msg.sender], + "unauthorized" + ); + require(loanParamsLocal.maxLoanTerm == 0, "indefinite-term only"); + require( + msg.value == 0 || (!useCollateral && loanParamsLocal.loanToken == address(wrbtcToken)), + "wrong asset sent" + ); + + /// Pay outstanding interest to lender. + _payInterest(loanLocal.lender, loanParamsLocal.loanToken); + + LoanInterest storage loanInterestLocal = loanInterest[loanLocal.id]; + + _settleFeeRewardForInterestExpense( + loanInterestLocal, + loanLocal.id, + loanParamsLocal.loanToken, /// fee token + loanParamsLocal.collateralToken, /// pairToken (used to check if there is any special rebates or not) -- to pay fee reward + loanLocal.borrower, + block.timestamp + ); + + /// Handle back interest: calculates interest owned since the loan + /// endtime passed but the loan remained open. + uint256 backInterestOwed; + if (block.timestamp > loanLocal.endTimestamp) { + backInterestOwed = block.timestamp.sub(loanLocal.endTimestamp); + backInterestOwed = backInterestOwed.mul(loanInterestLocal.owedPerDay); + backInterestOwed = backInterestOwed.div(86400); + + require(depositAmount > backInterestOwed, "deposit cannot cover back interest"); + } + + /// Deposit interest. + if (useCollateral) { + _doCollateralSwap(loanLocal, loanParamsLocal, depositAmount); + } else { + if (msg.value == 0) { + vaultDeposit(loanParamsLocal.loanToken, msg.sender, depositAmount); + } else { + require(msg.value == depositAmount, "ether deposit mismatch"); + vaultEtherDeposit(msg.sender, msg.value); + } + } + + if (backInterestOwed != 0) { + depositAmount = depositAmount.sub(backInterestOwed); + + /// Pay out backInterestOwed + _payInterestTransfer(loanLocal.lender, loanParamsLocal.loanToken, backInterestOwed); + } + + secondsExtended = depositAmount.mul(86400).div(loanInterestLocal.owedPerDay); + + loanLocal.endTimestamp = loanLocal.endTimestamp.add(secondsExtended); + + require(loanLocal.endTimestamp > block.timestamp, "loan too short"); + + uint256 maxDuration = loanLocal.endTimestamp.sub(block.timestamp); + + /// Loan term has to at least be greater than one hour. + require(maxDuration > 3600, "loan too short"); + + loanInterestLocal.depositTotal = loanInterestLocal.depositTotal.add(depositAmount); + + lenderInterest[loanLocal.lender][loanParamsLocal.loanToken].owedTotal = lenderInterest[ + loanLocal.lender + ][loanParamsLocal.loanToken] + .owedTotal + .add(depositAmount); + } + + /** + * @notice Reduce the loan duration by withdrawing from the deposited interest. + * + * @param loanId A unique ID representing the loan. + * @param receiver The account getting the withdrawal. + * @param withdrawAmount The amount to be withdrawn in loan tokens. + * + * @return secondsReduced The amount of time in seconds the loan is reduced. + * */ + function reduceLoanDuration( + bytes32 loanId, + address receiver, + uint256 withdrawAmount + ) external nonReentrant whenNotPaused returns (uint256 secondsReduced) { + require(withdrawAmount != 0, "withdrawAmount is 0"); + Loan storage loanLocal = loans[loanId]; + LoanParams storage loanParamsLocal = loanParams[loanLocal.loanParamsId]; + + require(loanLocal.active, "loan is closed"); + require( + msg.sender == loanLocal.borrower || delegatedManagers[loanLocal.id][msg.sender], + "unauthorized" + ); + require(loanParamsLocal.maxLoanTerm == 0, "indefinite-term only"); + require(loanLocal.endTimestamp > block.timestamp, "loan term has ended"); + + /// Pay outstanding interest to lender. + _payInterest(loanLocal.lender, loanParamsLocal.loanToken); + + LoanInterest storage loanInterestLocal = loanInterest[loanLocal.id]; + + _settleFeeRewardForInterestExpense( + loanInterestLocal, + loanLocal.id, + loanParamsLocal.loanToken, /// fee token + loanParamsLocal.collateralToken, /// pairToken (used to check if there is any special rebates or not) -- to pay fee reward + loanLocal.borrower, + block.timestamp + ); + + uint256 interestDepositRemaining = + loanLocal.endTimestamp.sub(block.timestamp).mul(loanInterestLocal.owedPerDay).div( + 86400 + ); + require(withdrawAmount < interestDepositRemaining, "withdraw amount too high"); + + /// Withdraw interest. + if (loanParamsLocal.loanToken == address(wrbtcToken)) { + vaultEtherWithdraw(receiver, withdrawAmount); + } else { + vaultWithdraw(loanParamsLocal.loanToken, receiver, withdrawAmount); + } + + secondsReduced = withdrawAmount.mul(86400).div(loanInterestLocal.owedPerDay); + + require(loanLocal.endTimestamp > secondsReduced, "loan too short"); + + loanLocal.endTimestamp = loanLocal.endTimestamp.sub(secondsReduced); + + require(loanLocal.endTimestamp > block.timestamp, "loan too short"); + + uint256 maxDuration = loanLocal.endTimestamp.sub(block.timestamp); + + /// Loan term has to at least be greater than one hour. + require(maxDuration > 3600, "loan too short"); + + loanInterestLocal.depositTotal = loanInterestLocal.depositTotal.sub(withdrawAmount); + + lenderInterest[loanLocal.lender][loanParamsLocal.loanToken].owedTotal = lenderInterest[ + loanLocal.lender + ][loanParamsLocal.loanToken] + .owedTotal + .sub(withdrawAmount); + } + + /** + * @notice Get current lender interest data totals for all loans + * with a specific oracle and interest token. + * + * @param lender The lender address. + * @param loanToken The loan token address. + * + * @return interestPaid The total amount of interest that has been paid to a lender so far. + * @return interestPaidDate The date of the last interest pay out, or 0 if no interest has been withdrawn yet. + * @return interestOwedPerDay The amount of interest the lender is earning per day. + * @return interestUnPaid The total amount of interest the lender is owned and not yet withdrawn. + * @return interestFeePercent The fee retained by the protocol before interest is paid to the lender. + * @return principalTotal The total amount of outstanding principal the lender has loaned. + * */ + function getLenderInterestData(address lender, address loanToken) + external + view + returns ( + uint256 interestPaid, + uint256 interestPaidDate, + uint256 interestOwedPerDay, + uint256 interestUnPaid, + uint256 interestFeePercent, + uint256 principalTotal + ) + { + LenderInterest memory lenderInterestLocal = lenderInterest[lender][loanToken]; + + interestUnPaid = block + .timestamp + .sub(lenderInterestLocal.updatedTimestamp) + .mul(lenderInterestLocal.owedPerDay) + .div(86400); + if (interestUnPaid > lenderInterestLocal.owedTotal) + interestUnPaid = lenderInterestLocal.owedTotal; + + return ( + lenderInterestLocal.paidTotal, + lenderInterestLocal.paidTotal != 0 ? lenderInterestLocal.updatedTimestamp : 0, + lenderInterestLocal.owedPerDay, + lenderInterestLocal.updatedTimestamp != 0 ? interestUnPaid : 0, + lendingFeePercent, + lenderInterestLocal.principalTotal + ); + } + + /** + * @notice Get current interest data for a loan. + * + * @param loanId A unique ID representing the loan. + * + * @return loanToken The loan token that interest is paid in. + * @return interestOwedPerDay The amount of interest the borrower is paying per day. + * @return interestDepositTotal The total amount of interest the borrower has deposited. + * @return interestDepositRemaining The amount of deposited interest that is not yet owed to a lender. + * */ + function getLoanInterestData(bytes32 loanId) + external + view + returns ( + address loanToken, + uint256 interestOwedPerDay, + uint256 interestDepositTotal, + uint256 interestDepositRemaining + ) + { + loanToken = loanParams[loans[loanId].loanParamsId].loanToken; + interestOwedPerDay = loanInterest[loanId].owedPerDay; + interestDepositTotal = loanInterest[loanId].depositTotal; + + uint256 endTimestamp = loans[loanId].endTimestamp; + uint256 interestTime = block.timestamp > endTimestamp ? endTimestamp : block.timestamp; + interestDepositRemaining = endTimestamp > interestTime + ? endTimestamp.sub(interestTime).mul(interestOwedPerDay).div(86400) + : 0; + } + + /** + * @notice Get all user loans. + * + * Only returns data for loans that are active. + * + * @param user The user address. + * @param start The lower loan ID to start with. + * @param count The maximum number of results. + * @param loanType The type of loan. + * loanType 0: all loans. + * loanType 1: margin trade loans. + * loanType 2: non-margin trade loans. + * @param isLender Whether the user is lender or borrower. + * @param unsafeOnly The safe filter (True/False). + * + * @return loansData The array of loans as query result. + * */ + function getUserLoans( + address user, + uint256 start, + uint256 count, + uint256 loanType, + bool isLender, + bool unsafeOnly + ) external view returns (LoanReturnData[] memory loansData) { + EnumerableBytes32Set.Bytes32Set storage set = + isLender ? lenderLoanSets[user] : borrowerLoanSets[user]; + + uint256 end = start.add(count).min256(set.length()); + if (start >= end) { + return loansData; + } + + loansData = new LoanReturnData[](count); + uint256 itemCount; + for (uint256 i = end - start; i > 0; i--) { + if (itemCount == count) { + break; + } + LoanReturnData memory loanData = + _getLoan( + set.get(i + start - 1), /// loanId + loanType, + unsafeOnly + ); + if (loanData.loanId == 0) continue; + + loansData[itemCount] = loanData; + itemCount++; + } + + if (itemCount < count) { + assembly { + mstore(loansData, itemCount) + } + } + } + + /** + * @notice Get all user loans. + * + * Only returns data for loans that are active. + * + * @param user The user address. + * @param start The lower loan ID to start with. + * @param count The maximum number of results. + * @param loanType The type of loan. + * loanType 0: all loans. + * loanType 1: margin trade loans. + * loanType 2: non-margin trade loans. + * @param isLender Whether the user is lender or borrower. + * @param unsafeOnly The safe filter (True/False). + * + * @return loansData The array of loans as query result. + * */ + function getUserLoansV2( + address user, + uint256 start, + uint256 count, + uint256 loanType, + bool isLender, + bool unsafeOnly + ) external view returns (LoanReturnDataV2[] memory loansDataV2) { + EnumerableBytes32Set.Bytes32Set storage set = + isLender ? lenderLoanSets[user] : borrowerLoanSets[user]; + + uint256 end = start.add(count).min256(set.length()); + if (start >= end) { + return loansDataV2; + } + + loansDataV2 = new LoanReturnDataV2[](count); + uint256 itemCount; + for (uint256 i = end - start; i > 0; i--) { + if (itemCount == count) { + break; + } + LoanReturnDataV2 memory loanDataV2 = + _getLoanV2( + set.get(i + start - 1), /// loanId + loanType, + unsafeOnly + ); + if (loanDataV2.loanId == 0) continue; + + loansDataV2[itemCount] = loanDataV2; + itemCount++; + } + + if (itemCount < count) { + assembly { + mstore(loansDataV2, itemCount) + } + } + } + + /** + * @notice Get one loan data structure by matching ID. + * + * Wrapper to internal _getLoan call. + * + * @param loanId A unique ID representing the loan. + * + * @return loansData The data structure w/ loan information. + * */ + function getLoan(bytes32 loanId) external view returns (LoanReturnData memory loanData) { + return + _getLoan( + loanId, + 0, /// loanType + false /// unsafeOnly + ); + } + + /** + * @notice Get one loan data structure by matching ID. + * + * Wrapper to internal _getLoan call. + * + * @param loanId A unique ID representing the loan. + * + * @return loansData The data structure w/ loan information. + * */ + function getLoanV2(bytes32 loanId) external view returns (LoanReturnDataV2 memory loanDataV2) { + return + _getLoanV2( + loanId, + 0, /// loanType + false /// unsafeOnly + ); + } + + /** + * @notice Get all active loans. + * + * @param start The lower loan ID to start with. + * @param count The maximum number of results. + * @param unsafeOnly The safe filter (True/False). + * + * @return loansData The data structure w/ loan information. + * */ + function getActiveLoans( + uint256 start, + uint256 count, + bool unsafeOnly + ) external view returns (LoanReturnData[] memory loansData) { + uint256 end = start.add(count).min256(activeLoansSet.length()); + if (start >= end) { + return loansData; + } + + loansData = new LoanReturnData[](count); + uint256 itemCount; + for (uint256 i = end - start; i > 0; i--) { + if (itemCount == count) { + break; + } + LoanReturnData memory loanData = + _getLoan( + activeLoansSet.get(i + start - 1), /// loanId + 0, /// loanType + unsafeOnly + ); + if (loanData.loanId == 0) continue; + + loansData[itemCount] = loanData; + itemCount++; + } + + if (itemCount < count) { + assembly { + mstore(loansData, itemCount) + } + } + } + + /** + * @dev New view function which will return the loan data. + * @dev This function was created to support backward compatibility + * @dev As in we the old getActiveLoans function is not expected to be changed by the wathcers. + * + * @param start The lower loan ID to start with. + * @param count The maximum number of results. + * @param unsafeOnly The safe filter (True/False). + * + * @return loanData The data structure + * @return extendedLoanData The data structure which contained (borrower & creation time) + */ + function getActiveLoansV2( + uint256 start, + uint256 count, + bool unsafeOnly + ) external view returns (LoanReturnDataV2[] memory loansDataV2) { + uint256 end = start.add(count).min256(activeLoansSet.length()); + if (start >= end) { + return loansDataV2; + } + + loansDataV2 = new LoanReturnDataV2[](count); + uint256 itemCount; + for (uint256 i = end - start; i > 0; i--) { + if (itemCount == count) { + break; + } + LoanReturnDataV2 memory loanDataV2 = + _getLoanV2( + activeLoansSet.get(i + start - 1), /// loanId + 0, /// loanType + unsafeOnly + ); + if (loanDataV2.loanId == 0) continue; + + loansDataV2[itemCount] = loanDataV2; + itemCount++; + } + + if (itemCount < count) { + assembly { + mstore(loansDataV2, itemCount) + } + } + } + + /** + * @notice Internal function to get one loan data structure. + * + * @param loanId A unique ID representing the loan. + * @param loanType The type of loan. + * loanType 0: all loans. + * loanType 1: margin trade loans. + * loanType 2: non-margin trade loans. + * @param unsafeOnly The safe filter (True/False). + * + * @return loansData The data structure w/ the loan information. + * */ + function _getLoan( + bytes32 loanId, + uint256 loanType, + bool unsafeOnly + ) internal view returns (LoanReturnData memory loanData) { + Loan memory loanLocal = loans[loanId]; + LoanParams memory loanParamsLocal = loanParams[loanLocal.loanParamsId]; + + if (loanType != 0) { + if ( + !((loanType == 1 && loanParamsLocal.maxLoanTerm != 0) || + (loanType == 2 && loanParamsLocal.maxLoanTerm == 0)) + ) { + return loanData; + } + } + + LoanInterest memory loanInterestLocal = loanInterest[loanId]; + + (uint256 currentMargin, uint256 collateralToLoanRate) = + IPriceFeeds(priceFeeds).getCurrentMargin( + loanParamsLocal.loanToken, + loanParamsLocal.collateralToken, + loanLocal.principal, + loanLocal.collateral + ); + + uint256 maxLiquidatable; + uint256 maxSeizable; + if (currentMargin <= loanParamsLocal.maintenanceMargin) { + (maxLiquidatable, maxSeizable, ) = _getLiquidationAmounts( + loanLocal.principal, + loanLocal.collateral, + currentMargin, + loanParamsLocal.maintenanceMargin, + collateralToLoanRate + ); + } else if (unsafeOnly) { + return loanData; + } + + return + LoanReturnData({ + loanId: loanId, + loanToken: loanParamsLocal.loanToken, + collateralToken: loanParamsLocal.collateralToken, + principal: loanLocal.principal, + collateral: loanLocal.collateral, + interestOwedPerDay: loanInterestLocal.owedPerDay, + interestDepositRemaining: loanLocal.endTimestamp >= block.timestamp + ? loanLocal + .endTimestamp + .sub(block.timestamp) + .mul(loanInterestLocal.owedPerDay) + .div(86400) + : 0, + startRate: loanLocal.startRate, + startMargin: loanLocal.startMargin, + maintenanceMargin: loanParamsLocal.maintenanceMargin, + currentMargin: currentMargin, + maxLoanTerm: loanParamsLocal.maxLoanTerm, + endTimestamp: loanLocal.endTimestamp, + maxLiquidatable: maxLiquidatable, + maxSeizable: maxSeizable + }); + } + + /** + * @notice Internal function to get one loan data structure v2. + * + * @param loanId A unique ID representing the loan. + * @param loanType The type of loan. + * loanType 0: all loans. + * loanType 1: margin trade loans. + * loanType 2: non-margin trade loans. + * @param unsafeOnly The safe filter (True/False). + * + * @return loansData The data v2 structure w/ the loan information. + * */ + function _getLoanV2( + bytes32 loanId, + uint256 loanType, + bool unsafeOnly + ) internal view returns (LoanReturnDataV2 memory loanDataV2) { + Loan memory loanLocal = loans[loanId]; + LoanParams memory loanParamsLocal = loanParams[loanLocal.loanParamsId]; + + if (loanType != 0) { + if ( + !((loanType == 1 && loanParamsLocal.maxLoanTerm != 0) || + (loanType == 2 && loanParamsLocal.maxLoanTerm == 0)) + ) { + return loanDataV2; + } + } + + LoanInterest memory loanInterestLocal = loanInterest[loanId]; + + (uint256 currentMargin, uint256 collateralToLoanRate) = + IPriceFeeds(priceFeeds).getCurrentMargin( + loanParamsLocal.loanToken, + loanParamsLocal.collateralToken, + loanLocal.principal, + loanLocal.collateral + ); + + uint256 maxLiquidatable; + uint256 maxSeizable; + if (currentMargin <= loanParamsLocal.maintenanceMargin) { + (maxLiquidatable, maxSeizable, ) = _getLiquidationAmounts( + loanLocal.principal, + loanLocal.collateral, + currentMargin, + loanParamsLocal.maintenanceMargin, + collateralToLoanRate + ); + } else if (unsafeOnly) { + return loanDataV2; + } + + return + LoanReturnDataV2({ + loanId: loanId, + loanToken: loanParamsLocal.loanToken, + collateralToken: loanParamsLocal.collateralToken, + borrower: loanLocal.borrower, + principal: loanLocal.principal, + collateral: loanLocal.collateral, + interestOwedPerDay: loanInterestLocal.owedPerDay, + interestDepositRemaining: loanLocal.endTimestamp >= block.timestamp + ? loanLocal + .endTimestamp + .sub(block.timestamp) + .mul(loanInterestLocal.owedPerDay) + .div(86400) + : 0, + startRate: loanLocal.startRate, + startMargin: loanLocal.startMargin, + maintenanceMargin: loanParamsLocal.maintenanceMargin, + currentMargin: currentMargin, + maxLoanTerm: loanParamsLocal.maxLoanTerm, + endTimestamp: loanLocal.endTimestamp, + maxLiquidatable: maxLiquidatable, + maxSeizable: maxSeizable, + creationTimestamp: loanLocal.startTimestamp + }); + } + + /** + * @notice Internal function to collect interest from the collateral. + * + * @param loanLocal The loan object. + * @param loanParamsLocal The loan parameters. + * @param depositAmount The amount of underlying tokens provided on the loan. + * */ + function _doCollateralSwap( + Loan storage loanLocal, + LoanParams memory loanParamsLocal, + uint256 depositAmount + ) internal { + /// Reverts in _loanSwap if amountNeeded can't be bought. + (, uint256 sourceTokenAmountUsed, ) = + _loanSwap( + loanLocal.id, + loanParamsLocal.collateralToken, + loanParamsLocal.loanToken, + loanLocal.borrower, + loanLocal.collateral, /// minSourceTokenAmount + 0, /// maxSourceTokenAmount (0 means minSourceTokenAmount) + depositAmount, /// requiredDestTokenAmount (partial spend of loanLocal.collateral to fill this amount) + true, /// bypassFee + "" /// loanDataBytes + ); + loanLocal.collateral = loanLocal.collateral.sub(sourceTokenAmountUsed); + + /// Ensure the loan is still healthy. + (uint256 currentMargin, ) = + IPriceFeeds(priceFeeds).getCurrentMargin( + loanParamsLocal.loanToken, + loanParamsLocal.collateralToken, + loanLocal.principal, + loanLocal.collateral + ); + require(currentMargin > loanParamsLocal.maintenanceMargin, "unhealthy position"); + } } diff --git a/contracts/modules/LoanOpenings.sol b/contracts/modules/LoanOpenings.sol index 517b4f4a2..fb0942216 100644 --- a/contracts/modules/LoanOpenings.sol +++ b/contracts/modules/LoanOpenings.sol @@ -21,185 +21,209 @@ import "../mixins/ModuleCommonFunctionalities.sol"; * * This contract contains functions to borrow and trade. * */ -contract LoanOpenings is LoanOpeningsEvents, VaultController, InterestUser, SwapsUser, ModuleCommonFunctionalities { - constructor() public {} - - /** - * @notice Fallback function is to react to receiving value (rBTC). - * */ - function() external { - revert("fallback not allowed"); - } - - /** - * @notice Set function selectors on target contract. - * - * @param target The address of the target contract. - * */ - function initialize(address target) external onlyOwner { - address prevModuleContractAddress = logicTargets[this.borrowOrTradeFromPool.selector]; - _setTarget(this.borrowOrTradeFromPool.selector, target); - _setTarget(this.setDelegatedManager.selector, target); - _setTarget(this.getEstimatedMarginExposure.selector, target); - _setTarget(this.getRequiredCollateral.selector, target); - _setTarget(this.getBorrowAmount.selector, target); - emit ProtocolModuleContractReplaced(prevModuleContractAddress, target, "LoanOpenings"); - } - - /** - * @notice Borrow or trade from pool. - * - * @dev Note: Only callable by loan pools (iTokens). - * Wrapper to _borrowOrTrade internal function. - * - * @param loanParamsId The ID of the loan parameters. - * @param loanId The ID of the loan. If 0, start a new loan. - * @param isTorqueLoan Whether the loan is a Torque loan. - * @param initialMargin The initial amount of margin. - * @param sentAddresses The addresses to send tokens: lender, borrower, - * receiver and manager: - * lender: must match loan if loanId provided. - * borrower: must match loan if loanId provided. - * receiver: receiver of funds (address(0) assumes borrower address). - * manager: delegated manager of loan unless address(0). - * @param sentValues The values to send: - * newRate: New loan interest rate. - * newPrincipal: New loan size (borrowAmount + any borrowed interest). - * torqueInterest: New amount of interest to escrow for Torque loan (determines initial loan length). - * loanTokenReceived: Total loanToken deposit (amount not sent to borrower in the case of Torque loans). - * collateralTokenReceived: Total collateralToken deposit. - * @param loanDataBytes The payload for the call. These loan DataBytes are - * additional loan data (not in use for token swaps). - * - * @return newPrincipal The new loan size. - * @return newCollateral The new collateral amount. - * */ - function borrowOrTradeFromPool( - bytes32 loanParamsId, - bytes32 loanId, - bool isTorqueLoan, - uint256 initialMargin, - address[4] calldata sentAddresses, - uint256[5] calldata sentValues, - bytes calldata loanDataBytes - ) external payable nonReentrant whenNotPaused returns (uint256 newPrincipal, uint256 newCollateral) { - require(msg.value == 0 || loanDataBytes.length != 0, "loanDataBytes required with ether"); - - /// Only callable by loan pools. - require(loanPoolToUnderlying[msg.sender] != address(0), "not authorized"); - - LoanParams memory loanParamsLocal = loanParams[loanParamsId]; - require(loanParamsLocal.id != 0, "loanParams not exists"); - - /// Get required collateral. - uint256 collateralAmountRequired = - _getRequiredCollateral(loanParamsLocal.loanToken, loanParamsLocal.collateralToken, sentValues[1], initialMargin, isTorqueLoan); - require(collateralAmountRequired != 0, "collateral is 0"); - - return - _borrowOrTrade( - loanParamsLocal, - loanId, - isTorqueLoan, - collateralAmountRequired, - initialMargin, - sentAddresses, - sentValues, - loanDataBytes - ); - } - - /** - * @notice Set the delegated manager. - * - * @dev Wrapper for _setDelegatedManager internal function. - * - * @param loanId The ID of the loan. If 0, start a new loan. - * @param delegated The address of the delegated manager. - * @param toggle The flag true/false for the delegated manager. - * */ - function setDelegatedManager( - bytes32 loanId, - address delegated, - bool toggle - ) external whenNotPaused { - require(loans[loanId].borrower == msg.sender, "unauthorized"); - - _setDelegatedManager(loanId, msg.sender, delegated, toggle); - } - - /** - * @notice Get the estimated margin exposure. - * - * Margin is the money borrowed from a broker to purchase an investment - * and is the difference between the total value of investment and the - * loan amount. Margin trading refers to the practice of using borrowed - * funds from a broker to trade a financial asset, which forms the - * collateral for the loan from the broker. - * - * @param loanToken The loan token instance address. - * @param collateralToken The collateral token instance address. - * @param loanTokenSent The amount of loan tokens sent. - * @param collateralTokenSent The amount of collateral tokens sent. - * @param interestRate The interest rate. Percentage w/ 18 decimals. - * @param newPrincipal The updated amount of principal (current debt). - * - * @return The margin exposure. - * */ - function getEstimatedMarginExposure( - address loanToken, - address collateralToken, - uint256 loanTokenSent, - uint256 collateralTokenSent, - uint256 interestRate, - uint256 newPrincipal - ) external view returns (uint256) { - uint256 maxLoanTerm = 2419200; // 28 days - - uint256 owedPerDay = newPrincipal.mul(interestRate).div(365 * 10**20); - - uint256 interestAmountRequired = maxLoanTerm.mul(owedPerDay).div(86400); - - uint256 swapAmount = loanTokenSent.sub(interestAmountRequired); - uint256 tradingFee = _getTradingFee(swapAmount); - if (tradingFee != 0) { - swapAmount = swapAmount.sub(tradingFee); - } - - uint256 receivedAmount = _swapsExpectedReturn(loanToken, collateralToken, swapAmount); - if (receivedAmount == 0) { - return 0; - } else { - return collateralTokenSent.add(receivedAmount); - } - } - - /** - * @notice Get the required collateral. - * - * @dev Calls internal _getRequiredCollateral and add fees. - * - * @param loanToken The loan token instance address. - * @param collateralToken The collateral token instance address. - * @param newPrincipal The updated amount of principal (current debt). - * @param marginAmount The amount of margin of the trade. - * @param isTorqueLoan Whether the loan is a Torque loan. - * - * @return collateralAmountRequired The required collateral. - * */ - function getRequiredCollateral( - address loanToken, - address collateralToken, - uint256 newPrincipal, - uint256 marginAmount, - bool isTorqueLoan - ) public view returns (uint256 collateralAmountRequired) { - if (marginAmount != 0) { - collateralAmountRequired = _getRequiredCollateral(loanToken, collateralToken, newPrincipal, marginAmount, isTorqueLoan); - - // p3.9 from bzx peckshield-audit-report-bZxV2-v1.0rc1.pdf - // cannot be applied solely as it drives to some other tests failure - /* +contract LoanOpenings is + LoanOpeningsEvents, + VaultController, + InterestUser, + SwapsUser, + ModuleCommonFunctionalities +{ + constructor() public {} + + /** + * @notice Fallback function is to react to receiving value (rBTC). + * */ + function() external { + revert("fallback not allowed"); + } + + /** + * @notice Set function selectors on target contract. + * + * @param target The address of the target contract. + * */ + function initialize(address target) external onlyOwner { + address prevModuleContractAddress = logicTargets[this.borrowOrTradeFromPool.selector]; + _setTarget(this.borrowOrTradeFromPool.selector, target); + _setTarget(this.setDelegatedManager.selector, target); + _setTarget(this.getEstimatedMarginExposure.selector, target); + _setTarget(this.getRequiredCollateral.selector, target); + _setTarget(this.getBorrowAmount.selector, target); + emit ProtocolModuleContractReplaced(prevModuleContractAddress, target, "LoanOpenings"); + } + + /** + * @notice Borrow or trade from pool. + * + * @dev Note: Only callable by loan pools (iTokens). + * Wrapper to _borrowOrTrade internal function. + * + * @param loanParamsId The ID of the loan parameters. + * @param loanId The ID of the loan. If 0, start a new loan. + * @param isTorqueLoan Whether the loan is a Torque loan. + * @param initialMargin The initial amount of margin. + * @param sentAddresses The addresses to send tokens: lender, borrower, + * receiver and manager: + * lender: must match loan if loanId provided. + * borrower: must match loan if loanId provided. + * receiver: receiver of funds (address(0) assumes borrower address). + * manager: delegated manager of loan unless address(0). + * @param sentValues The values to send: + * newRate: New loan interest rate. + * newPrincipal: New loan size (borrowAmount + any borrowed interest). + * torqueInterest: New amount of interest to escrow for Torque loan (determines initial loan length). + * loanTokenReceived: Total loanToken deposit (amount not sent to borrower in the case of Torque loans). + * collateralTokenReceived: Total collateralToken deposit. + * @param loanDataBytes The payload for the call. These loan DataBytes are + * additional loan data (not in use for token swaps). + * + * @return newPrincipal The new loan size. + * @return newCollateral The new collateral amount. + * */ + function borrowOrTradeFromPool( + bytes32 loanParamsId, + bytes32 loanId, + bool isTorqueLoan, + uint256 initialMargin, + address[4] calldata sentAddresses, + uint256[5] calldata sentValues, + bytes calldata loanDataBytes + ) + external + payable + nonReentrant + whenNotPaused + returns (uint256 newPrincipal, uint256 newCollateral) + { + require(msg.value == 0 || loanDataBytes.length != 0, "loanDataBytes required with ether"); + + /// Only callable by loan pools. + require(loanPoolToUnderlying[msg.sender] != address(0), "not authorized"); + + LoanParams memory loanParamsLocal = loanParams[loanParamsId]; + require(loanParamsLocal.id != 0, "loanParams not exists"); + + /// Get required collateral. + uint256 collateralAmountRequired = + _getRequiredCollateral( + loanParamsLocal.loanToken, + loanParamsLocal.collateralToken, + sentValues[1], + initialMargin, + isTorqueLoan + ); + require(collateralAmountRequired != 0, "collateral is 0"); + + return + _borrowOrTrade( + loanParamsLocal, + loanId, + isTorqueLoan, + collateralAmountRequired, + initialMargin, + sentAddresses, + sentValues, + loanDataBytes + ); + } + + /** + * @notice Set the delegated manager. + * + * @dev Wrapper for _setDelegatedManager internal function. + * + * @param loanId The ID of the loan. If 0, start a new loan. + * @param delegated The address of the delegated manager. + * @param toggle The flag true/false for the delegated manager. + * */ + function setDelegatedManager( + bytes32 loanId, + address delegated, + bool toggle + ) external whenNotPaused { + require(loans[loanId].borrower == msg.sender, "unauthorized"); + + _setDelegatedManager(loanId, msg.sender, delegated, toggle); + } + + /** + * @notice Get the estimated margin exposure. + * + * Margin is the money borrowed from a broker to purchase an investment + * and is the difference between the total value of investment and the + * loan amount. Margin trading refers to the practice of using borrowed + * funds from a broker to trade a financial asset, which forms the + * collateral for the loan from the broker. + * + * @param loanToken The loan token instance address. + * @param collateralToken The collateral token instance address. + * @param loanTokenSent The amount of loan tokens sent. + * @param collateralTokenSent The amount of collateral tokens sent. + * @param interestRate The interest rate. Percentage w/ 18 decimals. + * @param newPrincipal The updated amount of principal (current debt). + * + * @return The margin exposure. + * */ + function getEstimatedMarginExposure( + address loanToken, + address collateralToken, + uint256 loanTokenSent, + uint256 collateralTokenSent, + uint256 interestRate, + uint256 newPrincipal + ) external view returns (uint256) { + uint256 maxLoanTerm = 2419200; // 28 days + + uint256 owedPerDay = newPrincipal.mul(interestRate).div(365 * 10**20); + + uint256 interestAmountRequired = maxLoanTerm.mul(owedPerDay).div(86400); + + uint256 swapAmount = loanTokenSent.sub(interestAmountRequired); + uint256 tradingFee = _getTradingFee(swapAmount); + if (tradingFee != 0) { + swapAmount = swapAmount.sub(tradingFee); + } + + uint256 receivedAmount = _swapsExpectedReturn(loanToken, collateralToken, swapAmount); + if (receivedAmount == 0) { + return 0; + } else { + return collateralTokenSent.add(receivedAmount); + } + } + + /** + * @notice Get the required collateral. + * + * @dev Calls internal _getRequiredCollateral and add fees. + * + * @param loanToken The loan token instance address. + * @param collateralToken The collateral token instance address. + * @param newPrincipal The updated amount of principal (current debt). + * @param marginAmount The amount of margin of the trade. + * @param isTorqueLoan Whether the loan is a Torque loan. + * + * @return collateralAmountRequired The required collateral. + * */ + function getRequiredCollateral( + address loanToken, + address collateralToken, + uint256 newPrincipal, + uint256 marginAmount, + bool isTorqueLoan + ) public view returns (uint256 collateralAmountRequired) { + if (marginAmount != 0) { + collateralAmountRequired = _getRequiredCollateral( + loanToken, + collateralToken, + newPrincipal, + marginAmount, + isTorqueLoan + ); + + // p3.9 from bzx peckshield-audit-report-bZxV2-v1.0rc1.pdf + // cannot be applied solely as it drives to some other tests failure + /* uint256 feePercent = isTorqueLoan ? borrowingFeePercent : tradingFeePercent; if (collateralAmountRequired != 0 && feePercent != 0) { collateralAmountRequired = collateralAmountRequired.mul(10**20).divCeil( @@ -207,55 +231,63 @@ contract LoanOpenings is LoanOpeningsEvents, VaultController, InterestUser, Swap ); }*/ - uint256 fee = isTorqueLoan ? _getBorrowingFee(collateralAmountRequired) : _getTradingFee(collateralAmountRequired); - if (fee != 0) { - collateralAmountRequired = collateralAmountRequired.add(fee); - } - } - } - - /** - * @notice Get the borrow amount of a trade loan. - * - * @dev Basically borrowAmount = collateral / marginAmount - * - * Collateral is something that helps secure a loan. When you borrow money, - * you agree that your lender can take something and sell it to get their - * money back if you fail to repay the loan. That's the collateral. - * - * @param loanToken The loan token instance address. - * @param collateralToken The collateral token instance address. - * @param collateralTokenAmount The amount of collateral. - * @param marginAmount The amount of margin of the trade. - * @param isTorqueLoan Whether the loan is a Torque loan. - * - * @return borrowAmount The borrow amount. - * */ - function getBorrowAmount( - address loanToken, - address collateralToken, - uint256 collateralTokenAmount, - uint256 marginAmount, - bool isTorqueLoan - ) public view returns (uint256 borrowAmount) { - if (marginAmount != 0) { - if (isTorqueLoan) { - marginAmount = marginAmount.add(10**20); /// Adjust for over-collateralized loan. - } - uint256 collateral = collateralTokenAmount; - uint256 fee = isTorqueLoan ? _getBorrowingFee(collateral) : _getTradingFee(collateral); - if (fee != 0) { - collateral = collateral.sub(fee); - } - if (loanToken == collateralToken) { - borrowAmount = collateral.mul(10**20).div(marginAmount); - } else { - (uint256 sourceToDestRate, uint256 sourceToDestPrecision) = IPriceFeeds(priceFeeds).queryRate(collateralToken, loanToken); - if (sourceToDestPrecision != 0) { - borrowAmount = collateral.mul(10**20).mul(sourceToDestRate).div(marginAmount).div(sourceToDestPrecision); - } - } - /* + uint256 fee = + isTorqueLoan + ? _getBorrowingFee(collateralAmountRequired) + : _getTradingFee(collateralAmountRequired); + if (fee != 0) { + collateralAmountRequired = collateralAmountRequired.add(fee); + } + } + } + + /** + * @notice Get the borrow amount of a trade loan. + * + * @dev Basically borrowAmount = collateral / marginAmount + * + * Collateral is something that helps secure a loan. When you borrow money, + * you agree that your lender can take something and sell it to get their + * money back if you fail to repay the loan. That's the collateral. + * + * @param loanToken The loan token instance address. + * @param collateralToken The collateral token instance address. + * @param collateralTokenAmount The amount of collateral. + * @param marginAmount The amount of margin of the trade. + * @param isTorqueLoan Whether the loan is a Torque loan. + * + * @return borrowAmount The borrow amount. + * */ + function getBorrowAmount( + address loanToken, + address collateralToken, + uint256 collateralTokenAmount, + uint256 marginAmount, + bool isTorqueLoan + ) public view returns (uint256 borrowAmount) { + if (marginAmount != 0) { + if (isTorqueLoan) { + marginAmount = marginAmount.add(10**20); /// Adjust for over-collateralized loan. + } + uint256 collateral = collateralTokenAmount; + uint256 fee = isTorqueLoan ? _getBorrowingFee(collateral) : _getTradingFee(collateral); + if (fee != 0) { + collateral = collateral.sub(fee); + } + if (loanToken == collateralToken) { + borrowAmount = collateral.mul(10**20).div(marginAmount); + } else { + (uint256 sourceToDestRate, uint256 sourceToDestPrecision) = + IPriceFeeds(priceFeeds).queryRate(collateralToken, loanToken); + if (sourceToDestPrecision != 0) { + borrowAmount = collateral + .mul(10**20) + .mul(sourceToDestRate) + .div(marginAmount) + .div(sourceToDestPrecision); + } + } + /* // p3.9 from bzx peckshield-audit-report-bZxV2-v1.0rc1.pdf // cannot be applied solely as it drives to some other tests failure uint256 feePercent = isTorqueLoan ? borrowingFeePercent : tradingFeePercent; @@ -266,503 +298,552 @@ contract LoanOpenings is LoanOpeningsEvents, VaultController, InterestUser, Swap ) .divCeil(10**20); }*/ - } - } - - /** - * @notice Borrow or trade. - * - * @param loanParamsLocal The loan parameters. - * @param loanId The ID of the loan. If 0, start a new loan. - * @param isTorqueLoan Whether the loan is a Torque loan. - * @param collateralAmountRequired The required amount of collateral. - * @param initialMargin The initial amount of margin. - * @param sentAddresses The addresses to send tokens: lender, borrower, - * receiver and manager: - * lender: must match loan if loanId provided. - * borrower: must match loan if loanId provided. - * receiver: receiver of funds (address(0) assumes borrower address). - * manager: delegated manager of loan unless address(0). - * @param sentValues The values to send: - * newRate: New loan interest rate. - * newPrincipal: New loan size (borrowAmount + any borrowed interest). - * torqueInterest: New amount of interest to escrow for Torque loan (determines initial loan length). - * loanTokenReceived: Total loanToken deposit (amount not sent to borrower in the case of Torque loans). - * collateralTokenReceived: Total collateralToken deposit. - * @param loanDataBytes The payload for the call. These loan DataBytes are - * additional loan data (not in use for token swaps). - * - * @return The new loan size. - * @return The new collateral amount. - * */ - function _borrowOrTrade( - LoanParams memory loanParamsLocal, - bytes32 loanId, - bool isTorqueLoan, - uint256 collateralAmountRequired, - uint256 initialMargin, - address[4] memory sentAddresses, - uint256[5] memory sentValues, - bytes memory loanDataBytes - ) internal returns (uint256, uint256) { - require(loanParamsLocal.collateralToken != loanParamsLocal.loanToken, "collateral/loan match"); - require(initialMargin >= loanParamsLocal.minInitialMargin, "initialMargin too low"); - - /// maxLoanTerm == 0 indicates a Torque loan and requires that torqueInterest != 0 - require( - loanParamsLocal.maxLoanTerm != 0 || sentValues[2] != 0, /// torqueInterest - "invalid interest" - ); - - /// Initialize loan. - Loan storage loanLocal = loans[_initializeLoan(loanParamsLocal, loanId, initialMargin, sentAddresses, sentValues)]; - - // Get required interest. - uint256 amount = - _initializeInterest( - loanParamsLocal, - loanLocal, - sentValues[0], /// newRate - sentValues[1], /// newPrincipal, - sentValues[2] /// torqueInterest - ); - - /// substract out interest from usable loanToken sent. - sentValues[3] = sentValues[3].sub(amount); - - if (isTorqueLoan) { - require(sentValues[3] == 0, "surplus loan token"); - - uint256 borrowingFee = _getBorrowingFee(sentValues[4]); - // need to temp into local state to avoid - address _collateralToken = loanParamsLocal.collateralToken; - address _loanToken = loanParamsLocal.loanToken; - if (borrowingFee != 0) { - _payBorrowingFee( - sentAddresses[1], /// borrower - loanLocal.id, - _collateralToken, /// fee token - _loanToken, /// pairToken (used to check if there is any special rebates or not) -- to pay fee reward - borrowingFee - ); - - sentValues[4] = sentValues[4] /// collateralTokenReceived - .sub(borrowingFee); - } - } else { - /// Update collateral after trade. - /// sentValues[3] is repurposed to hold loanToCollateralSwapRate to avoid stack too deep error. - uint256 receivedAmount; - (receivedAmount, , sentValues[3]) = _loanSwap( - loanId, - loanParamsLocal.loanToken, - loanParamsLocal.collateralToken, - sentAddresses[1], /// borrower - sentValues[3], /// loanTokenUsable (minSourceTokenAmount) - 0, /// maxSourceTokenAmount (0 means minSourceTokenAmount) - 0, /// requiredDestTokenAmount (enforces that all of loanTokenUsable is swapped) - false, /// bypassFee - loanDataBytes - ); - sentValues[4] = sentValues[4] /// collateralTokenReceived - .add(receivedAmount); - } - - /// Settle collateral. - require( - _isCollateralSatisfied(loanParamsLocal, loanLocal, initialMargin, sentValues[4], collateralAmountRequired), - "collateral insufficient" - ); - - loanLocal.collateral = loanLocal.collateral.add(sentValues[4]); - - if (isTorqueLoan) { - /// reclaiming varaible -> interestDuration - sentValues[2] = loanLocal.endTimestamp.sub(block.timestamp); - } else { - /// reclaiming varaible -> entryLeverage = 100 / initialMargin - sentValues[2] = SafeMath.div(10**38, initialMargin); - } - - _finalizeOpen(loanParamsLocal, loanLocal, sentAddresses, sentValues, isTorqueLoan); - - return (sentValues[1], sentValues[4]); /// newPrincipal, newCollateral - } - - /** - * @notice Finalize an open loan. - * - * @dev Finalize it by updating local parameters of the loan. - * - * @param loanParamsLocal The loan parameters. - * @param loanLocal The loan object. - * @param sentAddresses The addresses to send tokens: lender, borrower, - * receiver and manager: - * lender: must match loan if loanId provided. - * borrower: must match loan if loanId provided. - * receiver: receiver of funds (address(0) assumes borrower address). - * manager: delegated manager of loan unless address(0). - * @param sentValues The values to send: - * newRate: New loan interest rate. - * newPrincipal: New loan size (borrowAmount + any borrowed interest). - * torqueInterest: New amount of interest to escrow for Torque loan (determines initial loan length). - * loanTokenReceived: Total loanToken deposit (amount not sent to borrower in the case of Torque loans). - * collateralTokenReceived: Total collateralToken deposit. - * @param isTorqueLoan Whether the loan is a Torque loan. - * */ - function _finalizeOpen( - LoanParams memory loanParamsLocal, - Loan storage loanLocal, - address[4] memory sentAddresses, - uint256[5] memory sentValues, - bool isTorqueLoan - ) internal { - /// @dev TODO: here the actual used rate and margin should go. - (uint256 initialMargin, uint256 collateralToLoanRate) = - IPriceFeeds(priceFeeds).getCurrentMargin( - loanParamsLocal.loanToken, - loanParamsLocal.collateralToken, - loanLocal.principal, - loanLocal.collateral - ); - require(initialMargin > loanParamsLocal.maintenanceMargin, "unhealthy position"); - - if (loanLocal.startTimestamp == block.timestamp) { - uint256 loanToCollateralPrecision = - IPriceFeeds(priceFeeds).queryPrecision(loanParamsLocal.loanToken, loanParamsLocal.collateralToken); - uint256 collateralToLoanPrecision = - IPriceFeeds(priceFeeds).queryPrecision(loanParamsLocal.collateralToken, loanParamsLocal.loanToken); - uint256 totalSwapRate = loanToCollateralPrecision.mul(collateralToLoanPrecision); - loanLocal.startRate = isTorqueLoan ? collateralToLoanRate : totalSwapRate.div(sentValues[3]); - } - - _emitOpeningEvents(loanParamsLocal, loanLocal, sentAddresses, sentValues, collateralToLoanRate, initialMargin, isTorqueLoan); - } - - /** - * @notice Emit the opening events. - * - * @param loanParamsLocal The loan parameters. - * @param loanLocal The loan object. - * @param sentAddresses The addresses to send tokens: lender, borrower, - * receiver and manager: - * lender: must match loan if loanId provided. - * borrower: must match loan if loanId provided. - * receiver: receiver of funds (address(0) assumes borrower address). - * manager: delegated manager of loan unless address(0). - * @param sentValues The values to send: - * newRate: New loan interest rate. - * newPrincipal: New loan size (borrowAmount + any borrowed interest). - * torqueInterest: New amount of interest to escrow for Torque loan (determines initial loan length). - * loanTokenReceived: Total loanToken deposit (amount not sent to borrower in the case of Torque loans). - * collateralTokenReceived: Total collateralToken deposit. - * @param collateralToLoanRate The exchange rate from collateral to loan - * tokens. - * @param margin The amount of margin of the trade. - * @param isTorqueLoan Whether the loan is a Torque loan. - * */ - function _emitOpeningEvents( - LoanParams memory loanParamsLocal, - Loan memory loanLocal, - address[4] memory sentAddresses, - uint256[5] memory sentValues, - uint256 collateralToLoanRate, - uint256 margin, - bool isTorqueLoan - ) internal { - if (isTorqueLoan) { - emit Borrow( - sentAddresses[1], /// user (borrower) - sentAddresses[0], /// lender - loanLocal.id, /// loanId - loanParamsLocal.loanToken, /// loanToken - loanParamsLocal.collateralToken, /// collateralToken - sentValues[1], /// newPrincipal - sentValues[4], /// newCollateral - sentValues[0], /// interestRate - sentValues[2], /// interestDuration - collateralToLoanRate, /// collateralToLoanRate, - margin /// currentMargin - ); - } else { - /// currentLeverage = 100 / currentMargin - margin = SafeMath.div(10**38, margin); - - emit Trade( - sentAddresses[1], /// user (trader) - sentAddresses[0], /// lender - loanLocal.id, /// loanId - loanParamsLocal.collateralToken, /// collateralToken - loanParamsLocal.loanToken, /// loanToken - sentValues[4], /// positionSize - sentValues[1], /// borrowedAmount - sentValues[0], /// interestRate, - loanLocal.endTimestamp, /// settlementDate - sentValues[3], /// entryPrice (loanToCollateralSwapRate) - sentValues[2], /// entryLeverage - margin /// currentLeverage - ); - } - } - - /** - * @notice Set the delegated manager. - * - * @param loanId The ID of the loan. If 0, start a new loan. - * @param delegator The address of previous manager. - * @param delegated The address of the delegated manager. - * @param toggle The flag true/false for the delegated manager. - * */ - function _setDelegatedManager( - bytes32 loanId, - address delegator, - address delegated, - bool toggle - ) internal { - delegatedManagers[loanId][delegated] = toggle; - - emit DelegatedManagerSet(loanId, delegator, delegated, toggle); - } - - /** - * @notice Calculate whether the collateral is satisfied. - * - * @dev Basically check collateral + drawdown >= 98% of required. - * - * @param loanParamsLocal The loan parameters. - * @param loanLocal The loan object. - * @param initialMargin The initial amount of margin. - * @param newCollateral The amount of new collateral. - * @param collateralAmountRequired The amount of required collateral. - * - * @return Whether the collateral is satisfied. - * */ - function _isCollateralSatisfied( - LoanParams memory loanParamsLocal, - Loan memory loanLocal, - uint256 initialMargin, - uint256 newCollateral, - uint256 collateralAmountRequired - ) internal view returns (bool) { - /// Allow at most 2% under-collateralized. - collateralAmountRequired = collateralAmountRequired.mul(98 ether).div(100 ether); - - if (newCollateral < collateralAmountRequired) { - /// Check that existing collateral is sufficient coverage. - if (loanLocal.collateral != 0) { - uint256 maxDrawdown = - IPriceFeeds(priceFeeds).getMaxDrawdown( - loanParamsLocal.loanToken, - loanParamsLocal.collateralToken, - loanLocal.principal, - loanLocal.collateral, - initialMargin - ); - return newCollateral.add(maxDrawdown) >= collateralAmountRequired; - } else { - return false; - } - } - return true; - } - - /** - * @notice Initialize a loan. - * - * @param loanParamsLocal The loan parameters. - * @param loanId The ID of the loan. - * @param initialMargin The amount of margin of the trade. - * @param sentAddresses The addresses to send tokens: lender, borrower, - * receiver and manager: - * lender: must match loan if loanId provided. - * borrower: must match loan if loanId provided. - * receiver: receiver of funds (address(0) assumes borrower address). - * manager: delegated manager of loan unless address(0). - * @param sentValues The values to send: - * newRate: New loan interest rate. - * newPrincipal: New loan size (borrowAmount + any borrowed interest). - * torqueInterest: New amount of interest to escrow for Torque loan (determines initial loan length). - * loanTokenReceived: Total loanToken deposit (amount not sent to borrower in the case of Torque loans). - * collateralTokenReceived: Total collateralToken deposit. - * @return The loanId. - * */ - function _initializeLoan( - LoanParams memory loanParamsLocal, - bytes32 loanId, - uint256 initialMargin, - address[4] memory sentAddresses, - uint256[5] memory sentValues - ) internal returns (bytes32) { - require(loanParamsLocal.active, "loanParams disabled"); - - address lender = sentAddresses[0]; - address borrower = sentAddresses[1]; - address manager = sentAddresses[3]; - uint256 newPrincipal = sentValues[1]; - - Loan memory loanLocal; - - if (loanId == 0) { - borrowerNonce[borrower]++; - loanId = keccak256(abi.encodePacked(loanParamsLocal.id, lender, borrower, borrowerNonce[borrower])); - require(loans[loanId].id == 0, "loan exists"); - - loanLocal = Loan({ - id: loanId, - loanParamsId: loanParamsLocal.id, - pendingTradesId: 0, - active: true, - principal: newPrincipal, - collateral: 0, /// calculated later - startTimestamp: block.timestamp, - endTimestamp: 0, /// calculated later - startMargin: initialMargin, - startRate: 0, /// queried later - borrower: borrower, - lender: lender - }); - - activeLoansSet.addBytes32(loanId); - lenderLoanSets[lender].addBytes32(loanId); - borrowerLoanSets[borrower].addBytes32(loanId); - } else { - loanLocal = loans[loanId]; - require(loanLocal.active && block.timestamp < loanLocal.endTimestamp, "loan has ended"); - require(loanLocal.borrower == borrower, "borrower mismatch"); - require(loanLocal.lender == lender, "lender mismatch"); - require(loanLocal.loanParamsId == loanParamsLocal.id, "loanParams mismatch"); - - loanLocal.principal = loanLocal.principal.add(newPrincipal); - } - - if (manager != address(0)) { - _setDelegatedManager(loanId, borrower, manager, true); - } - - loans[loanId] = loanLocal; - - return loanId; - } - - /** - * @notice Initialize a loan interest. - * - * @dev A Torque loan is an indefinite-term loan. - * - * @param loanParamsLocal The loan parameters. - * @param loanLocal The loan object. - * @param newRate The new interest rate of the loan. - * @param newPrincipal The new principal amount of the loan. - * @param torqueInterest The interest rate of the Torque loan. - * - * @return interestAmountRequired The interest amount required. - * */ - function _initializeInterest( - LoanParams memory loanParamsLocal, - Loan storage loanLocal, - uint256 newRate, - uint256 newPrincipal, - uint256 torqueInterest /// ignored for fixed-term loans - ) internal returns (uint256 interestAmountRequired) { - /// Pay outstanding interest to lender. - _payInterest(loanLocal.lender, loanParamsLocal.loanToken); - - LoanInterest storage loanInterestLocal = loanInterest[loanLocal.id]; - LenderInterest storage lenderInterestLocal = lenderInterest[loanLocal.lender][loanParamsLocal.loanToken]; - - uint256 maxLoanTerm = loanParamsLocal.maxLoanTerm; - - _settleFeeRewardForInterestExpense( - loanInterestLocal, - loanLocal.id, - loanParamsLocal.loanToken, /// fee token - loanParamsLocal.collateralToken, /// pairToken (used to check if there is any special rebates or not) -- to pay fee reward - loanLocal.borrower, - block.timestamp - ); - - uint256 previousDepositRemaining; - if (maxLoanTerm == 0 && loanLocal.endTimestamp != 0) { - previousDepositRemaining = loanLocal - .endTimestamp - .sub(block.timestamp) /// block.timestamp < endTimestamp was confirmed earlier. - .mul(loanInterestLocal.owedPerDay) - .div(86400); - } - - uint256 owedPerDay = newPrincipal.mul(newRate).div(365 * 10**20); - - /// Update stored owedPerDay - loanInterestLocal.owedPerDay = loanInterestLocal.owedPerDay.add(owedPerDay); - lenderInterestLocal.owedPerDay = lenderInterestLocal.owedPerDay.add(owedPerDay); - - if (maxLoanTerm == 0) { - /// Indefinite-term (Torque) loan. - - /// torqueInterest != 0 was confirmed earlier. - loanLocal.endTimestamp = torqueInterest.add(previousDepositRemaining).mul(86400).div(loanInterestLocal.owedPerDay).add( - block.timestamp - ); - - maxLoanTerm = loanLocal.endTimestamp.sub(block.timestamp); - - /// Loan term has to at least be greater than one hour. - require(maxLoanTerm > 3600, "loan too short"); - - interestAmountRequired = torqueInterest; - } else { - /// Fixed-term loan. - - if (loanLocal.endTimestamp == 0) { - loanLocal.endTimestamp = block.timestamp.add(maxLoanTerm); - } - - interestAmountRequired = loanLocal.endTimestamp.sub(block.timestamp).mul(owedPerDay).div(86400); - } - - loanInterestLocal.depositTotal = loanInterestLocal.depositTotal.add(interestAmountRequired); - - /// Update remaining lender interest values. - lenderInterestLocal.principalTotal = lenderInterestLocal.principalTotal.add(newPrincipal); - lenderInterestLocal.owedTotal = lenderInterestLocal.owedTotal.add(interestAmountRequired); - } - - /** - * @notice Get the required collateral. - * - * @dev Basically collateral = newPrincipal * marginAmount - * - * @param loanToken The loan token instance address. - * @param collateralToken The collateral token instance address. - * @param newPrincipal The updated amount of principal (current debt). - * @param marginAmount The amount of margin of the trade. - * @param isTorqueLoan Whether the loan is a Torque loan. - * - * @return collateralTokenAmount The required collateral. - * */ - function _getRequiredCollateral( - address loanToken, - address collateralToken, - uint256 newPrincipal, - uint256 marginAmount, - bool isTorqueLoan - ) internal view returns (uint256 collateralTokenAmount) { - if (loanToken == collateralToken) { - collateralTokenAmount = newPrincipal.mul(marginAmount).div(10**20); - } else { - /// Using the price feed instead of the swap expected return - /// because we need the rate in the inverse direction - /// so the swap is probably farther off than the price feed. - (uint256 sourceToDestRate, uint256 sourceToDestPrecision) = IPriceFeeds(priceFeeds).queryRate(collateralToken, loanToken); - if (sourceToDestRate != 0) { - collateralTokenAmount = newPrincipal.mul(sourceToDestPrecision).div(sourceToDestRate).mul(marginAmount).div(10**20); - /*TODO: review + } + } + + /** + * @notice Borrow or trade. + * + * @param loanParamsLocal The loan parameters. + * @param loanId The ID of the loan. If 0, start a new loan. + * @param isTorqueLoan Whether the loan is a Torque loan. + * @param collateralAmountRequired The required amount of collateral. + * @param initialMargin The initial amount of margin. + * @param sentAddresses The addresses to send tokens: lender, borrower, + * receiver and manager: + * lender: must match loan if loanId provided. + * borrower: must match loan if loanId provided. + * receiver: receiver of funds (address(0) assumes borrower address). + * manager: delegated manager of loan unless address(0). + * @param sentValues The values to send: + * newRate: New loan interest rate. + * newPrincipal: New loan size (borrowAmount + any borrowed interest). + * torqueInterest: New amount of interest to escrow for Torque loan (determines initial loan length). + * loanTokenReceived: Total loanToken deposit (amount not sent to borrower in the case of Torque loans). + * collateralTokenReceived: Total collateralToken deposit. + * @param loanDataBytes The payload for the call. These loan DataBytes are + * additional loan data (not in use for token swaps). + * + * @return The new loan size. + * @return The new collateral amount. + * */ + function _borrowOrTrade( + LoanParams memory loanParamsLocal, + bytes32 loanId, + bool isTorqueLoan, + uint256 collateralAmountRequired, + uint256 initialMargin, + address[4] memory sentAddresses, + uint256[5] memory sentValues, + bytes memory loanDataBytes + ) internal returns (uint256, uint256) { + require( + loanParamsLocal.collateralToken != loanParamsLocal.loanToken, + "collateral/loan match" + ); + require(initialMargin >= loanParamsLocal.minInitialMargin, "initialMargin too low"); + + /// maxLoanTerm == 0 indicates a Torque loan and requires that torqueInterest != 0 + require( + loanParamsLocal.maxLoanTerm != 0 || sentValues[2] != 0, /// torqueInterest + "invalid interest" + ); + + /// Initialize loan. + Loan storage loanLocal = + loans[ + _initializeLoan(loanParamsLocal, loanId, initialMargin, sentAddresses, sentValues) + ]; + + // Get required interest. + uint256 amount = + _initializeInterest( + loanParamsLocal, + loanLocal, + sentValues[0], /// newRate + sentValues[1], /// newPrincipal, + sentValues[2] /// torqueInterest + ); + + /// substract out interest from usable loanToken sent. + sentValues[3] = sentValues[3].sub(amount); + + if (isTorqueLoan) { + require(sentValues[3] == 0, "surplus loan token"); + + uint256 borrowingFee = _getBorrowingFee(sentValues[4]); + // need to temp into local state to avoid + address _collateralToken = loanParamsLocal.collateralToken; + address _loanToken = loanParamsLocal.loanToken; + if (borrowingFee != 0) { + _payBorrowingFee( + sentAddresses[1], /// borrower + loanLocal.id, + _collateralToken, /// fee token + _loanToken, /// pairToken (used to check if there is any special rebates or not) -- to pay fee reward + borrowingFee + ); + + sentValues[4] = sentValues[4] /// collateralTokenReceived + .sub(borrowingFee); + } + } else { + /// Update collateral after trade. + /// sentValues[3] is repurposed to hold loanToCollateralSwapRate to avoid stack too deep error. + uint256 receivedAmount; + (receivedAmount, , sentValues[3]) = _loanSwap( + loanId, + loanParamsLocal.loanToken, + loanParamsLocal.collateralToken, + sentAddresses[1], /// borrower + sentValues[3], /// loanTokenUsable (minSourceTokenAmount) + 0, /// maxSourceTokenAmount (0 means minSourceTokenAmount) + 0, /// requiredDestTokenAmount (enforces that all of loanTokenUsable is swapped) + false, /// bypassFee + loanDataBytes + ); + sentValues[4] = sentValues[4] /// collateralTokenReceived + .add(receivedAmount); + } + + /// Settle collateral. + require( + _isCollateralSatisfied( + loanParamsLocal, + loanLocal, + initialMargin, + sentValues[4], + collateralAmountRequired + ), + "collateral insufficient" + ); + + loanLocal.collateral = loanLocal.collateral.add(sentValues[4]); + + if (isTorqueLoan) { + /// reclaiming varaible -> interestDuration + sentValues[2] = loanLocal.endTimestamp.sub(block.timestamp); + } else { + /// reclaiming varaible -> entryLeverage = 100 / initialMargin + sentValues[2] = SafeMath.div(10**38, initialMargin); + } + + _finalizeOpen(loanParamsLocal, loanLocal, sentAddresses, sentValues, isTorqueLoan); + + return (sentValues[1], sentValues[4]); /// newPrincipal, newCollateral + } + + /** + * @notice Finalize an open loan. + * + * @dev Finalize it by updating local parameters of the loan. + * + * @param loanParamsLocal The loan parameters. + * @param loanLocal The loan object. + * @param sentAddresses The addresses to send tokens: lender, borrower, + * receiver and manager: + * lender: must match loan if loanId provided. + * borrower: must match loan if loanId provided. + * receiver: receiver of funds (address(0) assumes borrower address). + * manager: delegated manager of loan unless address(0). + * @param sentValues The values to send: + * newRate: New loan interest rate. + * newPrincipal: New loan size (borrowAmount + any borrowed interest). + * torqueInterest: New amount of interest to escrow for Torque loan (determines initial loan length). + * loanTokenReceived: Total loanToken deposit (amount not sent to borrower in the case of Torque loans). + * collateralTokenReceived: Total collateralToken deposit. + * @param isTorqueLoan Whether the loan is a Torque loan. + * */ + function _finalizeOpen( + LoanParams memory loanParamsLocal, + Loan storage loanLocal, + address[4] memory sentAddresses, + uint256[5] memory sentValues, + bool isTorqueLoan + ) internal { + /// @dev TODO: here the actual used rate and margin should go. + (uint256 initialMargin, uint256 collateralToLoanRate) = + IPriceFeeds(priceFeeds).getCurrentMargin( + loanParamsLocal.loanToken, + loanParamsLocal.collateralToken, + loanLocal.principal, + loanLocal.collateral + ); + require(initialMargin > loanParamsLocal.maintenanceMargin, "unhealthy position"); + + if (loanLocal.startTimestamp == block.timestamp) { + uint256 loanToCollateralPrecision = + IPriceFeeds(priceFeeds).queryPrecision( + loanParamsLocal.loanToken, + loanParamsLocal.collateralToken + ); + uint256 collateralToLoanPrecision = + IPriceFeeds(priceFeeds).queryPrecision( + loanParamsLocal.collateralToken, + loanParamsLocal.loanToken + ); + uint256 totalSwapRate = loanToCollateralPrecision.mul(collateralToLoanPrecision); + loanLocal.startRate = isTorqueLoan + ? collateralToLoanRate + : totalSwapRate.div(sentValues[3]); + } + + _emitOpeningEvents( + loanParamsLocal, + loanLocal, + sentAddresses, + sentValues, + collateralToLoanRate, + initialMargin, + isTorqueLoan + ); + } + + /** + * @notice Emit the opening events. + * + * @param loanParamsLocal The loan parameters. + * @param loanLocal The loan object. + * @param sentAddresses The addresses to send tokens: lender, borrower, + * receiver and manager: + * lender: must match loan if loanId provided. + * borrower: must match loan if loanId provided. + * receiver: receiver of funds (address(0) assumes borrower address). + * manager: delegated manager of loan unless address(0). + * @param sentValues The values to send: + * newRate: New loan interest rate. + * newPrincipal: New loan size (borrowAmount + any borrowed interest). + * torqueInterest: New amount of interest to escrow for Torque loan (determines initial loan length). + * loanTokenReceived: Total loanToken deposit (amount not sent to borrower in the case of Torque loans). + * collateralTokenReceived: Total collateralToken deposit. + * @param collateralToLoanRate The exchange rate from collateral to loan + * tokens. + * @param margin The amount of margin of the trade. + * @param isTorqueLoan Whether the loan is a Torque loan. + * */ + function _emitOpeningEvents( + LoanParams memory loanParamsLocal, + Loan memory loanLocal, + address[4] memory sentAddresses, + uint256[5] memory sentValues, + uint256 collateralToLoanRate, + uint256 margin, + bool isTorqueLoan + ) internal { + if (isTorqueLoan) { + emit Borrow( + sentAddresses[1], /// user (borrower) + sentAddresses[0], /// lender + loanLocal.id, /// loanId + loanParamsLocal.loanToken, /// loanToken + loanParamsLocal.collateralToken, /// collateralToken + sentValues[1], /// newPrincipal + sentValues[4], /// newCollateral + sentValues[0], /// interestRate + sentValues[2], /// interestDuration + collateralToLoanRate, /// collateralToLoanRate, + margin /// currentMargin + ); + } else { + /// currentLeverage = 100 / currentMargin + margin = SafeMath.div(10**38, margin); + + emit Trade( + sentAddresses[1], /// user (trader) + sentAddresses[0], /// lender + loanLocal.id, /// loanId + loanParamsLocal.collateralToken, /// collateralToken + loanParamsLocal.loanToken, /// loanToken + sentValues[4], /// positionSize + sentValues[1], /// borrowedAmount + sentValues[0], /// interestRate, + loanLocal.endTimestamp, /// settlementDate + sentValues[3], /// entryPrice (loanToCollateralSwapRate) + sentValues[2], /// entryLeverage + margin /// currentLeverage + ); + } + } + + /** + * @notice Set the delegated manager. + * + * @param loanId The ID of the loan. If 0, start a new loan. + * @param delegator The address of previous manager. + * @param delegated The address of the delegated manager. + * @param toggle The flag true/false for the delegated manager. + * */ + function _setDelegatedManager( + bytes32 loanId, + address delegator, + address delegated, + bool toggle + ) internal { + delegatedManagers[loanId][delegated] = toggle; + + emit DelegatedManagerSet(loanId, delegator, delegated, toggle); + } + + /** + * @notice Calculate whether the collateral is satisfied. + * + * @dev Basically check collateral + drawdown >= 98% of required. + * + * @param loanParamsLocal The loan parameters. + * @param loanLocal The loan object. + * @param initialMargin The initial amount of margin. + * @param newCollateral The amount of new collateral. + * @param collateralAmountRequired The amount of required collateral. + * + * @return Whether the collateral is satisfied. + * */ + function _isCollateralSatisfied( + LoanParams memory loanParamsLocal, + Loan memory loanLocal, + uint256 initialMargin, + uint256 newCollateral, + uint256 collateralAmountRequired + ) internal view returns (bool) { + /// Allow at most 2% under-collateralized. + collateralAmountRequired = collateralAmountRequired.mul(98 ether).div(100 ether); + + if (newCollateral < collateralAmountRequired) { + /// Check that existing collateral is sufficient coverage. + if (loanLocal.collateral != 0) { + uint256 maxDrawdown = + IPriceFeeds(priceFeeds).getMaxDrawdown( + loanParamsLocal.loanToken, + loanParamsLocal.collateralToken, + loanLocal.principal, + loanLocal.collateral, + initialMargin + ); + return newCollateral.add(maxDrawdown) >= collateralAmountRequired; + } else { + return false; + } + } + return true; + } + + /** + * @notice Initialize a loan. + * + * @param loanParamsLocal The loan parameters. + * @param loanId The ID of the loan. + * @param initialMargin The amount of margin of the trade. + * @param sentAddresses The addresses to send tokens: lender, borrower, + * receiver and manager: + * lender: must match loan if loanId provided. + * borrower: must match loan if loanId provided. + * receiver: receiver of funds (address(0) assumes borrower address). + * manager: delegated manager of loan unless address(0). + * @param sentValues The values to send: + * newRate: New loan interest rate. + * newPrincipal: New loan size (borrowAmount + any borrowed interest). + * torqueInterest: New amount of interest to escrow for Torque loan (determines initial loan length). + * loanTokenReceived: Total loanToken deposit (amount not sent to borrower in the case of Torque loans). + * collateralTokenReceived: Total collateralToken deposit. + * @return The loanId. + * */ + function _initializeLoan( + LoanParams memory loanParamsLocal, + bytes32 loanId, + uint256 initialMargin, + address[4] memory sentAddresses, + uint256[5] memory sentValues + ) internal returns (bytes32) { + require(loanParamsLocal.active, "loanParams disabled"); + + address lender = sentAddresses[0]; + address borrower = sentAddresses[1]; + address manager = sentAddresses[3]; + uint256 newPrincipal = sentValues[1]; + + Loan memory loanLocal; + + if (loanId == 0) { + borrowerNonce[borrower]++; + loanId = keccak256( + abi.encodePacked(loanParamsLocal.id, lender, borrower, borrowerNonce[borrower]) + ); + require(loans[loanId].id == 0, "loan exists"); + + loanLocal = Loan({ + id: loanId, + loanParamsId: loanParamsLocal.id, + pendingTradesId: 0, + active: true, + principal: newPrincipal, + collateral: 0, /// calculated later + startTimestamp: block.timestamp, + endTimestamp: 0, /// calculated later + startMargin: initialMargin, + startRate: 0, /// queried later + borrower: borrower, + lender: lender + }); + + activeLoansSet.addBytes32(loanId); + lenderLoanSets[lender].addBytes32(loanId); + borrowerLoanSets[borrower].addBytes32(loanId); + } else { + loanLocal = loans[loanId]; + require( + loanLocal.active && block.timestamp < loanLocal.endTimestamp, + "loan has ended" + ); + require(loanLocal.borrower == borrower, "borrower mismatch"); + require(loanLocal.lender == lender, "lender mismatch"); + require(loanLocal.loanParamsId == loanParamsLocal.id, "loanParams mismatch"); + + loanLocal.principal = loanLocal.principal.add(newPrincipal); + } + + if (manager != address(0)) { + _setDelegatedManager(loanId, borrower, manager, true); + } + + loans[loanId] = loanLocal; + + return loanId; + } + + /** + * @notice Initialize a loan interest. + * + * @dev A Torque loan is an indefinite-term loan. + * + * @param loanParamsLocal The loan parameters. + * @param loanLocal The loan object. + * @param newRate The new interest rate of the loan. + * @param newPrincipal The new principal amount of the loan. + * @param torqueInterest The interest rate of the Torque loan. + * + * @return interestAmountRequired The interest amount required. + * */ + function _initializeInterest( + LoanParams memory loanParamsLocal, + Loan storage loanLocal, + uint256 newRate, + uint256 newPrincipal, + uint256 torqueInterest /// ignored for fixed-term loans + ) internal returns (uint256 interestAmountRequired) { + /// Pay outstanding interest to lender. + _payInterest(loanLocal.lender, loanParamsLocal.loanToken); + + LoanInterest storage loanInterestLocal = loanInterest[loanLocal.id]; + LenderInterest storage lenderInterestLocal = + lenderInterest[loanLocal.lender][loanParamsLocal.loanToken]; + + uint256 maxLoanTerm = loanParamsLocal.maxLoanTerm; + + _settleFeeRewardForInterestExpense( + loanInterestLocal, + loanLocal.id, + loanParamsLocal.loanToken, /// fee token + loanParamsLocal.collateralToken, /// pairToken (used to check if there is any special rebates or not) -- to pay fee reward + loanLocal.borrower, + block.timestamp + ); + + uint256 previousDepositRemaining; + if (maxLoanTerm == 0 && loanLocal.endTimestamp != 0) { + previousDepositRemaining = loanLocal + .endTimestamp + .sub(block.timestamp) /// block.timestamp < endTimestamp was confirmed earlier. + .mul(loanInterestLocal.owedPerDay) + .div(86400); + } + + uint256 owedPerDay = newPrincipal.mul(newRate).div(365 * 10**20); + + /// Update stored owedPerDay + loanInterestLocal.owedPerDay = loanInterestLocal.owedPerDay.add(owedPerDay); + lenderInterestLocal.owedPerDay = lenderInterestLocal.owedPerDay.add(owedPerDay); + + if (maxLoanTerm == 0) { + /// Indefinite-term (Torque) loan. + + /// torqueInterest != 0 was confirmed earlier. + loanLocal.endTimestamp = torqueInterest + .add(previousDepositRemaining) + .mul(86400) + .div(loanInterestLocal.owedPerDay) + .add(block.timestamp); + + maxLoanTerm = loanLocal.endTimestamp.sub(block.timestamp); + + /// Loan term has to at least be greater than one hour. + require(maxLoanTerm > 3600, "loan too short"); + + interestAmountRequired = torqueInterest; + } else { + /// Fixed-term loan. + + if (loanLocal.endTimestamp == 0) { + loanLocal.endTimestamp = block.timestamp.add(maxLoanTerm); + } + + interestAmountRequired = loanLocal + .endTimestamp + .sub(block.timestamp) + .mul(owedPerDay) + .div(86400); + } + + loanInterestLocal.depositTotal = loanInterestLocal.depositTotal.add( + interestAmountRequired + ); + + /// Update remaining lender interest values. + lenderInterestLocal.principalTotal = lenderInterestLocal.principalTotal.add(newPrincipal); + lenderInterestLocal.owedTotal = lenderInterestLocal.owedTotal.add(interestAmountRequired); + } + + /** + * @notice Get the required collateral. + * + * @dev Basically collateral = newPrincipal * marginAmount + * + * @param loanToken The loan token instance address. + * @param collateralToken The collateral token instance address. + * @param newPrincipal The updated amount of principal (current debt). + * @param marginAmount The amount of margin of the trade. + * @param isTorqueLoan Whether the loan is a Torque loan. + * + * @return collateralTokenAmount The required collateral. + * */ + function _getRequiredCollateral( + address loanToken, + address collateralToken, + uint256 newPrincipal, + uint256 marginAmount, + bool isTorqueLoan + ) internal view returns (uint256 collateralTokenAmount) { + if (loanToken == collateralToken) { + collateralTokenAmount = newPrincipal.mul(marginAmount).div(10**20); + } else { + /// Using the price feed instead of the swap expected return + /// because we need the rate in the inverse direction + /// so the swap is probably farther off than the price feed. + (uint256 sourceToDestRate, uint256 sourceToDestPrecision) = + IPriceFeeds(priceFeeds).queryRate(collateralToken, loanToken); + if (sourceToDestRate != 0) { + collateralTokenAmount = newPrincipal + .mul(sourceToDestPrecision) + .div(sourceToDestRate) + .mul(marginAmount) + .div(10**20); + /*TODO: review collateralTokenAmount = newPrincipal.mul(sourceToDestPrecision).mul(marginAmount).div(sourceToDestRate).div(10**20);*/ - } - } - // ./tests/loan-token/TradingTestToken.test.js - if (isTorqueLoan && collateralTokenAmount != 0) { - collateralTokenAmount = collateralTokenAmount.mul(10**20).div(marginAmount).add(collateralTokenAmount); - } - } + } + } + // ./tests/loan-token/TradingTestToken.test.js + if (isTorqueLoan && collateralTokenAmount != 0) { + collateralTokenAmount = collateralTokenAmount.mul(10**20).div(marginAmount).add( + collateralTokenAmount + ); + } + } } diff --git a/contracts/modules/LoanSettings.sol b/contracts/modules/LoanSettings.sol index 364a59e2e..35f4d7547 100644 --- a/contracts/modules/LoanSettings.sol +++ b/contracts/modules/LoanSettings.sol @@ -19,204 +19,212 @@ import "../mixins/ModuleCommonFunctionalities.sol"; * This contract contains functions to get and set loan parameters. * */ contract LoanSettings is State, LoanSettingsEvents, ModuleCommonFunctionalities { - /** - * @notice Empty public constructor. - * */ - constructor() public {} - - /** - * @notice Fallback function is to react to receiving value (rBTC). - * */ - function() external { - revert("LoanSettings - fallback not allowed"); - } - - /** - * @notice Set function selectors on target contract. - * - * @param target The address of the target contract. - * */ - function initialize(address target) external onlyOwner { - address prevModuleContractAddress = logicTargets[this.setupLoanParams.selector]; - _setTarget(this.setupLoanParams.selector, target); - _setTarget(this.disableLoanParams.selector, target); - _setTarget(this.getLoanParams.selector, target); - _setTarget(this.getLoanParamsList.selector, target); - _setTarget(this.getTotalPrincipal.selector, target); - _setTarget(this.minInitialMargin.selector, target); - emit ProtocolModuleContractReplaced(prevModuleContractAddress, target, "LoanSettings"); - } - - /** - * @notice Setup loan parameters, by looping every loan - * and populating its parameters. - * - * @dev For each loan calls _setupLoanParams internal function. - * - * @param loanParamsList The array of loan parameters. - * - * @return loanParamsIdList The array of loan parameters IDs. - * */ - function setupLoanParams(LoanParams[] calldata loanParamsList) external whenNotPaused returns (bytes32[] memory loanParamsIdList) { - loanParamsIdList = new bytes32[](loanParamsList.length); - for (uint256 i = 0; i < loanParamsList.length; i++) { - loanParamsIdList[i] = _setupLoanParams(loanParamsList[i]); - } - } - - /** - * @notice Deactivate LoanParams for future loans. Active loans - * using it are unaffected. - * - * @param loanParamsIdList The array of loan parameters IDs to deactivate. - * */ - function disableLoanParams(bytes32[] calldata loanParamsIdList) external whenNotPaused { - for (uint256 i = 0; i < loanParamsIdList.length; i++) { - require(msg.sender == loanParams[loanParamsIdList[i]].owner, "unauthorized owner"); - loanParams[loanParamsIdList[i]].active = false; - - LoanParams memory loanParamsLocal = loanParams[loanParamsIdList[i]]; - emit LoanParamsDisabled( - loanParamsLocal.id, - loanParamsLocal.owner, - loanParamsLocal.loanToken, - loanParamsLocal.collateralToken, - loanParamsLocal.minInitialMargin, - loanParamsLocal.maintenanceMargin, - loanParamsLocal.maxLoanTerm - ); - emit LoanParamsIdDisabled(loanParamsLocal.id, loanParamsLocal.owner); - } - } - - /** - * @notice Get loan parameters for every matching IDs. - * - * @param loanParamsIdList The array of loan parameters IDs to match. - * - * @return loanParamsList The result array of loan parameters. - * */ - function getLoanParams(bytes32[] memory loanParamsIdList) public view returns (LoanParams[] memory loanParamsList) { - loanParamsList = new LoanParams[](loanParamsIdList.length); - uint256 itemCount; - - for (uint256 i = 0; i < loanParamsIdList.length; i++) { - LoanParams memory loanParamsLocal = loanParams[loanParamsIdList[i]]; - if (loanParamsLocal.id == 0) { - continue; - } - loanParamsList[itemCount] = loanParamsLocal; - itemCount++; - } - - if (itemCount < loanParamsList.length) { - assembly { - mstore(loanParamsList, itemCount) - } - } - } - - /** - * @notice Get loan parameters for an owner and a given page - * defined by an offset and a limit. - * - * @param owner The address of the loan owner. - * @param start The page offset. - * @param count The page limit. - * - * @return loanParamsList The result array of loan parameters. - * */ - function getLoanParamsList( - address owner, - uint256 start, - uint256 count - ) external view returns (bytes32[] memory loanParamsList) { - EnumerableBytes32Set.Bytes32Set storage set = userLoanParamSets[owner]; - uint256 end = start.add(count).min256(set.length()); - if (start >= end) { - return loanParamsList; - } - - loanParamsList = new bytes32[](count); - uint256 itemCount; - for (uint256 i = end - start; i > 0; i--) { - if (itemCount == count) { - break; - } - loanParamsList[itemCount] = set.get(i + start - 1); - itemCount++; - } - - if (itemCount < count) { - assembly { - mstore(loanParamsList, itemCount) - } - } - } - - /** - * @notice Get the total principal of the loans by a lender. - * - * @param lender The address of the lender. - * @param loanToken The address of the token instance. - * - * @return The total principal of the loans. - * */ - function getTotalPrincipal(address lender, address loanToken) external view returns (uint256) { - return lenderInterest[lender][loanToken].principalTotal; - } - - /** - * @notice Setup a loan parameters. - * - * @param loanParamsLocal The loan parameters. - * - * @return loanParamsId The loan parameters ID. - * */ - function _setupLoanParams(LoanParams memory loanParamsLocal) internal returns (bytes32) { - bytes32 loanParamsId = - keccak256( - abi.encodePacked( - loanParamsLocal.loanToken, - loanParamsLocal.collateralToken, - loanParamsLocal.minInitialMargin, - loanParamsLocal.maintenanceMargin, - loanParamsLocal.maxLoanTerm, - block.timestamp - ) - ); - require(loanParams[loanParamsId].id == 0, "loanParams exists"); - - require( - loanParamsLocal.loanToken != address(0) && - loanParamsLocal.collateralToken != address(0) && - loanParamsLocal.minInitialMargin > loanParamsLocal.maintenanceMargin && - (loanParamsLocal.maxLoanTerm == 0 || loanParamsLocal.maxLoanTerm > 3600), /// A defined maxLoanTerm has to be greater than one hour. - "invalid params" - ); - - loanParamsLocal.id = loanParamsId; - loanParamsLocal.active = true; - loanParamsLocal.owner = msg.sender; - - loanParams[loanParamsId] = loanParamsLocal; - userLoanParamSets[msg.sender].addBytes32(loanParamsId); - - emit LoanParamsSetup( - loanParamsId, - loanParamsLocal.owner, - loanParamsLocal.loanToken, - loanParamsLocal.collateralToken, - loanParamsLocal.minInitialMargin, - loanParamsLocal.maintenanceMargin, - loanParamsLocal.maxLoanTerm - ); - emit LoanParamsIdSetup(loanParamsId, loanParamsLocal.owner); - - return loanParamsId; - } - - function minInitialMargin(bytes32 loanParamsId) external view returns (uint256) { - return loanParams[loanParamsId].minInitialMargin; - } + /** + * @notice Empty public constructor. + * */ + constructor() public {} + + /** + * @notice Fallback function is to react to receiving value (rBTC). + * */ + function() external { + revert("LoanSettings - fallback not allowed"); + } + + /** + * @notice Set function selectors on target contract. + * + * @param target The address of the target contract. + * */ + function initialize(address target) external onlyOwner { + address prevModuleContractAddress = logicTargets[this.setupLoanParams.selector]; + _setTarget(this.setupLoanParams.selector, target); + _setTarget(this.disableLoanParams.selector, target); + _setTarget(this.getLoanParams.selector, target); + _setTarget(this.getLoanParamsList.selector, target); + _setTarget(this.getTotalPrincipal.selector, target); + _setTarget(this.minInitialMargin.selector, target); + emit ProtocolModuleContractReplaced(prevModuleContractAddress, target, "LoanSettings"); + } + + /** + * @notice Setup loan parameters, by looping every loan + * and populating its parameters. + * + * @dev For each loan calls _setupLoanParams internal function. + * + * @param loanParamsList The array of loan parameters. + * + * @return loanParamsIdList The array of loan parameters IDs. + * */ + function setupLoanParams(LoanParams[] calldata loanParamsList) + external + whenNotPaused + returns (bytes32[] memory loanParamsIdList) + { + loanParamsIdList = new bytes32[](loanParamsList.length); + for (uint256 i = 0; i < loanParamsList.length; i++) { + loanParamsIdList[i] = _setupLoanParams(loanParamsList[i]); + } + } + + /** + * @notice Deactivate LoanParams for future loans. Active loans + * using it are unaffected. + * + * @param loanParamsIdList The array of loan parameters IDs to deactivate. + * */ + function disableLoanParams(bytes32[] calldata loanParamsIdList) external whenNotPaused { + for (uint256 i = 0; i < loanParamsIdList.length; i++) { + require(msg.sender == loanParams[loanParamsIdList[i]].owner, "unauthorized owner"); + loanParams[loanParamsIdList[i]].active = false; + + LoanParams memory loanParamsLocal = loanParams[loanParamsIdList[i]]; + emit LoanParamsDisabled( + loanParamsLocal.id, + loanParamsLocal.owner, + loanParamsLocal.loanToken, + loanParamsLocal.collateralToken, + loanParamsLocal.minInitialMargin, + loanParamsLocal.maintenanceMargin, + loanParamsLocal.maxLoanTerm + ); + emit LoanParamsIdDisabled(loanParamsLocal.id, loanParamsLocal.owner); + } + } + + /** + * @notice Get loan parameters for every matching IDs. + * + * @param loanParamsIdList The array of loan parameters IDs to match. + * + * @return loanParamsList The result array of loan parameters. + * */ + function getLoanParams(bytes32[] memory loanParamsIdList) + public + view + returns (LoanParams[] memory loanParamsList) + { + loanParamsList = new LoanParams[](loanParamsIdList.length); + uint256 itemCount; + + for (uint256 i = 0; i < loanParamsIdList.length; i++) { + LoanParams memory loanParamsLocal = loanParams[loanParamsIdList[i]]; + if (loanParamsLocal.id == 0) { + continue; + } + loanParamsList[itemCount] = loanParamsLocal; + itemCount++; + } + + if (itemCount < loanParamsList.length) { + assembly { + mstore(loanParamsList, itemCount) + } + } + } + + /** + * @notice Get loan parameters for an owner and a given page + * defined by an offset and a limit. + * + * @param owner The address of the loan owner. + * @param start The page offset. + * @param count The page limit. + * + * @return loanParamsList The result array of loan parameters. + * */ + function getLoanParamsList( + address owner, + uint256 start, + uint256 count + ) external view returns (bytes32[] memory loanParamsList) { + EnumerableBytes32Set.Bytes32Set storage set = userLoanParamSets[owner]; + uint256 end = start.add(count).min256(set.length()); + if (start >= end) { + return loanParamsList; + } + + loanParamsList = new bytes32[](count); + uint256 itemCount; + for (uint256 i = end - start; i > 0; i--) { + if (itemCount == count) { + break; + } + loanParamsList[itemCount] = set.get(i + start - 1); + itemCount++; + } + + if (itemCount < count) { + assembly { + mstore(loanParamsList, itemCount) + } + } + } + + /** + * @notice Get the total principal of the loans by a lender. + * + * @param lender The address of the lender. + * @param loanToken The address of the token instance. + * + * @return The total principal of the loans. + * */ + function getTotalPrincipal(address lender, address loanToken) external view returns (uint256) { + return lenderInterest[lender][loanToken].principalTotal; + } + + /** + * @notice Setup a loan parameters. + * + * @param loanParamsLocal The loan parameters. + * + * @return loanParamsId The loan parameters ID. + * */ + function _setupLoanParams(LoanParams memory loanParamsLocal) internal returns (bytes32) { + bytes32 loanParamsId = + keccak256( + abi.encodePacked( + loanParamsLocal.loanToken, + loanParamsLocal.collateralToken, + loanParamsLocal.minInitialMargin, + loanParamsLocal.maintenanceMargin, + loanParamsLocal.maxLoanTerm, + block.timestamp + ) + ); + require(loanParams[loanParamsId].id == 0, "loanParams exists"); + + require( + loanParamsLocal.loanToken != address(0) && + loanParamsLocal.collateralToken != address(0) && + loanParamsLocal.minInitialMargin > loanParamsLocal.maintenanceMargin && + (loanParamsLocal.maxLoanTerm == 0 || loanParamsLocal.maxLoanTerm > 3600), /// A defined maxLoanTerm has to be greater than one hour. + "invalid params" + ); + + loanParamsLocal.id = loanParamsId; + loanParamsLocal.active = true; + loanParamsLocal.owner = msg.sender; + + loanParams[loanParamsId] = loanParamsLocal; + userLoanParamSets[msg.sender].addBytes32(loanParamsId); + + emit LoanParamsSetup( + loanParamsId, + loanParamsLocal.owner, + loanParamsLocal.loanToken, + loanParamsLocal.collateralToken, + loanParamsLocal.minInitialMargin, + loanParamsLocal.maintenanceMargin, + loanParamsLocal.maxLoanTerm + ); + emit LoanParamsIdSetup(loanParamsId, loanParamsLocal.owner); + + return loanParamsId; + } + + function minInitialMargin(bytes32 loanParamsId) external view returns (uint256) { + return loanParams[loanParamsId].minInitialMargin; + } } diff --git a/contracts/modules/ProtocolSettings.sol b/contracts/modules/ProtocolSettings.sol index 41ee62e9e..def54dedc 100644 --- a/contracts/modules/ProtocolSettings.sol +++ b/contracts/modules/ProtocolSettings.sol @@ -24,748 +24,843 @@ import "../feeds/IPriceFeeds.sol"; * * This contract contains functions to customize protocol settings. * */ -contract ProtocolSettings is State, ProtocolTokenUser, ProtocolSettingsEvents, ModuleCommonFunctionalities { - using SafeERC20 for IERC20; - using SafeMath for uint256; - - /** - * @notice Empty public constructor. - * */ - constructor() public {} - - /** - * @notice Fallback function is to react to receiving value (rBTC). - * */ - function() external { - revert("fallback not allowed"); - } - - /** - * @notice Set function selectors on target contract. - * - * @param target The address of the target contract. - * */ - function initialize(address target) external onlyOwner { - address prevModuleContractAddress = logicTargets[this.setPriceFeedContract.selector]; - _setTarget(this.setPriceFeedContract.selector, target); - _setTarget(this.setSwapsImplContract.selector, target); - _setTarget(this.setLoanPool.selector, target); - _setTarget(this.setSupportedTokens.selector, target); - _setTarget(this.setLendingFeePercent.selector, target); - _setTarget(this.setTradingFeePercent.selector, target); - _setTarget(this.setBorrowingFeePercent.selector, target); - _setTarget(this.setSwapExternalFeePercent.selector, target); - _setTarget(this.setAffiliateFeePercent.selector, target); - _setTarget(this.setAffiliateTradingTokenFeePercent.selector, target); - _setTarget(this.setLiquidationIncentivePercent.selector, target); - _setTarget(this.setMaxDisagreement.selector, target); - _setTarget(this.setSourceBuffer.selector, target); - _setTarget(this.setMaxSwapSize.selector, target); - _setTarget(this.setFeesController.selector, target); - _setTarget(this.withdrawFees.selector, target); - _setTarget(this.withdrawLendingFees.selector, target); - _setTarget(this.withdrawTradingFees.selector, target); - _setTarget(this.withdrawBorrowingFees.selector, target); - _setTarget(this.withdrawProtocolToken.selector, target); - _setTarget(this.depositProtocolToken.selector, target); - _setTarget(this.getLoanPoolsList.selector, target); - _setTarget(this.isLoanPool.selector, target); - _setTarget(this.setSovrynSwapContractRegistryAddress.selector, target); - _setTarget(this.setWrbtcToken.selector, target); - _setTarget(this.setProtocolTokenAddress.selector, target); - _setTarget(this.setRolloverBaseReward.selector, target); - _setTarget(this.setRebatePercent.selector, target); - _setTarget(this.setSpecialRebates.selector, target); - _setTarget(this.setSovrynProtocolAddress.selector, target); - _setTarget(this.setSOVTokenAddress.selector, target); - _setTarget(this.setLockedSOVAddress.selector, target); - _setTarget(this.setMinReferralsToPayoutAffiliates.selector, target); - _setTarget(this.getSpecialRebates.selector, target); - _setTarget(this.getProtocolAddress.selector, target); - _setTarget(this.getSovTokenAddress.selector, target); - _setTarget(this.getLockedSOVAddress.selector, target); - _setTarget(this.getFeeRebatePercent.selector, target); - _setTarget(this.togglePaused.selector, target); - _setTarget(this.isProtocolPaused.selector, target); - _setTarget(this.getSwapExternalFeePercent.selector, target); - _setTarget(this.setTradingRebateRewardsBasisPoint.selector, target); - _setTarget(this.getTradingRebateRewardsBasisPoint.selector, target); - _setTarget(this.getDedicatedSOVRebate.selector, target); - _setTarget(this.setRolloverFlexFeePercent.selector, target); - emit ProtocolModuleContractReplaced(prevModuleContractAddress, target, "ProtocolSettings"); - } - - /** - * setting wrong address will break inter module functions calling - * should be set once - */ - function setSovrynProtocolAddress(address newProtocolAddress) external onlyOwner whenNotPaused { - address oldProtocolAddress = protocolAddress; - protocolAddress = newProtocolAddress; - - emit SetProtocolAddress(msg.sender, oldProtocolAddress, newProtocolAddress); - } - - function setSOVTokenAddress(address newSovTokenAddress) external onlyOwner whenNotPaused { - require(Address.isContract(newSovTokenAddress), "newSovTokenAddress not a contract"); - - address oldTokenAddress = sovTokenAddress; - sovTokenAddress = newSovTokenAddress; - - emit SetSOVTokenAddress(msg.sender, oldTokenAddress, newSovTokenAddress); - } - - function setLockedSOVAddress(address newLockedSOVAddress) external onlyOwner whenNotPaused { - require(Address.isContract(newLockedSOVAddress), "newLockSOVAddress not a contract"); - - address oldLockedSOVAddress = lockedSOVAddress; - lockedSOVAddress = newLockedSOVAddress; - - emit SetLockedSOVAddress(msg.sender, oldLockedSOVAddress, newLockedSOVAddress); - } - - /** - * @notice Set the basis point of trading rebate rewards (SOV), max value is 9999 (99.99% liquid, 0.01% vested). - * - * @param newBasisPoint Basis point value. - */ - function setTradingRebateRewardsBasisPoint(uint256 newBasisPoint) external onlyOwner whenNotPaused { - require(newBasisPoint <= 9999, "value too high"); - - uint256 oldBasisPoint = tradingRebateRewardsBasisPoint; - tradingRebateRewardsBasisPoint = newBasisPoint; - - emit SetTradingRebateRewardsBasisPoint(msg.sender, oldBasisPoint, newBasisPoint); - } - - /** - * @notice Update the minimum number of referrals to get affiliates rewards. - * - * @param newMinReferrals The new minimum number of referrals. - * */ - function setMinReferralsToPayoutAffiliates(uint256 newMinReferrals) external onlyOwner whenNotPaused { - uint256 oldMinReferrals = minReferralsToPayout; - minReferralsToPayout = newMinReferrals; - - emit SetMinReferralsToPayoutAffiliates(msg.sender, oldMinReferrals, newMinReferrals); - } - - /** - * @notice Set the address of the Price Feed instance. - * - * @param newContract The address of the Price Feed new instance. - * */ - function setPriceFeedContract(address newContract) external onlyOwner whenNotPaused { - address oldContract = priceFeeds; - priceFeeds = newContract; - - emit SetPriceFeedContract(msg.sender, oldContract, newContract); - } - - /** - * @notice Set the address of the asset swapper instance. - * - * @param newContract The address of the asset swapper new instance. - * */ - function setSwapsImplContract(address newContract) external onlyOwner whenNotPaused { - address oldContract = swapsImpl; - swapsImpl = newContract; - - emit SetSwapsImplContract(msg.sender, oldContract, newContract); - } - - /** - * @notice Set a list of loan pools and its tokens. - * - * @param pools The array of addresses of new loan pool instances. - * @param assets The array of addresses of the corresponding underlying tokens. - * */ - function setLoanPool(address[] calldata pools, address[] calldata assets) external onlyOwner whenNotPaused { - require(pools.length == assets.length, "count mismatch"); - - for (uint256 i = 0; i < pools.length; i++) { - require(pools[i] != assets[i], "pool == asset"); - require(pools[i] != address(0), "pool == 0"); - require(assets[i] != address(0) || loanPoolToUnderlying[pools[i]] != address(0), "pool not exists"); - if (assets[i] == address(0)) { - underlyingToLoanPool[loanPoolToUnderlying[pools[i]]] = address(0); - loanPoolToUnderlying[pools[i]] = address(0); - loanPoolsSet.removeAddress(pools[i]); - } else { - loanPoolToUnderlying[pools[i]] = assets[i]; - underlyingToLoanPool[assets[i]] = pools[i]; - loanPoolsSet.addAddress(pools[i]); - } - - emit SetLoanPool(msg.sender, pools[i], assets[i]); - } - } - - /** - * @notice Set a list of supported tokens by populating the - * storage supportedTokens mapping. - * - * @param addrs The array of addresses of the tokens. - * @param toggles The array of flags indicating whether - * the corresponding token is supported or not. - * */ - function setSupportedTokens(address[] calldata addrs, bool[] calldata toggles) external onlyOwner whenNotPaused { - require(addrs.length == toggles.length, "count mismatch"); - - for (uint256 i = 0; i < addrs.length; i++) { - supportedTokens[addrs[i]] = toggles[i]; - - emit SetSupportedTokens(msg.sender, addrs[i], toggles[i]); - } - } - - /** - * @notice Set the value of lendingFeePercent storage variable. - * - * @param newValue The new value for lendingFeePercent. - * */ - function setLendingFeePercent(uint256 newValue) external onlyOwner whenNotPaused { - require(newValue <= 10**20, "value too high"); - uint256 oldValue = lendingFeePercent; - lendingFeePercent = newValue; - - emit SetLendingFeePercent(msg.sender, oldValue, newValue); - } - - /** - * @notice Set the value of tradingFeePercent storage variable. - * - * @param newValue The new value for tradingFeePercent. - * */ - function setTradingFeePercent(uint256 newValue) external onlyOwner whenNotPaused { - require(newValue <= 10**20, "value too high"); - uint256 oldValue = tradingFeePercent; - tradingFeePercent = newValue; - - emit SetTradingFeePercent(msg.sender, oldValue, newValue); - } - - /** - * @notice Set the value of borrowingFeePercent storage variable. - * - * @param newValue The new value for borrowingFeePercent. - * */ - function setBorrowingFeePercent(uint256 newValue) external onlyOwner whenNotPaused { - require(newValue <= 10**20, "value too high"); - uint256 oldValue = borrowingFeePercent; - borrowingFeePercent = newValue; - - emit SetBorrowingFeePercent(msg.sender, oldValue, newValue); - } - - /** - * @notice Set the value of swapExtrernalFeePercent storage variable - * - * @param newValue the new value for swapExternalFeePercent - */ - function setSwapExternalFeePercent(uint256 newValue) external onlyOwner whenNotPaused { - require(newValue <= 10**20, "value too high"); - uint256 oldValue = swapExtrernalFeePercent; - swapExtrernalFeePercent = newValue; - - emit SetSwapExternalFeePercent(msg.sender, oldValue, newValue); - } - - /** - * @notice Set the value of affiliateFeePercent storage variable. - * - * @param newValue The new value for affiliateFeePercent. - * */ - function setAffiliateFeePercent(uint256 newValue) external onlyOwner whenNotPaused { - require(newValue <= 10**20, "value too high"); - uint256 oldValue = affiliateFeePercent; - affiliateFeePercent = newValue; - - emit SetAffiliateFeePercent(msg.sender, oldValue, newValue); - } - - /** - * @notice Set the value of affiliateTradingTokenFeePercent storage variable. - * - * @param newValue The new value for affiliateTradingTokenFeePercent. - * */ - function setAffiliateTradingTokenFeePercent(uint256 newValue) external onlyOwner whenNotPaused { - require(newValue <= 10**20, "value too high"); - uint256 oldValue = affiliateTradingTokenFeePercent; - affiliateTradingTokenFeePercent = newValue; - - emit SetAffiliateTradingTokenFeePercent(msg.sender, oldValue, newValue); - } - - /** - * @notice Set the value of liquidationIncentivePercent storage variable. - * - * @param newValue The new value for liquidationIncentivePercent. - * */ - function setLiquidationIncentivePercent(uint256 newValue) external onlyOwner whenNotPaused { - require(newValue <= 10**20, "value too high"); - uint256 oldValue = liquidationIncentivePercent; - liquidationIncentivePercent = newValue; - - emit SetLiquidationIncentivePercent(msg.sender, oldValue, newValue); - } - - /** - * @notice Set the value of the maximum swap spread. - * - * @param newValue The new value for maxDisagreement. - * */ - function setMaxDisagreement(uint256 newValue) external onlyOwner whenNotPaused { - maxDisagreement = newValue; - } - - /** - * @notice Set the value of the maximum source buffer. - * - * @dev To avoid rounding issues on the swap rate a small buffer is implemented. - * - * @param newValue The new value for the maximum source buffer. - * */ - function setSourceBuffer(uint256 newValue) external onlyOwner whenNotPaused { - sourceBuffer = newValue; - } - - /** - * @notice Set the value of the swap size limit. - * - * @param newValue The new value for the maximum swap size. - * */ - function setMaxSwapSize(uint256 newValue) external onlyOwner whenNotPaused { - uint256 oldValue = maxSwapSize; - maxSwapSize = newValue; - - emit SetMaxSwapSize(msg.sender, oldValue, newValue); - } - - /** - * @notice Set the address of the feesController instance. - * - * @dev The fee sharing proxy must be the feesController of the - * protocol contract. This allows the fee sharing proxy - * to withdraw the fees. - * - * @param newController The new address of the feesController. - * */ - function setFeesController(address newController) external onlyOwner whenNotPaused { - address oldController = feesController; - feesController = newController; - - emit SetFeesController(msg.sender, oldController, newController); - } - - /** - * @notice The feesController calls this function to withdraw fees - * from three sources: lending, trading and borrowing. - * The fees (except SOV) will be converted to wRBTC. - * For SOV, it will be deposited directly to feeSharingProxy from the protocol. - * - * @param tokens The array of address of the token instance. - * @param receiver The address of the withdrawal recipient. - * - * @return The withdrawn total amount in wRBTC - * */ - function withdrawFees(address[] calldata tokens, address receiver) external whenNotPaused returns (uint256 totalWRBTCWithdrawn) { - require(msg.sender == feesController, "unauthorized"); - - for (uint256 i = 0; i < tokens.length; i++) { - uint256 lendingBalance = lendingFeeTokensHeld[tokens[i]]; - if (lendingBalance > 0) { - lendingFeeTokensHeld[tokens[i]] = 0; - lendingFeeTokensPaid[tokens[i]] = lendingFeeTokensPaid[tokens[i]].add(lendingBalance); - } - - uint256 tradingBalance = tradingFeeTokensHeld[tokens[i]]; - if (tradingBalance > 0) { - tradingFeeTokensHeld[tokens[i]] = 0; - tradingFeeTokensPaid[tokens[i]] = tradingFeeTokensPaid[tokens[i]].add(tradingBalance); - } - - uint256 borrowingBalance = borrowingFeeTokensHeld[tokens[i]]; - if (borrowingBalance > 0) { - borrowingFeeTokensHeld[tokens[i]] = 0; - borrowingFeeTokensPaid[tokens[i]] = borrowingFeeTokensPaid[tokens[i]].add(borrowingBalance); - } - - uint256 tempAmount = lendingBalance.add(tradingBalance).add(borrowingBalance); - - if (tempAmount == 0) { - continue; - } - - uint256 amountConvertedToWRBTC; - if (tokens[i] == address(sovTokenAddress)) { - IERC20(tokens[i]).approve(feesController, tempAmount); - IFeeSharingProxy(feesController).transferTokens(address(sovTokenAddress), uint96(tempAmount)); - amountConvertedToWRBTC = 0; - } else { - if (tokens[i] == address(wrbtcToken)) { - amountConvertedToWRBTC = tempAmount; - - IERC20(address(wrbtcToken)).safeTransfer(receiver, amountConvertedToWRBTC); - } else { - IERC20(tokens[i]).approve(protocolAddress, tempAmount); - - (amountConvertedToWRBTC, ) = ProtocolSwapExternalInterface(protocolAddress).swapExternal( - tokens[i], // source token address - address(wrbtcToken), // dest token address - feesController, // set feeSharingProxy as receiver - protocolAddress, // protocol as the sender - tempAmount, // source token amount - 0, // reqDestToken - 0, // minReturn - "" // loan data bytes - ); - - /// Will revert if disagreement found. - IPriceFeeds(priceFeeds).checkPriceDisagreement( - tokens[i], - address(wrbtcToken), - tempAmount, - amountConvertedToWRBTC, - maxDisagreement - ); - } - - totalWRBTCWithdrawn = totalWRBTCWithdrawn.add(amountConvertedToWRBTC); - } - - emit WithdrawFees(msg.sender, tokens[i], receiver, lendingBalance, tradingBalance, borrowingBalance, amountConvertedToWRBTC); - } - - return totalWRBTCWithdrawn; - } - - /** - * @notice The feesController calls this function to withdraw fees - * accrued from lending operations. - * - * @param token The address of the token instance. - * @param receiver The address of the withdrawal recipient. - * @param amount The amount of fees to get, ignored if greater than balance. - * - * @return Whether withdrawal was successful. - * */ - function withdrawLendingFees( - address token, - address receiver, - uint256 amount - ) external whenNotPaused returns (bool) { - require(msg.sender == feesController, "unauthorized"); - - uint256 withdrawAmount = amount; - - uint256 balance = lendingFeeTokensHeld[token]; - if (withdrawAmount > balance) { - withdrawAmount = balance; - } - if (withdrawAmount == 0) { - return false; - } - - lendingFeeTokensHeld[token] = balance.sub(withdrawAmount); - lendingFeeTokensPaid[token] = lendingFeeTokensPaid[token].add(withdrawAmount); - - IERC20(token).safeTransfer(receiver, withdrawAmount); - - emit WithdrawLendingFees(msg.sender, token, receiver, withdrawAmount); - - return true; - } - - /** - * @notice The feesController calls this function to withdraw fees - * accrued from trading operations. - * - * @param token The address of the token instance. - * @param receiver The address of the withdrawal recipient. - * @param amount The amount of fees to get, ignored if greater than balance. - * - * @return Whether withdrawal was successful. - * */ - function withdrawTradingFees( - address token, - address receiver, - uint256 amount - ) external whenNotPaused returns (bool) { - require(msg.sender == feesController, "unauthorized"); - - uint256 withdrawAmount = amount; - - uint256 balance = tradingFeeTokensHeld[token]; - if (withdrawAmount > balance) { - withdrawAmount = balance; - } - if (withdrawAmount == 0) { - return false; - } - - tradingFeeTokensHeld[token] = balance.sub(withdrawAmount); - tradingFeeTokensPaid[token] = tradingFeeTokensPaid[token].add(withdrawAmount); - - IERC20(token).safeTransfer(receiver, withdrawAmount); - - emit WithdrawTradingFees(msg.sender, token, receiver, withdrawAmount); - - return true; - } - - /** - * @notice The feesController calls this function to withdraw fees - * accrued from borrowing operations. - * - * @param token The address of the token instance. - * @param receiver The address of the withdrawal recipient. - * @param amount The amount of fees to get, ignored if greater than balance. - * - * @return Whether withdrawal was successful. - * */ - function withdrawBorrowingFees( - address token, - address receiver, - uint256 amount - ) external whenNotPaused returns (bool) { - require(msg.sender == feesController, "unauthorized"); - - uint256 withdrawAmount = amount; - - uint256 balance = borrowingFeeTokensHeld[token]; - if (withdrawAmount > balance) { - withdrawAmount = balance; - } - if (withdrawAmount == 0) { - return false; - } - - borrowingFeeTokensHeld[token] = balance.sub(withdrawAmount); - borrowingFeeTokensPaid[token] = borrowingFeeTokensPaid[token].add(withdrawAmount); - - IERC20(token).safeTransfer(receiver, withdrawAmount); - - emit WithdrawBorrowingFees(msg.sender, token, receiver, withdrawAmount); - - return true; - } - - /** - * @notice The owner calls this function to withdraw protocol tokens. - * - * @dev Wrapper for ProtocolTokenUser::_withdrawProtocolToken internal function. - * - * @param receiver The address of the withdrawal recipient. - * @param amount The amount of tokens to get. - * - * @return The protocol token address. - * @return Withdrawal success (true/false). - * */ - function withdrawProtocolToken(address receiver, uint256 amount) external onlyOwner whenNotPaused returns (address, bool) { - return _withdrawProtocolToken(receiver, amount); - } - - /** - * @notice The owner calls this function to deposit protocol tokens. - * - * @param amount The tokens of fees to send. - * */ - function depositProtocolToken(uint256 amount) external onlyOwner whenNotPaused { - /// @dev Update local balance - protocolTokenHeld = protocolTokenHeld.add(amount); - - /// @dev Send the tokens - IERC20(protocolTokenAddress).safeTransferFrom(msg.sender, address(this), amount); - } - - /** - * @notice Get a list of loan pools. - * - * @param start The offset. - * @param count The limit. - * - * @return The array of loan pools. - * */ - function getLoanPoolsList(uint256 start, uint256 count) external view returns (bytes32[] memory) { - return loanPoolsSet.enumerate(start, count); - } - - /** - * @notice Check whether a token is a pool token. - * - * @dev By querying its underlying token. - * - * @param loanPool The token address to check. - * */ - function isLoanPool(address loanPool) external view returns (bool) { - return loanPoolToUnderlying[loanPool] != address(0); - } - - /** - * @notice Set the contract registry address of the SovrynSwap network. - * - * @param registryAddress the address of the registry contract. - * */ - function setSovrynSwapContractRegistryAddress(address registryAddress) external onlyOwner whenNotPaused { - require(Address.isContract(registryAddress), "registryAddress not a contract"); - - address oldSovrynSwapContractRegistryAddress = sovrynSwapContractRegistryAddress; - sovrynSwapContractRegistryAddress = registryAddress; - - emit SetSovrynSwapContractRegistryAddress(msg.sender, oldSovrynSwapContractRegistryAddress, sovrynSwapContractRegistryAddress); - } - - /** - * @notice Set the wrBTC contract address. - * - * @param wrbtcTokenAddress The address of the wrBTC contract. - * */ - function setWrbtcToken(address wrbtcTokenAddress) external onlyOwner whenNotPaused { - require(Address.isContract(wrbtcTokenAddress), "wrbtcTokenAddress not a contract"); - - address oldwrbtcToken = address(wrbtcToken); - wrbtcToken = IWrbtcERC20(wrbtcTokenAddress); - - emit SetWrbtcToken(msg.sender, oldwrbtcToken, wrbtcTokenAddress); - } - - /** - * @notice Set the protocol token contract address. - * - * @param _protocolTokenAddress The address of the protocol token contract. - * */ - function setProtocolTokenAddress(address _protocolTokenAddress) external onlyOwner whenNotPaused { - require(Address.isContract(_protocolTokenAddress), "_protocolTokenAddress not a contract"); - - address oldProtocolTokenAddress = protocolTokenAddress; - protocolTokenAddress = _protocolTokenAddress; - - emit SetProtocolTokenAddress(msg.sender, oldProtocolTokenAddress, _protocolTokenAddress); - } - - /** - * @notice Set rollover base reward. It should be denominated in wrBTC. - * - * @param baseRewardValue The base reward. - * */ - function setRolloverBaseReward(uint256 baseRewardValue) external onlyOwner whenNotPaused { - require(baseRewardValue > 0, "Base reward is zero"); - - uint256 oldValue = rolloverBaseReward; - rolloverBaseReward = baseRewardValue; - - emit SetRolloverBaseReward(msg.sender, oldValue, rolloverBaseReward); - } - - /** - * @notice Set the fee rebate percent. - * - * @param rebatePercent The fee rebate percent. - * */ - function setRebatePercent(uint256 rebatePercent) external onlyOwner whenNotPaused { - require(rebatePercent <= 10**20, "Fee rebate is too high"); - - uint256 oldRebatePercent = feeRebatePercent; - feeRebatePercent = rebatePercent; - - emit SetRebatePercent(msg.sender, oldRebatePercent, rebatePercent); - } - - /** - * @notice Set the special fee rebate percent for specific pair - * - * @param specialRebatesPercent The new special fee rebate percent. - * */ - function setSpecialRebates( - address sourceToken, - address destToken, - uint256 specialRebatesPercent - ) external onlyOwner whenNotPaused { - // Set max special rebates to 1000% - require(specialRebatesPercent <= 1000e18, "Special fee rebate is too high"); - - uint256 oldSpecialRebatesPercent = specialRebates[sourceToken][destToken]; - specialRebates[sourceToken][destToken] = specialRebatesPercent; - - emit SetSpecialRebates(msg.sender, sourceToken, destToken, oldSpecialRebatesPercent, specialRebatesPercent); - } - - /** - * @notice Get a rebate percent of specific pairs. - * - * @param sourceTokenAddress The source of pairs. - * @param destTokenAddress The dest of pairs. - * - * @return The percent rebates of the pairs. - * */ - function getSpecialRebates(address sourceTokenAddress, address destTokenAddress) external view returns (uint256 specialRebatesPercent) { - return specialRebates[sourceTokenAddress][destTokenAddress]; - } - - function getProtocolAddress() external view returns (address) { - return protocolAddress; - } - - function getSovTokenAddress() external view returns (address) { - return sovTokenAddress; - } - - function getLockedSOVAddress() external view returns (address) { - return lockedSOVAddress; - } - - function getFeeRebatePercent() external view returns (uint256) { - return feeRebatePercent; - } - - function togglePaused(bool paused) external onlyOwner { - require(paused != pause, "Can't toggle"); - pause = paused; - emit TogglePaused(msg.sender, !paused, paused); - } - - function isProtocolPaused() external view returns (bool) { - return pause; - } - - function getSwapExternalFeePercent() external view returns (uint256) { - return swapExtrernalFeePercent; - } - - /** - * @notice Get the basis point of trading rebate rewards. - * - * @return The basis point value. - */ - function getTradingRebateRewardsBasisPoint() external view returns (uint256) { - return tradingRebateRewardsBasisPoint; - } - - /** - * @dev Get how much SOV that is dedicated to pay the trading rebate rewards. - * @notice If SOV balance is less than the fees held, it will return 0. - * - * @return total dedicated SOV. - */ - function getDedicatedSOVRebate() public view returns (uint256) { - uint256 sovProtocolBalance = IERC20(sovTokenAddress).balanceOf(address(this)); - uint256 sovFees = - lendingFeeTokensHeld[sovTokenAddress].add(tradingFeeTokensHeld[sovTokenAddress]).add(borrowingFeeTokensHeld[sovTokenAddress]); - - return sovProtocolBalance >= sovFees ? sovProtocolBalance.sub(sovFees) : 0; - } - - /** - * @notice Set rolloverFlexFeePercent (max value is 1%) - * - * @param newRolloverFlexFeePercent uint256 value of new rollover flex fee percentage (0.1 ether = 0.1%) - */ - function setRolloverFlexFeePercent(uint256 newRolloverFlexFeePercent) external onlyOwner whenNotPaused { - require(newRolloverFlexFeePercent <= 1e18, "value too high"); - uint256 oldRolloverFlexFeePercent = rolloverFlexFeePercent; - rolloverFlexFeePercent = newRolloverFlexFeePercent; - - emit SetRolloverFlexFeePercent(msg.sender, oldRolloverFlexFeePercent, newRolloverFlexFeePercent); - } +contract ProtocolSettings is + State, + ProtocolTokenUser, + ProtocolSettingsEvents, + ModuleCommonFunctionalities +{ + using SafeERC20 for IERC20; + using SafeMath for uint256; + + /** + * @notice Empty public constructor. + * */ + constructor() public {} + + /** + * @notice Fallback function is to react to receiving value (rBTC). + * */ + function() external { + revert("fallback not allowed"); + } + + /** + * @notice Set function selectors on target contract. + * + * @param target The address of the target contract. + * */ + function initialize(address target) external onlyOwner { + address prevModuleContractAddress = logicTargets[this.setPriceFeedContract.selector]; + _setTarget(this.setPriceFeedContract.selector, target); + _setTarget(this.setSwapsImplContract.selector, target); + _setTarget(this.setLoanPool.selector, target); + _setTarget(this.setSupportedTokens.selector, target); + _setTarget(this.setLendingFeePercent.selector, target); + _setTarget(this.setTradingFeePercent.selector, target); + _setTarget(this.setBorrowingFeePercent.selector, target); + _setTarget(this.setSwapExternalFeePercent.selector, target); + _setTarget(this.setAffiliateFeePercent.selector, target); + _setTarget(this.setAffiliateTradingTokenFeePercent.selector, target); + _setTarget(this.setLiquidationIncentivePercent.selector, target); + _setTarget(this.setMaxDisagreement.selector, target); + _setTarget(this.setSourceBuffer.selector, target); + _setTarget(this.setMaxSwapSize.selector, target); + _setTarget(this.setFeesController.selector, target); + _setTarget(this.withdrawFees.selector, target); + _setTarget(this.withdrawLendingFees.selector, target); + _setTarget(this.withdrawTradingFees.selector, target); + _setTarget(this.withdrawBorrowingFees.selector, target); + _setTarget(this.withdrawProtocolToken.selector, target); + _setTarget(this.depositProtocolToken.selector, target); + _setTarget(this.getLoanPoolsList.selector, target); + _setTarget(this.isLoanPool.selector, target); + _setTarget(this.setSovrynSwapContractRegistryAddress.selector, target); + _setTarget(this.setWrbtcToken.selector, target); + _setTarget(this.setProtocolTokenAddress.selector, target); + _setTarget(this.setRolloverBaseReward.selector, target); + _setTarget(this.setRebatePercent.selector, target); + _setTarget(this.setSpecialRebates.selector, target); + _setTarget(this.setSovrynProtocolAddress.selector, target); + _setTarget(this.setSOVTokenAddress.selector, target); + _setTarget(this.setLockedSOVAddress.selector, target); + _setTarget(this.setMinReferralsToPayoutAffiliates.selector, target); + _setTarget(this.getSpecialRebates.selector, target); + _setTarget(this.getProtocolAddress.selector, target); + _setTarget(this.getSovTokenAddress.selector, target); + _setTarget(this.getLockedSOVAddress.selector, target); + _setTarget(this.getFeeRebatePercent.selector, target); + _setTarget(this.togglePaused.selector, target); + _setTarget(this.isProtocolPaused.selector, target); + _setTarget(this.getSwapExternalFeePercent.selector, target); + _setTarget(this.setTradingRebateRewardsBasisPoint.selector, target); + _setTarget(this.getTradingRebateRewardsBasisPoint.selector, target); + _setTarget(this.getDedicatedSOVRebate.selector, target); + _setTarget(this.setRolloverFlexFeePercent.selector, target); + emit ProtocolModuleContractReplaced(prevModuleContractAddress, target, "ProtocolSettings"); + } + + /** + * setting wrong address will break inter module functions calling + * should be set once + */ + function setSovrynProtocolAddress(address newProtocolAddress) + external + onlyOwner + whenNotPaused + { + address oldProtocolAddress = protocolAddress; + protocolAddress = newProtocolAddress; + + emit SetProtocolAddress(msg.sender, oldProtocolAddress, newProtocolAddress); + } + + function setSOVTokenAddress(address newSovTokenAddress) external onlyOwner whenNotPaused { + require(Address.isContract(newSovTokenAddress), "newSovTokenAddress not a contract"); + + address oldTokenAddress = sovTokenAddress; + sovTokenAddress = newSovTokenAddress; + + emit SetSOVTokenAddress(msg.sender, oldTokenAddress, newSovTokenAddress); + } + + function setLockedSOVAddress(address newLockedSOVAddress) external onlyOwner whenNotPaused { + require(Address.isContract(newLockedSOVAddress), "newLockSOVAddress not a contract"); + + address oldLockedSOVAddress = lockedSOVAddress; + lockedSOVAddress = newLockedSOVAddress; + + emit SetLockedSOVAddress(msg.sender, oldLockedSOVAddress, newLockedSOVAddress); + } + + /** + * @notice Set the basis point of trading rebate rewards (SOV), max value is 9999 (99.99% liquid, 0.01% vested). + * + * @param newBasisPoint Basis point value. + */ + function setTradingRebateRewardsBasisPoint(uint256 newBasisPoint) + external + onlyOwner + whenNotPaused + { + require(newBasisPoint <= 9999, "value too high"); + + uint256 oldBasisPoint = tradingRebateRewardsBasisPoint; + tradingRebateRewardsBasisPoint = newBasisPoint; + + emit SetTradingRebateRewardsBasisPoint(msg.sender, oldBasisPoint, newBasisPoint); + } + + /** + * @notice Update the minimum number of referrals to get affiliates rewards. + * + * @param newMinReferrals The new minimum number of referrals. + * */ + function setMinReferralsToPayoutAffiliates(uint256 newMinReferrals) + external + onlyOwner + whenNotPaused + { + uint256 oldMinReferrals = minReferralsToPayout; + minReferralsToPayout = newMinReferrals; + + emit SetMinReferralsToPayoutAffiliates(msg.sender, oldMinReferrals, newMinReferrals); + } + + /** + * @notice Set the address of the Price Feed instance. + * + * @param newContract The address of the Price Feed new instance. + * */ + function setPriceFeedContract(address newContract) external onlyOwner whenNotPaused { + address oldContract = priceFeeds; + priceFeeds = newContract; + + emit SetPriceFeedContract(msg.sender, oldContract, newContract); + } + + /** + * @notice Set the address of the asset swapper instance. + * + * @param newContract The address of the asset swapper new instance. + * */ + function setSwapsImplContract(address newContract) external onlyOwner whenNotPaused { + address oldContract = swapsImpl; + swapsImpl = newContract; + + emit SetSwapsImplContract(msg.sender, oldContract, newContract); + } + + /** + * @notice Set a list of loan pools and its tokens. + * + * @param pools The array of addresses of new loan pool instances. + * @param assets The array of addresses of the corresponding underlying tokens. + * */ + function setLoanPool(address[] calldata pools, address[] calldata assets) + external + onlyOwner + whenNotPaused + { + require(pools.length == assets.length, "count mismatch"); + + for (uint256 i = 0; i < pools.length; i++) { + require(pools[i] != assets[i], "pool == asset"); + require(pools[i] != address(0), "pool == 0"); + require( + assets[i] != address(0) || loanPoolToUnderlying[pools[i]] != address(0), + "pool not exists" + ); + if (assets[i] == address(0)) { + underlyingToLoanPool[loanPoolToUnderlying[pools[i]]] = address(0); + loanPoolToUnderlying[pools[i]] = address(0); + loanPoolsSet.removeAddress(pools[i]); + } else { + loanPoolToUnderlying[pools[i]] = assets[i]; + underlyingToLoanPool[assets[i]] = pools[i]; + loanPoolsSet.addAddress(pools[i]); + } + + emit SetLoanPool(msg.sender, pools[i], assets[i]); + } + } + + /** + * @notice Set a list of supported tokens by populating the + * storage supportedTokens mapping. + * + * @param addrs The array of addresses of the tokens. + * @param toggles The array of flags indicating whether + * the corresponding token is supported or not. + * */ + function setSupportedTokens(address[] calldata addrs, bool[] calldata toggles) + external + onlyOwner + whenNotPaused + { + require(addrs.length == toggles.length, "count mismatch"); + + for (uint256 i = 0; i < addrs.length; i++) { + supportedTokens[addrs[i]] = toggles[i]; + + emit SetSupportedTokens(msg.sender, addrs[i], toggles[i]); + } + } + + /** + * @notice Set the value of lendingFeePercent storage variable. + * + * @param newValue The new value for lendingFeePercent. + * */ + function setLendingFeePercent(uint256 newValue) external onlyOwner whenNotPaused { + require(newValue <= 10**20, "value too high"); + uint256 oldValue = lendingFeePercent; + lendingFeePercent = newValue; + + emit SetLendingFeePercent(msg.sender, oldValue, newValue); + } + + /** + * @notice Set the value of tradingFeePercent storage variable. + * + * @param newValue The new value for tradingFeePercent. + * */ + function setTradingFeePercent(uint256 newValue) external onlyOwner whenNotPaused { + require(newValue <= 10**20, "value too high"); + uint256 oldValue = tradingFeePercent; + tradingFeePercent = newValue; + + emit SetTradingFeePercent(msg.sender, oldValue, newValue); + } + + /** + * @notice Set the value of borrowingFeePercent storage variable. + * + * @param newValue The new value for borrowingFeePercent. + * */ + function setBorrowingFeePercent(uint256 newValue) external onlyOwner whenNotPaused { + require(newValue <= 10**20, "value too high"); + uint256 oldValue = borrowingFeePercent; + borrowingFeePercent = newValue; + + emit SetBorrowingFeePercent(msg.sender, oldValue, newValue); + } + + /** + * @notice Set the value of swapExtrernalFeePercent storage variable + * + * @param newValue the new value for swapExternalFeePercent + */ + function setSwapExternalFeePercent(uint256 newValue) external onlyOwner whenNotPaused { + require(newValue <= 10**20, "value too high"); + uint256 oldValue = swapExtrernalFeePercent; + swapExtrernalFeePercent = newValue; + + emit SetSwapExternalFeePercent(msg.sender, oldValue, newValue); + } + + /** + * @notice Set the value of affiliateFeePercent storage variable. + * + * @param newValue The new value for affiliateFeePercent. + * */ + function setAffiliateFeePercent(uint256 newValue) external onlyOwner whenNotPaused { + require(newValue <= 10**20, "value too high"); + uint256 oldValue = affiliateFeePercent; + affiliateFeePercent = newValue; + + emit SetAffiliateFeePercent(msg.sender, oldValue, newValue); + } + + /** + * @notice Set the value of affiliateTradingTokenFeePercent storage variable. + * + * @param newValue The new value for affiliateTradingTokenFeePercent. + * */ + function setAffiliateTradingTokenFeePercent(uint256 newValue) + external + onlyOwner + whenNotPaused + { + require(newValue <= 10**20, "value too high"); + uint256 oldValue = affiliateTradingTokenFeePercent; + affiliateTradingTokenFeePercent = newValue; + + emit SetAffiliateTradingTokenFeePercent(msg.sender, oldValue, newValue); + } + + /** + * @notice Set the value of liquidationIncentivePercent storage variable. + * + * @param newValue The new value for liquidationIncentivePercent. + * */ + function setLiquidationIncentivePercent(uint256 newValue) external onlyOwner whenNotPaused { + require(newValue <= 10**20, "value too high"); + uint256 oldValue = liquidationIncentivePercent; + liquidationIncentivePercent = newValue; + + emit SetLiquidationIncentivePercent(msg.sender, oldValue, newValue); + } + + /** + * @notice Set the value of the maximum swap spread. + * + * @param newValue The new value for maxDisagreement. + * */ + function setMaxDisagreement(uint256 newValue) external onlyOwner whenNotPaused { + maxDisagreement = newValue; + } + + /** + * @notice Set the value of the maximum source buffer. + * + * @dev To avoid rounding issues on the swap rate a small buffer is implemented. + * + * @param newValue The new value for the maximum source buffer. + * */ + function setSourceBuffer(uint256 newValue) external onlyOwner whenNotPaused { + sourceBuffer = newValue; + } + + /** + * @notice Set the value of the swap size limit. + * + * @param newValue The new value for the maximum swap size. + * */ + function setMaxSwapSize(uint256 newValue) external onlyOwner whenNotPaused { + uint256 oldValue = maxSwapSize; + maxSwapSize = newValue; + + emit SetMaxSwapSize(msg.sender, oldValue, newValue); + } + + /** + * @notice Set the address of the feesController instance. + * + * @dev The fee sharing proxy must be the feesController of the + * protocol contract. This allows the fee sharing proxy + * to withdraw the fees. + * + * @param newController The new address of the feesController. + * */ + function setFeesController(address newController) external onlyOwner whenNotPaused { + address oldController = feesController; + feesController = newController; + + emit SetFeesController(msg.sender, oldController, newController); + } + + /** + * @notice The feesController calls this function to withdraw fees + * from three sources: lending, trading and borrowing. + * The fees (except SOV) will be converted to wRBTC. + * For SOV, it will be deposited directly to feeSharingProxy from the protocol. + * + * @param tokens The array of address of the token instance. + * @param receiver The address of the withdrawal recipient. + * + * @return The withdrawn total amount in wRBTC + * */ + function withdrawFees(address[] calldata tokens, address receiver) + external + whenNotPaused + returns (uint256 totalWRBTCWithdrawn) + { + require(msg.sender == feesController, "unauthorized"); + + for (uint256 i = 0; i < tokens.length; i++) { + uint256 lendingBalance = lendingFeeTokensHeld[tokens[i]]; + if (lendingBalance > 0) { + lendingFeeTokensHeld[tokens[i]] = 0; + lendingFeeTokensPaid[tokens[i]] = lendingFeeTokensPaid[tokens[i]].add( + lendingBalance + ); + } + + uint256 tradingBalance = tradingFeeTokensHeld[tokens[i]]; + if (tradingBalance > 0) { + tradingFeeTokensHeld[tokens[i]] = 0; + tradingFeeTokensPaid[tokens[i]] = tradingFeeTokensPaid[tokens[i]].add( + tradingBalance + ); + } + + uint256 borrowingBalance = borrowingFeeTokensHeld[tokens[i]]; + if (borrowingBalance > 0) { + borrowingFeeTokensHeld[tokens[i]] = 0; + borrowingFeeTokensPaid[tokens[i]] = borrowingFeeTokensPaid[tokens[i]].add( + borrowingBalance + ); + } + + uint256 tempAmount = lendingBalance.add(tradingBalance).add(borrowingBalance); + + if (tempAmount == 0) { + continue; + } + + uint256 amountConvertedToWRBTC; + if (tokens[i] == address(sovTokenAddress)) { + IERC20(tokens[i]).approve(feesController, tempAmount); + IFeeSharingProxy(feesController).transferTokens( + address(sovTokenAddress), + uint96(tempAmount) + ); + amountConvertedToWRBTC = 0; + } else { + if (tokens[i] == address(wrbtcToken)) { + amountConvertedToWRBTC = tempAmount; + + IERC20(address(wrbtcToken)).safeTransfer(receiver, amountConvertedToWRBTC); + } else { + IERC20(tokens[i]).approve(protocolAddress, tempAmount); + + (amountConvertedToWRBTC, ) = ProtocolSwapExternalInterface(protocolAddress) + .swapExternal( + tokens[i], // source token address + address(wrbtcToken), // dest token address + feesController, // set feeSharingProxy as receiver + protocolAddress, // protocol as the sender + tempAmount, // source token amount + 0, // reqDestToken + 0, // minReturn + "" // loan data bytes + ); + + /// Will revert if disagreement found. + IPriceFeeds(priceFeeds).checkPriceDisagreement( + tokens[i], + address(wrbtcToken), + tempAmount, + amountConvertedToWRBTC, + maxDisagreement + ); + } + + totalWRBTCWithdrawn = totalWRBTCWithdrawn.add(amountConvertedToWRBTC); + } + + emit WithdrawFees( + msg.sender, + tokens[i], + receiver, + lendingBalance, + tradingBalance, + borrowingBalance, + amountConvertedToWRBTC + ); + } + + return totalWRBTCWithdrawn; + } + + /** + * @notice The feesController calls this function to withdraw fees + * accrued from lending operations. + * + * @param token The address of the token instance. + * @param receiver The address of the withdrawal recipient. + * @param amount The amount of fees to get, ignored if greater than balance. + * + * @return Whether withdrawal was successful. + * */ + function withdrawLendingFees( + address token, + address receiver, + uint256 amount + ) external whenNotPaused returns (bool) { + require(msg.sender == feesController, "unauthorized"); + + uint256 withdrawAmount = amount; + + uint256 balance = lendingFeeTokensHeld[token]; + if (withdrawAmount > balance) { + withdrawAmount = balance; + } + if (withdrawAmount == 0) { + return false; + } + + lendingFeeTokensHeld[token] = balance.sub(withdrawAmount); + lendingFeeTokensPaid[token] = lendingFeeTokensPaid[token].add(withdrawAmount); + + IERC20(token).safeTransfer(receiver, withdrawAmount); + + emit WithdrawLendingFees(msg.sender, token, receiver, withdrawAmount); + + return true; + } + + /** + * @notice The feesController calls this function to withdraw fees + * accrued from trading operations. + * + * @param token The address of the token instance. + * @param receiver The address of the withdrawal recipient. + * @param amount The amount of fees to get, ignored if greater than balance. + * + * @return Whether withdrawal was successful. + * */ + function withdrawTradingFees( + address token, + address receiver, + uint256 amount + ) external whenNotPaused returns (bool) { + require(msg.sender == feesController, "unauthorized"); + + uint256 withdrawAmount = amount; + + uint256 balance = tradingFeeTokensHeld[token]; + if (withdrawAmount > balance) { + withdrawAmount = balance; + } + if (withdrawAmount == 0) { + return false; + } + + tradingFeeTokensHeld[token] = balance.sub(withdrawAmount); + tradingFeeTokensPaid[token] = tradingFeeTokensPaid[token].add(withdrawAmount); + + IERC20(token).safeTransfer(receiver, withdrawAmount); + + emit WithdrawTradingFees(msg.sender, token, receiver, withdrawAmount); + + return true; + } + + /** + * @notice The feesController calls this function to withdraw fees + * accrued from borrowing operations. + * + * @param token The address of the token instance. + * @param receiver The address of the withdrawal recipient. + * @param amount The amount of fees to get, ignored if greater than balance. + * + * @return Whether withdrawal was successful. + * */ + function withdrawBorrowingFees( + address token, + address receiver, + uint256 amount + ) external whenNotPaused returns (bool) { + require(msg.sender == feesController, "unauthorized"); + + uint256 withdrawAmount = amount; + + uint256 balance = borrowingFeeTokensHeld[token]; + if (withdrawAmount > balance) { + withdrawAmount = balance; + } + if (withdrawAmount == 0) { + return false; + } + + borrowingFeeTokensHeld[token] = balance.sub(withdrawAmount); + borrowingFeeTokensPaid[token] = borrowingFeeTokensPaid[token].add(withdrawAmount); + + IERC20(token).safeTransfer(receiver, withdrawAmount); + + emit WithdrawBorrowingFees(msg.sender, token, receiver, withdrawAmount); + + return true; + } + + /** + * @notice The owner calls this function to withdraw protocol tokens. + * + * @dev Wrapper for ProtocolTokenUser::_withdrawProtocolToken internal function. + * + * @param receiver The address of the withdrawal recipient. + * @param amount The amount of tokens to get. + * + * @return The protocol token address. + * @return Withdrawal success (true/false). + * */ + function withdrawProtocolToken(address receiver, uint256 amount) + external + onlyOwner + whenNotPaused + returns (address, bool) + { + return _withdrawProtocolToken(receiver, amount); + } + + /** + * @notice The owner calls this function to deposit protocol tokens. + * + * @param amount The tokens of fees to send. + * */ + function depositProtocolToken(uint256 amount) external onlyOwner whenNotPaused { + /// @dev Update local balance + protocolTokenHeld = protocolTokenHeld.add(amount); + + /// @dev Send the tokens + IERC20(protocolTokenAddress).safeTransferFrom(msg.sender, address(this), amount); + } + + /** + * @notice Get a list of loan pools. + * + * @param start The offset. + * @param count The limit. + * + * @return The array of loan pools. + * */ + function getLoanPoolsList(uint256 start, uint256 count) + external + view + returns (bytes32[] memory) + { + return loanPoolsSet.enumerate(start, count); + } + + /** + * @notice Check whether a token is a pool token. + * + * @dev By querying its underlying token. + * + * @param loanPool The token address to check. + * */ + function isLoanPool(address loanPool) external view returns (bool) { + return loanPoolToUnderlying[loanPool] != address(0); + } + + /** + * @notice Set the contract registry address of the SovrynSwap network. + * + * @param registryAddress the address of the registry contract. + * */ + function setSovrynSwapContractRegistryAddress(address registryAddress) + external + onlyOwner + whenNotPaused + { + require(Address.isContract(registryAddress), "registryAddress not a contract"); + + address oldSovrynSwapContractRegistryAddress = sovrynSwapContractRegistryAddress; + sovrynSwapContractRegistryAddress = registryAddress; + + emit SetSovrynSwapContractRegistryAddress( + msg.sender, + oldSovrynSwapContractRegistryAddress, + sovrynSwapContractRegistryAddress + ); + } + + /** + * @notice Set the wrBTC contract address. + * + * @param wrbtcTokenAddress The address of the wrBTC contract. + * */ + function setWrbtcToken(address wrbtcTokenAddress) external onlyOwner whenNotPaused { + require(Address.isContract(wrbtcTokenAddress), "wrbtcTokenAddress not a contract"); + + address oldwrbtcToken = address(wrbtcToken); + wrbtcToken = IWrbtcERC20(wrbtcTokenAddress); + + emit SetWrbtcToken(msg.sender, oldwrbtcToken, wrbtcTokenAddress); + } + + /** + * @notice Set the protocol token contract address. + * + * @param _protocolTokenAddress The address of the protocol token contract. + * */ + function setProtocolTokenAddress(address _protocolTokenAddress) + external + onlyOwner + whenNotPaused + { + require(Address.isContract(_protocolTokenAddress), "_protocolTokenAddress not a contract"); + + address oldProtocolTokenAddress = protocolTokenAddress; + protocolTokenAddress = _protocolTokenAddress; + + emit SetProtocolTokenAddress(msg.sender, oldProtocolTokenAddress, _protocolTokenAddress); + } + + /** + * @notice Set rollover base reward. It should be denominated in wrBTC. + * + * @param baseRewardValue The base reward. + * */ + function setRolloverBaseReward(uint256 baseRewardValue) external onlyOwner whenNotPaused { + require(baseRewardValue > 0, "Base reward is zero"); + + uint256 oldValue = rolloverBaseReward; + rolloverBaseReward = baseRewardValue; + + emit SetRolloverBaseReward(msg.sender, oldValue, rolloverBaseReward); + } + + /** + * @notice Set the fee rebate percent. + * + * @param rebatePercent The fee rebate percent. + * */ + function setRebatePercent(uint256 rebatePercent) external onlyOwner whenNotPaused { + require(rebatePercent <= 10**20, "Fee rebate is too high"); + + uint256 oldRebatePercent = feeRebatePercent; + feeRebatePercent = rebatePercent; + + emit SetRebatePercent(msg.sender, oldRebatePercent, rebatePercent); + } + + /** + * @notice Set the special fee rebate percent for specific pair + * + * @param specialRebatesPercent The new special fee rebate percent. + * */ + function setSpecialRebates( + address sourceToken, + address destToken, + uint256 specialRebatesPercent + ) external onlyOwner whenNotPaused { + // Set max special rebates to 1000% + require(specialRebatesPercent <= 1000e18, "Special fee rebate is too high"); + + uint256 oldSpecialRebatesPercent = specialRebates[sourceToken][destToken]; + specialRebates[sourceToken][destToken] = specialRebatesPercent; + + emit SetSpecialRebates( + msg.sender, + sourceToken, + destToken, + oldSpecialRebatesPercent, + specialRebatesPercent + ); + } + + /** + * @notice Get a rebate percent of specific pairs. + * + * @param sourceTokenAddress The source of pairs. + * @param destTokenAddress The dest of pairs. + * + * @return The percent rebates of the pairs. + * */ + function getSpecialRebates(address sourceTokenAddress, address destTokenAddress) + external + view + returns (uint256 specialRebatesPercent) + { + return specialRebates[sourceTokenAddress][destTokenAddress]; + } + + function getProtocolAddress() external view returns (address) { + return protocolAddress; + } + + function getSovTokenAddress() external view returns (address) { + return sovTokenAddress; + } + + function getLockedSOVAddress() external view returns (address) { + return lockedSOVAddress; + } + + function getFeeRebatePercent() external view returns (uint256) { + return feeRebatePercent; + } + + function togglePaused(bool paused) external onlyOwner { + require(paused != pause, "Can't toggle"); + pause = paused; + emit TogglePaused(msg.sender, !paused, paused); + } + + function isProtocolPaused() external view returns (bool) { + return pause; + } + + function getSwapExternalFeePercent() external view returns (uint256) { + return swapExtrernalFeePercent; + } + + /** + * @notice Get the basis point of trading rebate rewards. + * + * @return The basis point value. + */ + function getTradingRebateRewardsBasisPoint() external view returns (uint256) { + return tradingRebateRewardsBasisPoint; + } + + /** + * @dev Get how much SOV that is dedicated to pay the trading rebate rewards. + * @notice If SOV balance is less than the fees held, it will return 0. + * + * @return total dedicated SOV. + */ + function getDedicatedSOVRebate() public view returns (uint256) { + uint256 sovProtocolBalance = IERC20(sovTokenAddress).balanceOf(address(this)); + uint256 sovFees = + lendingFeeTokensHeld[sovTokenAddress].add(tradingFeeTokensHeld[sovTokenAddress]).add( + borrowingFeeTokensHeld[sovTokenAddress] + ); + + return sovProtocolBalance >= sovFees ? sovProtocolBalance.sub(sovFees) : 0; + } + + /** + * @notice Set rolloverFlexFeePercent (max value is 1%) + * + * @param newRolloverFlexFeePercent uint256 value of new rollover flex fee percentage (0.1 ether = 0.1%) + */ + function setRolloverFlexFeePercent(uint256 newRolloverFlexFeePercent) + external + onlyOwner + whenNotPaused + { + require(newRolloverFlexFeePercent <= 1e18, "value too high"); + uint256 oldRolloverFlexFeePercent = rolloverFlexFeePercent; + rolloverFlexFeePercent = newRolloverFlexFeePercent; + + emit SetRolloverFlexFeePercent( + msg.sender, + oldRolloverFlexFeePercent, + newRolloverFlexFeePercent + ); + } } diff --git a/contracts/modules/SwapsExternal.sol b/contracts/modules/SwapsExternal.sol index bf605fc1f..6b3650a13 100644 --- a/contracts/modules/SwapsExternal.sol +++ b/contracts/modules/SwapsExternal.sol @@ -21,142 +21,148 @@ import "../mixins/ModuleCommonFunctionalities.sol"; * This contract contains functions to calculate and execute swaps. * */ contract SwapsExternal is VaultController, SwapsUser, ModuleCommonFunctionalities { - /** - * @notice Empty public constructor. - * */ - constructor() public {} + /** + * @notice Empty public constructor. + * */ + constructor() public {} - /** - * @notice Fallback function is to react to receiving value (rBTC). - * */ - function() external { - revert("fallback not allowed"); - } + /** + * @notice Fallback function is to react to receiving value (rBTC). + * */ + function() external { + revert("fallback not allowed"); + } - /** - * @notice Set function selectors on target contract. - * - * @param target The address of the target contract. - * */ - function initialize(address target) external onlyOwner { - address prevModuleContractAddress = logicTargets[this.swapExternal.selector]; - _setTarget(this.swapExternal.selector, target); - _setTarget(this.getSwapExpectedReturn.selector, target); - _setTarget(this.checkPriceDivergence.selector, target); - emit ProtocolModuleContractReplaced(prevModuleContractAddress, target, "SwapsExternal"); - } + /** + * @notice Set function selectors on target contract. + * + * @param target The address of the target contract. + * */ + function initialize(address target) external onlyOwner { + address prevModuleContractAddress = logicTargets[this.swapExternal.selector]; + _setTarget(this.swapExternal.selector, target); + _setTarget(this.getSwapExpectedReturn.selector, target); + _setTarget(this.checkPriceDivergence.selector, target); + emit ProtocolModuleContractReplaced(prevModuleContractAddress, target, "SwapsExternal"); + } - /** - * @notice Perform a swap w/ tokens or rBTC as source currency. - * - * @dev External wrapper that calls SwapsUser::_swapsCall - * after turning potential incoming rBTC into wrBTC tokens. - * - * @param sourceToken The address of the source token instance. - * @param destToken The address of the destiny token instance. - * @param receiver The address of the recipient account. - * @param returnToSender The address of the sender account. - * @param sourceTokenAmount The amount of source tokens. - * @param requiredDestTokenAmount The amount of required destiny tokens. - * @param minReturn Minimum amount (position size) in the collateral tokens. - * @param swapData Additional swap data (not in use yet). - * - * @return destTokenAmountReceived The amount of destiny tokens sent. - * @return sourceTokenAmountUsed The amount of source tokens spent. - * */ - function swapExternal( - address sourceToken, - address destToken, - address receiver, - address returnToSender, - uint256 sourceTokenAmount, - uint256 requiredDestTokenAmount, - uint256 minReturn, - bytes memory swapData - ) public payable nonReentrant whenNotPaused returns (uint256 destTokenAmountReceived, uint256 sourceTokenAmountUsed) { - require(sourceTokenAmount != 0, "sourceTokenAmount == 0"); - checkPriceDivergence(sourceToken, destToken, sourceTokenAmount, minReturn); + /** + * @notice Perform a swap w/ tokens or rBTC as source currency. + * + * @dev External wrapper that calls SwapsUser::_swapsCall + * after turning potential incoming rBTC into wrBTC tokens. + * + * @param sourceToken The address of the source token instance. + * @param destToken The address of the destiny token instance. + * @param receiver The address of the recipient account. + * @param returnToSender The address of the sender account. + * @param sourceTokenAmount The amount of source tokens. + * @param requiredDestTokenAmount The amount of required destiny tokens. + * @param minReturn Minimum amount (position size) in the collateral tokens. + * @param swapData Additional swap data (not in use yet). + * + * @return destTokenAmountReceived The amount of destiny tokens sent. + * @return sourceTokenAmountUsed The amount of source tokens spent. + * */ + function swapExternal( + address sourceToken, + address destToken, + address receiver, + address returnToSender, + uint256 sourceTokenAmount, + uint256 requiredDestTokenAmount, + uint256 minReturn, + bytes memory swapData + ) + public + payable + nonReentrant + whenNotPaused + returns (uint256 destTokenAmountReceived, uint256 sourceTokenAmountUsed) + { + require(sourceTokenAmount != 0, "sourceTokenAmount == 0"); + checkPriceDivergence(sourceToken, destToken, sourceTokenAmount, minReturn); - /// @dev Get payed value, be it rBTC or tokenized. - if (msg.value != 0) { - if (sourceToken == address(0)) { - sourceToken = address(wrbtcToken); - } - require(sourceToken == address(wrbtcToken), "sourceToken mismatch"); - require(msg.value == sourceTokenAmount, "sourceTokenAmount mismatch"); + /// @dev Get payed value, be it rBTC or tokenized. + if (msg.value != 0) { + if (sourceToken == address(0)) { + sourceToken = address(wrbtcToken); + } + require(sourceToken == address(wrbtcToken), "sourceToken mismatch"); + require(msg.value == sourceTokenAmount, "sourceTokenAmount mismatch"); - /// @dev Update wrBTC balance for this contract. - wrbtcToken.deposit.value(sourceTokenAmount)(); - } else { - if (address(this) != msg.sender) { - IERC20(sourceToken).safeTransferFrom(msg.sender, address(this), sourceTokenAmount); - } - } + /// @dev Update wrBTC balance for this contract. + wrbtcToken.deposit.value(sourceTokenAmount)(); + } else { + if (address(this) != msg.sender) { + IERC20(sourceToken).safeTransferFrom(msg.sender, address(this), sourceTokenAmount); + } + } - /// @dev Perform the swap w/ tokens. - (destTokenAmountReceived, sourceTokenAmountUsed) = _swapsCall( - [ - sourceToken, - destToken, - receiver, - returnToSender, - msg.sender /// user - ], - [ - sourceTokenAmount, /// minSourceTokenAmount - sourceTokenAmount, /// maxSourceTokenAmount - requiredDestTokenAmount - ], - 0, /// loanId (not tied to a specific loan) - false, /// bypassFee - swapData, - true // the flag for swapExternal (so that it will use the swapExternalFeePercent) - ); + /// @dev Perform the swap w/ tokens. + (destTokenAmountReceived, sourceTokenAmountUsed) = _swapsCall( + [ + sourceToken, + destToken, + receiver, + returnToSender, + msg.sender /// user + ], + [ + sourceTokenAmount, /// minSourceTokenAmount + sourceTokenAmount, /// maxSourceTokenAmount + requiredDestTokenAmount + ], + 0, /// loanId (not tied to a specific loan) + false, /// bypassFee + swapData, + true // the flag for swapExternal (so that it will use the swapExternalFeePercent) + ); - emit ExternalSwap( - msg.sender, /// user - sourceToken, - destToken, - sourceTokenAmountUsed, - destTokenAmountReceived - ); - } + emit ExternalSwap( + msg.sender, /// user + sourceToken, + destToken, + sourceTokenAmountUsed, + destTokenAmountReceived + ); + } - /** - * @notice Get the swap expected return value. - * - * @dev External wrapper that calls SwapsUser::_swapsExpectedReturn - * - * @param sourceToken The address of the source token instance. - * @param destToken The address of the destiny token instance. - * @param sourceTokenAmount The amount of source tokens. - * - * @return The expected return value. - * */ - function getSwapExpectedReturn( - address sourceToken, - address destToken, - uint256 sourceTokenAmount - ) external view returns (uint256) { - return _swapsExpectedReturn(sourceToken, destToken, sourceTokenAmount); - } + /** + * @notice Get the swap expected return value. + * + * @dev External wrapper that calls SwapsUser::_swapsExpectedReturn + * + * @param sourceToken The address of the source token instance. + * @param destToken The address of the destiny token instance. + * @param sourceTokenAmount The amount of source tokens. + * + * @return The expected return value. + * */ + function getSwapExpectedReturn( + address sourceToken, + address destToken, + uint256 sourceTokenAmount + ) external view returns (uint256) { + return _swapsExpectedReturn(sourceToken, destToken, sourceTokenAmount); + } - /** - * @notice Check the slippage based on the swapExpectedReturn. - * - * @param sourceToken The address of the source token instance. - * @param destToken The address of the destiny token instance. - * @param sourceTokenAmount The amount of source tokens. - * @param minReturn The amount (max slippage) that will be compared to the swapsExpectedReturn. - * - */ - function checkPriceDivergence( - address sourceToken, - address destToken, - uint256 sourceTokenAmount, - uint256 minReturn - ) public view { - uint256 destTokenAmount = _swapsExpectedReturn(sourceToken, destToken, sourceTokenAmount); - require(destTokenAmount >= minReturn, "destTokenAmountReceived too low"); - } + /** + * @notice Check the slippage based on the swapExpectedReturn. + * + * @param sourceToken The address of the source token instance. + * @param destToken The address of the destiny token instance. + * @param sourceTokenAmount The amount of source tokens. + * @param minReturn The amount (max slippage) that will be compared to the swapsExpectedReturn. + * + */ + function checkPriceDivergence( + address sourceToken, + address destToken, + uint256 sourceTokenAmount, + uint256 minReturn + ) public view { + uint256 destTokenAmount = _swapsExpectedReturn(sourceToken, destToken, sourceTokenAmount); + require(destTokenAmount >= minReturn, "destTokenAmountReceived too low"); + } } diff --git a/contracts/modules/interfaces/ProtocolAffiliatesInterface.sol b/contracts/modules/interfaces/ProtocolAffiliatesInterface.sol index baa5e200a..d505e10a2 100644 --- a/contracts/modules/interfaces/ProtocolAffiliatesInterface.sol +++ b/contracts/modules/interfaces/ProtocolAffiliatesInterface.sol @@ -6,16 +6,16 @@ pragma solidity 0.5.17; interface ProtocolAffiliatesInterface { - function setAffiliatesReferrer(address user, address referrer) external; + function setAffiliatesReferrer(address user, address referrer) external; - function setUserNotFirstTradeFlag(address user_) external; + function setUserNotFirstTradeFlag(address user_) external; - function getUserNotFirstTradeFlag(address user_) external returns (bool); + function getUserNotFirstTradeFlag(address user_) external returns (bool); - function payTradingFeeToAffiliatesReferrer( - address affiliate, - address trader, - address token, - uint256 amount - ) external returns (uint256 affiliatesBonusSOVAmount, uint256 affiliatesBonusTokenAmount); + function payTradingFeeToAffiliatesReferrer( + address affiliate, + address trader, + address token, + uint256 amount + ) external returns (uint256 affiliatesBonusSOVAmount, uint256 affiliatesBonusTokenAmount); } diff --git a/contracts/modules/interfaces/ProtocolSwapExternalInterface.sol b/contracts/modules/interfaces/ProtocolSwapExternalInterface.sol index 5e3cacebd..ed7a86e9a 100644 --- a/contracts/modules/interfaces/ProtocolSwapExternalInterface.sol +++ b/contracts/modules/interfaces/ProtocolSwapExternalInterface.sol @@ -6,14 +6,14 @@ pragma solidity 0.5.17; interface ProtocolSwapExternalInterface { - function swapExternal( - address sourceToken, - address destToken, - address receiver, - address returnToSender, - uint256 sourceTokenAmount, - uint256 requiredDestTokenAmount, - uint256 minReturn, - bytes calldata swapData - ) external returns (uint256 destTokenAmountReceived, uint256 sourceTokenAmountUsed); + function swapExternal( + address sourceToken, + address destToken, + address receiver, + address returnToSender, + uint256 sourceTokenAmount, + uint256 requiredDestTokenAmount, + uint256 minReturn, + bytes calldata swapData + ) external returns (uint256 destTokenAmountReceived, uint256 sourceTokenAmountUsed); } diff --git a/contracts/multisig/MultiSigKeyHolders.sol b/contracts/multisig/MultiSigKeyHolders.sol index 4d5c18216..08d8a33bd 100644 --- a/contracts/multisig/MultiSigKeyHolders.sol +++ b/contracts/multisig/MultiSigKeyHolders.sol @@ -10,270 +10,296 @@ import "../openzeppelin/Ownable.sol"; * key holders w/ rBTC and BTC addresses. * */ contract MultiSigKeyHolders is Ownable { - /* Storage */ - - uint256 public constant MAX_OWNER_COUNT = 50; - - string private constant ERROR_INVALID_ADDRESS = "Invalid address"; - string private constant ERROR_INVALID_REQUIRED = "Invalid required"; - - /// Flag and index for Ethereum address. - mapping(address => Data) private isEthereumAddressAdded; - - /// List of Ethereum addresses. - address[] private ethereumAddresses; - - /// Required number of signatures for the Ethereum multisig. - uint256 public ethereumRequired = 2; - - /// Flag and index for Bitcoin address. - mapping(string => Data) private isBitcoinAddressAdded; - - /// List of Bitcoin addresses. - string[] private bitcoinAddresses; - - /// Required number of signatures for the Bitcoin multisig. - uint256 public bitcoinRequired = 2; - - /// Helps removing items from array. - struct Data { - bool added; - uint248 index; - } - - /* Events */ - - event EthereumAddressAdded(address indexed account); - event EthereumAddressRemoved(address indexed account); - event EthereumRequirementChanged(uint256 required); - event BitcoinAddressAdded(string account); - event BitcoinAddressRemoved(string account); - event BitcoinRequirementChanged(uint256 required); - - /* Modifiers */ - - modifier validRequirement(uint256 ownerCount, uint256 _required) { - require(ownerCount <= MAX_OWNER_COUNT && _required <= ownerCount && _required != 0 && ownerCount != 0, ERROR_INVALID_REQUIRED); - _; - } - - /* Functions */ - - /** - * @notice Add rBTC address to the key holders. - * @param _address The address to be added. - * */ - function addEthereumAddress(address _address) public onlyOwner { - _addEthereumAddress(_address); - } - - /** - * @notice Add rBTC addresses to the key holders. - * @param _address The addresses to be added. - * */ - function addEthereumAddresses(address[] memory _address) public onlyOwner { - for (uint256 i = 0; i < _address.length; i++) { - _addEthereumAddress(_address[i]); - } - } - - /** - * @notice Internal function to add rBTC address to the key holders. - * @param _address The address to be added. - * */ - function _addEthereumAddress(address _address) internal { - require(_address != address(0), ERROR_INVALID_ADDRESS); - - if (!isEthereumAddressAdded[_address].added) { - isEthereumAddressAdded[_address] = Data({ added: true, index: uint248(ethereumAddresses.length) }); - ethereumAddresses.push(_address); - } - - emit EthereumAddressAdded(_address); - } - - /** - * @notice Remove rBTC address to the key holders. - * @param _address The address to be removed. - * */ - function removeEthereumAddress(address _address) public onlyOwner { - _removeEthereumAddress(_address); - } - - /** - * @notice Remove rBTC addresses to the key holders. - * @param _address The addresses to be removed. - * */ - function removeEthereumAddresses(address[] memory _address) public onlyOwner { - for (uint256 i = 0; i < _address.length; i++) { - _removeEthereumAddress(_address[i]); - } - } - - /** - * @notice Internal function to remove rBTC address to the key holders. - * @param _address The address to be removed. - * */ - function _removeEthereumAddress(address _address) internal { - require(_address != address(0), ERROR_INVALID_ADDRESS); - - if (isEthereumAddressAdded[_address].added) { - uint248 index = isEthereumAddressAdded[_address].index; - if (index != ethereumAddresses.length - 1) { - ethereumAddresses[index] = ethereumAddresses[ethereumAddresses.length - 1]; - isEthereumAddressAdded[ethereumAddresses[index]].index = index; - } - ethereumAddresses.length--; - delete isEthereumAddressAdded[_address]; - } - - emit EthereumAddressRemoved(_address); - } - - /** - * @notice Get whether rBTC address is a key holder. - * @param _address The rBTC address to be checked. - * */ - function isEthereumAddressOwner(address _address) public view returns (bool) { - return isEthereumAddressAdded[_address].added; - } - - /** - * @notice Get array of rBTC key holders. - * */ - function getEthereumAddresses() public view returns (address[] memory) { - return ethereumAddresses; - } - - /** - * @notice Set flag ethereumRequired to true/false. - * @param _required The new value of the ethereumRequired flag. - * */ - function changeEthereumRequirement(uint256 _required) public onlyOwner validRequirement(ethereumAddresses.length, _required) { - ethereumRequired = _required; - emit EthereumRequirementChanged(_required); - } - - /** - * @notice Add bitcoin address to the key holders. - * @param _address The address to be added. - * */ - function addBitcoinAddress(string memory _address) public onlyOwner { - _addBitcoinAddress(_address); - } - - /** - * @notice Add bitcoin addresses to the key holders. - * @param _address The addresses to be added. - * */ - function addBitcoinAddresses(string[] memory _address) public onlyOwner { - for (uint256 i = 0; i < _address.length; i++) { - _addBitcoinAddress(_address[i]); - } - } - - /** - * @notice Internal function to add bitcoin address to the key holders. - * @param _address The address to be added. - * */ - function _addBitcoinAddress(string memory _address) internal { - require(bytes(_address).length != 0, ERROR_INVALID_ADDRESS); - - if (!isBitcoinAddressAdded[_address].added) { - isBitcoinAddressAdded[_address] = Data({ added: true, index: uint248(bitcoinAddresses.length) }); - bitcoinAddresses.push(_address); - } - - emit BitcoinAddressAdded(_address); - } - - /** - * @notice Remove bitcoin address to the key holders. - * @param _address The address to be removed. - * */ - function removeBitcoinAddress(string memory _address) public onlyOwner { - _removeBitcoinAddress(_address); - } - - /** - * @notice Remove bitcoin addresses to the key holders. - * @param _address The addresses to be removed. - * */ - function removeBitcoinAddresses(string[] memory _address) public onlyOwner { - for (uint256 i = 0; i < _address.length; i++) { - _removeBitcoinAddress(_address[i]); - } - } - - /** - * @notice Internal function to remove bitcoin address to the key holders. - * @param _address The address to be removed. - * */ - function _removeBitcoinAddress(string memory _address) internal { - require(bytes(_address).length != 0, ERROR_INVALID_ADDRESS); - - if (isBitcoinAddressAdded[_address].added) { - uint248 index = isBitcoinAddressAdded[_address].index; - if (index != bitcoinAddresses.length - 1) { - bitcoinAddresses[index] = bitcoinAddresses[bitcoinAddresses.length - 1]; - isBitcoinAddressAdded[bitcoinAddresses[index]].index = index; - } - bitcoinAddresses.length--; - delete isBitcoinAddressAdded[_address]; - } - - emit BitcoinAddressRemoved(_address); - } - - /** - * @notice Get whether bitcoin address is a key holder. - * @param _address The bitcoin address to be checked. - * */ - function isBitcoinAddressOwner(string memory _address) public view returns (bool) { - return isBitcoinAddressAdded[_address].added; - } - - /** - * @notice Get array of bitcoin key holders. - * */ - function getBitcoinAddresses() public view returns (string[] memory) { - return bitcoinAddresses; - } - - /** - * @notice Set flag bitcoinRequired to true/false. - * @param _required The new value of the bitcoinRequired flag. - * */ - function changeBitcoinRequirement(uint256 _required) public onlyOwner validRequirement(bitcoinAddresses.length, _required) { - bitcoinRequired = _required; - emit BitcoinRequirementChanged(_required); - } - - /** - * @notice Add rBTC and bitcoin addresses to the key holders. - * @param _ethereumAddress the rBTC addresses to be added. - * @param _bitcoinAddress the bitcoin addresses to be added. - * */ - function addEthereumAndBitcoinAddresses(address[] memory _ethereumAddress, string[] memory _bitcoinAddress) public onlyOwner { - for (uint256 i = 0; i < _ethereumAddress.length; i++) { - _addEthereumAddress(_ethereumAddress[i]); - } - for (uint256 i = 0; i < _bitcoinAddress.length; i++) { - _addBitcoinAddress(_bitcoinAddress[i]); - } - } - - /** - * @notice Remove rBTC and bitcoin addresses to the key holders. - * @param _ethereumAddress The rBTC addresses to be removed. - * @param _bitcoinAddress The bitcoin addresses to be removed. - * */ - function removeEthereumAndBitcoinAddresses(address[] memory _ethereumAddress, string[] memory _bitcoinAddress) public onlyOwner { - for (uint256 i = 0; i < _ethereumAddress.length; i++) { - _removeEthereumAddress(_ethereumAddress[i]); - } - for (uint256 i = 0; i < _bitcoinAddress.length; i++) { - _removeBitcoinAddress(_bitcoinAddress[i]); - } - } + /* Storage */ + + uint256 public constant MAX_OWNER_COUNT = 50; + + string private constant ERROR_INVALID_ADDRESS = "Invalid address"; + string private constant ERROR_INVALID_REQUIRED = "Invalid required"; + + /// Flag and index for Ethereum address. + mapping(address => Data) private isEthereumAddressAdded; + + /// List of Ethereum addresses. + address[] private ethereumAddresses; + + /// Required number of signatures for the Ethereum multisig. + uint256 public ethereumRequired = 2; + + /// Flag and index for Bitcoin address. + mapping(string => Data) private isBitcoinAddressAdded; + + /// List of Bitcoin addresses. + string[] private bitcoinAddresses; + + /// Required number of signatures for the Bitcoin multisig. + uint256 public bitcoinRequired = 2; + + /// Helps removing items from array. + struct Data { + bool added; + uint248 index; + } + + /* Events */ + + event EthereumAddressAdded(address indexed account); + event EthereumAddressRemoved(address indexed account); + event EthereumRequirementChanged(uint256 required); + event BitcoinAddressAdded(string account); + event BitcoinAddressRemoved(string account); + event BitcoinRequirementChanged(uint256 required); + + /* Modifiers */ + + modifier validRequirement(uint256 ownerCount, uint256 _required) { + require( + ownerCount <= MAX_OWNER_COUNT && + _required <= ownerCount && + _required != 0 && + ownerCount != 0, + ERROR_INVALID_REQUIRED + ); + _; + } + + /* Functions */ + + /** + * @notice Add rBTC address to the key holders. + * @param _address The address to be added. + * */ + function addEthereumAddress(address _address) public onlyOwner { + _addEthereumAddress(_address); + } + + /** + * @notice Add rBTC addresses to the key holders. + * @param _address The addresses to be added. + * */ + function addEthereumAddresses(address[] memory _address) public onlyOwner { + for (uint256 i = 0; i < _address.length; i++) { + _addEthereumAddress(_address[i]); + } + } + + /** + * @notice Internal function to add rBTC address to the key holders. + * @param _address The address to be added. + * */ + function _addEthereumAddress(address _address) internal { + require(_address != address(0), ERROR_INVALID_ADDRESS); + + if (!isEthereumAddressAdded[_address].added) { + isEthereumAddressAdded[_address] = Data({ + added: true, + index: uint248(ethereumAddresses.length) + }); + ethereumAddresses.push(_address); + } + + emit EthereumAddressAdded(_address); + } + + /** + * @notice Remove rBTC address to the key holders. + * @param _address The address to be removed. + * */ + function removeEthereumAddress(address _address) public onlyOwner { + _removeEthereumAddress(_address); + } + + /** + * @notice Remove rBTC addresses to the key holders. + * @param _address The addresses to be removed. + * */ + function removeEthereumAddresses(address[] memory _address) public onlyOwner { + for (uint256 i = 0; i < _address.length; i++) { + _removeEthereumAddress(_address[i]); + } + } + + /** + * @notice Internal function to remove rBTC address to the key holders. + * @param _address The address to be removed. + * */ + function _removeEthereumAddress(address _address) internal { + require(_address != address(0), ERROR_INVALID_ADDRESS); + + if (isEthereumAddressAdded[_address].added) { + uint248 index = isEthereumAddressAdded[_address].index; + if (index != ethereumAddresses.length - 1) { + ethereumAddresses[index] = ethereumAddresses[ethereumAddresses.length - 1]; + isEthereumAddressAdded[ethereumAddresses[index]].index = index; + } + ethereumAddresses.length--; + delete isEthereumAddressAdded[_address]; + } + + emit EthereumAddressRemoved(_address); + } + + /** + * @notice Get whether rBTC address is a key holder. + * @param _address The rBTC address to be checked. + * */ + function isEthereumAddressOwner(address _address) public view returns (bool) { + return isEthereumAddressAdded[_address].added; + } + + /** + * @notice Get array of rBTC key holders. + * */ + function getEthereumAddresses() public view returns (address[] memory) { + return ethereumAddresses; + } + + /** + * @notice Set flag ethereumRequired to true/false. + * @param _required The new value of the ethereumRequired flag. + * */ + function changeEthereumRequirement(uint256 _required) + public + onlyOwner + validRequirement(ethereumAddresses.length, _required) + { + ethereumRequired = _required; + emit EthereumRequirementChanged(_required); + } + + /** + * @notice Add bitcoin address to the key holders. + * @param _address The address to be added. + * */ + function addBitcoinAddress(string memory _address) public onlyOwner { + _addBitcoinAddress(_address); + } + + /** + * @notice Add bitcoin addresses to the key holders. + * @param _address The addresses to be added. + * */ + function addBitcoinAddresses(string[] memory _address) public onlyOwner { + for (uint256 i = 0; i < _address.length; i++) { + _addBitcoinAddress(_address[i]); + } + } + + /** + * @notice Internal function to add bitcoin address to the key holders. + * @param _address The address to be added. + * */ + function _addBitcoinAddress(string memory _address) internal { + require(bytes(_address).length != 0, ERROR_INVALID_ADDRESS); + + if (!isBitcoinAddressAdded[_address].added) { + isBitcoinAddressAdded[_address] = Data({ + added: true, + index: uint248(bitcoinAddresses.length) + }); + bitcoinAddresses.push(_address); + } + + emit BitcoinAddressAdded(_address); + } + + /** + * @notice Remove bitcoin address to the key holders. + * @param _address The address to be removed. + * */ + function removeBitcoinAddress(string memory _address) public onlyOwner { + _removeBitcoinAddress(_address); + } + + /** + * @notice Remove bitcoin addresses to the key holders. + * @param _address The addresses to be removed. + * */ + function removeBitcoinAddresses(string[] memory _address) public onlyOwner { + for (uint256 i = 0; i < _address.length; i++) { + _removeBitcoinAddress(_address[i]); + } + } + + /** + * @notice Internal function to remove bitcoin address to the key holders. + * @param _address The address to be removed. + * */ + function _removeBitcoinAddress(string memory _address) internal { + require(bytes(_address).length != 0, ERROR_INVALID_ADDRESS); + + if (isBitcoinAddressAdded[_address].added) { + uint248 index = isBitcoinAddressAdded[_address].index; + if (index != bitcoinAddresses.length - 1) { + bitcoinAddresses[index] = bitcoinAddresses[bitcoinAddresses.length - 1]; + isBitcoinAddressAdded[bitcoinAddresses[index]].index = index; + } + bitcoinAddresses.length--; + delete isBitcoinAddressAdded[_address]; + } + + emit BitcoinAddressRemoved(_address); + } + + /** + * @notice Get whether bitcoin address is a key holder. + * @param _address The bitcoin address to be checked. + * */ + function isBitcoinAddressOwner(string memory _address) public view returns (bool) { + return isBitcoinAddressAdded[_address].added; + } + + /** + * @notice Get array of bitcoin key holders. + * */ + function getBitcoinAddresses() public view returns (string[] memory) { + return bitcoinAddresses; + } + + /** + * @notice Set flag bitcoinRequired to true/false. + * @param _required The new value of the bitcoinRequired flag. + * */ + function changeBitcoinRequirement(uint256 _required) + public + onlyOwner + validRequirement(bitcoinAddresses.length, _required) + { + bitcoinRequired = _required; + emit BitcoinRequirementChanged(_required); + } + + /** + * @notice Add rBTC and bitcoin addresses to the key holders. + * @param _ethereumAddress the rBTC addresses to be added. + * @param _bitcoinAddress the bitcoin addresses to be added. + * */ + function addEthereumAndBitcoinAddresses( + address[] memory _ethereumAddress, + string[] memory _bitcoinAddress + ) public onlyOwner { + for (uint256 i = 0; i < _ethereumAddress.length; i++) { + _addEthereumAddress(_ethereumAddress[i]); + } + for (uint256 i = 0; i < _bitcoinAddress.length; i++) { + _addBitcoinAddress(_bitcoinAddress[i]); + } + } + + /** + * @notice Remove rBTC and bitcoin addresses to the key holders. + * @param _ethereumAddress The rBTC addresses to be removed. + * @param _bitcoinAddress The bitcoin addresses to be removed. + * */ + function removeEthereumAndBitcoinAddresses( + address[] memory _ethereumAddress, + string[] memory _bitcoinAddress + ) public onlyOwner { + for (uint256 i = 0; i < _ethereumAddress.length; i++) { + _removeEthereumAddress(_ethereumAddress[i]); + } + for (uint256 i = 0; i < _bitcoinAddress.length; i++) { + _removeBitcoinAddress(_bitcoinAddress[i]); + } + } } diff --git a/contracts/multisig/MultiSigWallet.sol b/contracts/multisig/MultiSigWallet.sol index 419e81e72..c5cd157e9 100644 --- a/contracts/multisig/MultiSigWallet.sol +++ b/contracts/multisig/MultiSigWallet.sol @@ -7,398 +7,427 @@ pragma solidity ^0.5.17; * @author Stefan George - * */ contract MultiSigWallet { - /* - * Events - */ - event Confirmation(address indexed sender, uint256 indexed transactionId); - event Revocation(address indexed sender, uint256 indexed transactionId); - event Submission(uint256 indexed transactionId); - event Execution(uint256 indexed transactionId); - event ExecutionFailure(uint256 indexed transactionId); - event Deposit(address indexed sender, uint256 value); - event OwnerAddition(address indexed owner); - event OwnerRemoval(address indexed owner); - event RequirementChange(uint256 required); - - /* - * Constants - */ - uint256 public constant MAX_OWNER_COUNT = 50; - - /* - * Storage - */ - mapping(uint256 => Transaction) public transactions; - mapping(uint256 => mapping(address => bool)) public confirmations; - mapping(address => bool) public isOwner; - address[] public owners; - uint256 public required; - uint256 public transactionCount; - - struct Transaction { - address destination; - uint256 value; - bytes data; - bool executed; - } - - /* - * Modifiers - */ - modifier onlyWallet() { - require(msg.sender == address(this)); - _; - } - - modifier ownerDoesNotExist(address owner) { - require(!isOwner[owner]); - _; - } - - modifier ownerExists(address owner) { - require(isOwner[owner]); - _; - } - - modifier transactionExists(uint256 transactionId) { - require(transactions[transactionId].destination != address(0)); - _; - } - - modifier confirmed(uint256 transactionId, address owner) { - require(confirmations[transactionId][owner]); - _; - } - - modifier notConfirmed(uint256 transactionId, address owner) { - require(!confirmations[transactionId][owner]); - _; - } - - modifier notExecuted(uint256 transactionId) { - require(!transactions[transactionId].executed); - _; - } - - modifier notNull(address _address) { - require(_address != address(0)); - _; - } - - modifier validRequirement(uint256 ownerCount, uint256 _required) { - require(ownerCount <= MAX_OWNER_COUNT && _required <= ownerCount && _required != 0 && ownerCount != 0); - _; - } - - /// @notice Fallback function allows to deposit ether. - function() external payable { - if (msg.value > 0) emit Deposit(msg.sender, msg.value); - } - - /* - * Public functions - */ - - /** - * @notice Contract constructor sets initial owners and required number - * of confirmations. - * - * @param _owners List of initial owners. - * @param _required Number of required confirmations. - * */ - constructor(address[] memory _owners, uint256 _required) public validRequirement(_owners.length, _required) { - for (uint256 i = 0; i < _owners.length; i++) { - require(!isOwner[_owners[i]] && _owners[i] != address(0)); - isOwner[_owners[i]] = true; - } - owners = _owners; - required = _required; - } - - /** - * @notice Allows to add a new owner. Transaction has to be sent by wallet. - * @param owner Address of new owner. - * */ - function addOwner(address owner) - public - onlyWallet - ownerDoesNotExist(owner) - notNull(owner) - validRequirement(owners.length + 1, required) - { - isOwner[owner] = true; - owners.push(owner); - emit OwnerAddition(owner); - } - - /** - * @notice Allows to remove an owner. Transaction has to be sent by wallet. - * @param owner Address of owner. - * */ - function removeOwner(address owner) public onlyWallet ownerExists(owner) { - isOwner[owner] = false; - for (uint256 i = 0; i < owners.length - 1; i++) - if (owners[i] == owner) { - owners[i] = owners[owners.length - 1]; - break; - } - owners.length -= 1; - if (required > owners.length) changeRequirement(owners.length); - emit OwnerRemoval(owner); - } - - /** - * @notice Allows to replace an owner with a new owner. Transaction has - * to be sent by wallet. - * - * @param owner Address of owner to be replaced. - * @param newOwner Address of new owner. - * */ - function replaceOwner(address owner, address newOwner) public onlyWallet ownerExists(owner) ownerDoesNotExist(newOwner) { - for (uint256 i = 0; i < owners.length; i++) - if (owners[i] == owner) { - owners[i] = newOwner; - break; - } - isOwner[owner] = false; - isOwner[newOwner] = true; - emit OwnerRemoval(owner); - emit OwnerAddition(newOwner); - } - - /** - * @notice Allows to change the number of required confirmations. - * Transaction has to be sent by wallet. - * - * @param _required Number of required confirmations. - * */ - function changeRequirement(uint256 _required) public onlyWallet validRequirement(owners.length, _required) { - required = _required; - emit RequirementChange(_required); - } - - /** - * @notice Allows an owner to submit and confirm a transaction. - * - * @param destination Transaction target address. - * @param value Transaction ether value. - * @param data Transaction data payload. - * - * @return Returns transaction ID. - * */ - function submitTransaction( - address destination, - uint256 value, - bytes memory data - ) public returns (uint256 transactionId) { - transactionId = addTransaction(destination, value, data); - confirmTransaction(transactionId); - } - - /** - * @notice Allows an owner to confirm a transaction. - * @param transactionId Transaction ID. - * */ - function confirmTransaction(uint256 transactionId) - public - ownerExists(msg.sender) - transactionExists(transactionId) - notConfirmed(transactionId, msg.sender) - { - confirmations[transactionId][msg.sender] = true; - emit Confirmation(msg.sender, transactionId); - executeTransaction(transactionId); - } - - /** - * @notice Allows an owner to revoke a confirmation for a transaction. - * @param transactionId Transaction ID. - * */ - function revokeConfirmation(uint256 transactionId) - public - ownerExists(msg.sender) - confirmed(transactionId, msg.sender) - notExecuted(transactionId) - { - confirmations[transactionId][msg.sender] = false; - emit Revocation(msg.sender, transactionId); - } - - /** - * @notice Allows anyone to execute a confirmed transaction. - * @param transactionId Transaction ID. - * */ - function executeTransaction(uint256 transactionId) - public - ownerExists(msg.sender) - confirmed(transactionId, msg.sender) - notExecuted(transactionId) - { - if (isConfirmed(transactionId)) { - Transaction storage txn = transactions[transactionId]; - txn.executed = true; - if (external_call(txn.destination, txn.value, txn.data.length, txn.data)) emit Execution(transactionId); - else { - emit ExecutionFailure(transactionId); - txn.executed = false; - } - } - } - - /** - * @notice Low level transaction execution. - * - * @dev Call has been separated into its own function in order to - * take advantage of the Solidity's code generator to produce a - * loop that copies tx.data into memory. - * - * @param destination The address of the Smart Contract to call. - * @param value The amout of rBTC to send w/ the transaction. - * @param dataLength The size of the payload. - * @param data The payload. - * - * @return Success or failure. - * */ - function external_call( - address destination, - uint256 value, - uint256 dataLength, - bytes memory data - ) internal returns (bool) { - bool result; - assembly { - let x := mload(0x40) /// "Allocate" memory for output (0x40 is where "free memory" pointer is stored by convention) - let d := add(data, 32) /// First 32 bytes are the padded length of data, so exclude that - result := call( - sub(gas, 34710), /// 34710 is the value that solidity is currently emitting - /// It includes callGas (700) + callVeryLow (3, to pay for SUB) + callValueTransferGas (9000) + - /// callNewAccountGas (25000, in case the destination address does not exist and needs creating) - destination, - value, - d, - dataLength, /// Size of the input (in bytes) - this is what fixes the padding problem - x, - 0 /// Output is ignored, therefore the output size is zero - ) - } - return result; - } - - /** - * @notice Returns the confirmation status of a transaction. - * @param transactionId Transaction ID. - * @return Confirmation status. - * */ - function isConfirmed(uint256 transactionId) public view returns (bool) { - uint256 count = 0; - for (uint256 i = 0; i < owners.length; i++) { - if (confirmations[transactionId][owners[i]]) count += 1; - if (count == required) return true; - } - - return false; - } - - /* - * Internal functions - */ - - /** - * @notice Adds a new transaction to the transaction mapping, - * if transaction does not exist yet. - * - * @param destination Transaction target address. - * @param value Transaction ether value. - * @param data Transaction data payload. - * - * @return Returns transaction ID. - * */ - function addTransaction( - address destination, - uint256 value, - bytes memory data - ) internal notNull(destination) returns (uint256 transactionId) { - transactionId = transactionCount; - transactions[transactionId] = Transaction({ destination: destination, value: value, data: data, executed: false }); - transactionCount += 1; - emit Submission(transactionId); - } - - /* - * Web3 call functions - */ - - /** - * @notice Get the number of confirmations of a transaction. - * @param transactionId Transaction ID. - * @return Number of confirmations. - * */ - function getConfirmationCount(uint256 transactionId) public view returns (uint256 count) { - for (uint256 i = 0; i < owners.length; i++) if (confirmations[transactionId][owners[i]]) count += 1; - } - - /** - * @notice Get the total number of transactions after filers are applied. - * @param pending Include pending transactions. - * @param executed Include executed transactions. - * @return Total number of transactions after filters are applied. - * */ - function getTransactionCount(bool pending, bool executed) public view returns (uint256 count) { - for (uint256 i = 0; i < transactionCount; i++) - if ((pending && !transactions[i].executed) || (executed && transactions[i].executed)) count += 1; - } - - /** - * @notice Get the list of owners. - * @return List of owner addresses. - * */ - function getOwners() public view returns (address[] memory) { - return owners; - } - - /** - * @notice Get the array with owner addresses, which confirmed transaction. - * @param transactionId Transaction ID. - * @return Returns array of owner addresses. - * */ - function getConfirmations(uint256 transactionId) public view returns (address[] memory _confirmations) { - address[] memory confirmationsTemp = new address[](owners.length); - uint256 count = 0; - uint256 i; - for (i = 0; i < owners.length; i++) - if (confirmations[transactionId][owners[i]]) { - confirmationsTemp[count] = owners[i]; - count += 1; - } - _confirmations = new address[](count); - for (i = 0; i < count; i++) _confirmations[i] = confirmationsTemp[i]; - } - - /** - * @notice Get the list of transaction IDs in defined range. - * - * @param from Index start position of transaction array. - * @param to Index end position of transaction array. - * @param pending Include pending transactions. - * @param executed Include executed transactions. - * - * @return Returns array of transaction IDs. - * */ - function getTransactionIds( - uint256 from, - uint256 to, - bool pending, - bool executed - ) public view returns (uint256[] memory _transactionIds) { - uint256[] memory transactionIdsTemp = new uint256[](transactionCount); - uint256 count = 0; - uint256 i; - for (i = 0; i < transactionCount; i++) - if ((pending && !transactions[i].executed) || (executed && transactions[i].executed)) { - transactionIdsTemp[count] = i; - count += 1; - } - _transactionIds = new uint256[](to - from); - for (i = from; i < to; i++) _transactionIds[i - from] = transactionIdsTemp[i]; - } + /* + * Events + */ + event Confirmation(address indexed sender, uint256 indexed transactionId); + event Revocation(address indexed sender, uint256 indexed transactionId); + event Submission(uint256 indexed transactionId); + event Execution(uint256 indexed transactionId); + event ExecutionFailure(uint256 indexed transactionId); + event Deposit(address indexed sender, uint256 value); + event OwnerAddition(address indexed owner); + event OwnerRemoval(address indexed owner); + event RequirementChange(uint256 required); + + /* + * Constants + */ + uint256 public constant MAX_OWNER_COUNT = 50; + + /* + * Storage + */ + mapping(uint256 => Transaction) public transactions; + mapping(uint256 => mapping(address => bool)) public confirmations; + mapping(address => bool) public isOwner; + address[] public owners; + uint256 public required; + uint256 public transactionCount; + + struct Transaction { + address destination; + uint256 value; + bytes data; + bool executed; + } + + /* + * Modifiers + */ + modifier onlyWallet() { + require(msg.sender == address(this)); + _; + } + + modifier ownerDoesNotExist(address owner) { + require(!isOwner[owner]); + _; + } + + modifier ownerExists(address owner) { + require(isOwner[owner]); + _; + } + + modifier transactionExists(uint256 transactionId) { + require(transactions[transactionId].destination != address(0)); + _; + } + + modifier confirmed(uint256 transactionId, address owner) { + require(confirmations[transactionId][owner]); + _; + } + + modifier notConfirmed(uint256 transactionId, address owner) { + require(!confirmations[transactionId][owner]); + _; + } + + modifier notExecuted(uint256 transactionId) { + require(!transactions[transactionId].executed); + _; + } + + modifier notNull(address _address) { + require(_address != address(0)); + _; + } + + modifier validRequirement(uint256 ownerCount, uint256 _required) { + require( + ownerCount <= MAX_OWNER_COUNT && + _required <= ownerCount && + _required != 0 && + ownerCount != 0 + ); + _; + } + + /// @notice Fallback function allows to deposit ether. + function() external payable { + if (msg.value > 0) emit Deposit(msg.sender, msg.value); + } + + /* + * Public functions + */ + + /** + * @notice Contract constructor sets initial owners and required number + * of confirmations. + * + * @param _owners List of initial owners. + * @param _required Number of required confirmations. + * */ + constructor(address[] memory _owners, uint256 _required) + public + validRequirement(_owners.length, _required) + { + for (uint256 i = 0; i < _owners.length; i++) { + require(!isOwner[_owners[i]] && _owners[i] != address(0)); + isOwner[_owners[i]] = true; + } + owners = _owners; + required = _required; + } + + /** + * @notice Allows to add a new owner. Transaction has to be sent by wallet. + * @param owner Address of new owner. + * */ + function addOwner(address owner) + public + onlyWallet + ownerDoesNotExist(owner) + notNull(owner) + validRequirement(owners.length + 1, required) + { + isOwner[owner] = true; + owners.push(owner); + emit OwnerAddition(owner); + } + + /** + * @notice Allows to remove an owner. Transaction has to be sent by wallet. + * @param owner Address of owner. + * */ + function removeOwner(address owner) public onlyWallet ownerExists(owner) { + isOwner[owner] = false; + for (uint256 i = 0; i < owners.length - 1; i++) + if (owners[i] == owner) { + owners[i] = owners[owners.length - 1]; + break; + } + owners.length -= 1; + if (required > owners.length) changeRequirement(owners.length); + emit OwnerRemoval(owner); + } + + /** + * @notice Allows to replace an owner with a new owner. Transaction has + * to be sent by wallet. + * + * @param owner Address of owner to be replaced. + * @param newOwner Address of new owner. + * */ + function replaceOwner(address owner, address newOwner) + public + onlyWallet + ownerExists(owner) + ownerDoesNotExist(newOwner) + { + for (uint256 i = 0; i < owners.length; i++) + if (owners[i] == owner) { + owners[i] = newOwner; + break; + } + isOwner[owner] = false; + isOwner[newOwner] = true; + emit OwnerRemoval(owner); + emit OwnerAddition(newOwner); + } + + /** + * @notice Allows to change the number of required confirmations. + * Transaction has to be sent by wallet. + * + * @param _required Number of required confirmations. + * */ + function changeRequirement(uint256 _required) + public + onlyWallet + validRequirement(owners.length, _required) + { + required = _required; + emit RequirementChange(_required); + } + + /** + * @notice Allows an owner to submit and confirm a transaction. + * + * @param destination Transaction target address. + * @param value Transaction ether value. + * @param data Transaction data payload. + * + * @return Returns transaction ID. + * */ + function submitTransaction( + address destination, + uint256 value, + bytes memory data + ) public returns (uint256 transactionId) { + transactionId = addTransaction(destination, value, data); + confirmTransaction(transactionId); + } + + /** + * @notice Allows an owner to confirm a transaction. + * @param transactionId Transaction ID. + * */ + function confirmTransaction(uint256 transactionId) + public + ownerExists(msg.sender) + transactionExists(transactionId) + notConfirmed(transactionId, msg.sender) + { + confirmations[transactionId][msg.sender] = true; + emit Confirmation(msg.sender, transactionId); + executeTransaction(transactionId); + } + + /** + * @notice Allows an owner to revoke a confirmation for a transaction. + * @param transactionId Transaction ID. + * */ + function revokeConfirmation(uint256 transactionId) + public + ownerExists(msg.sender) + confirmed(transactionId, msg.sender) + notExecuted(transactionId) + { + confirmations[transactionId][msg.sender] = false; + emit Revocation(msg.sender, transactionId); + } + + /** + * @notice Allows anyone to execute a confirmed transaction. + * @param transactionId Transaction ID. + * */ + function executeTransaction(uint256 transactionId) + public + ownerExists(msg.sender) + confirmed(transactionId, msg.sender) + notExecuted(transactionId) + { + if (isConfirmed(transactionId)) { + Transaction storage txn = transactions[transactionId]; + txn.executed = true; + if (external_call(txn.destination, txn.value, txn.data.length, txn.data)) + emit Execution(transactionId); + else { + emit ExecutionFailure(transactionId); + txn.executed = false; + } + } + } + + /** + * @notice Low level transaction execution. + * + * @dev Call has been separated into its own function in order to + * take advantage of the Solidity's code generator to produce a + * loop that copies tx.data into memory. + * + * @param destination The address of the Smart Contract to call. + * @param value The amout of rBTC to send w/ the transaction. + * @param dataLength The size of the payload. + * @param data The payload. + * + * @return Success or failure. + * */ + function external_call( + address destination, + uint256 value, + uint256 dataLength, + bytes memory data + ) internal returns (bool) { + bool result; + assembly { + let x := mload(0x40) /// "Allocate" memory for output (0x40 is where "free memory" pointer is stored by convention) + let d := add(data, 32) /// First 32 bytes are the padded length of data, so exclude that + result := call( + sub(gas, 34710), /// 34710 is the value that solidity is currently emitting + /// It includes callGas (700) + callVeryLow (3, to pay for SUB) + callValueTransferGas (9000) + + /// callNewAccountGas (25000, in case the destination address does not exist and needs creating) + destination, + value, + d, + dataLength, /// Size of the input (in bytes) - this is what fixes the padding problem + x, + 0 /// Output is ignored, therefore the output size is zero + ) + } + return result; + } + + /** + * @notice Returns the confirmation status of a transaction. + * @param transactionId Transaction ID. + * @return Confirmation status. + * */ + function isConfirmed(uint256 transactionId) public view returns (bool) { + uint256 count = 0; + for (uint256 i = 0; i < owners.length; i++) { + if (confirmations[transactionId][owners[i]]) count += 1; + if (count == required) return true; + } + + return false; + } + + /* + * Internal functions + */ + + /** + * @notice Adds a new transaction to the transaction mapping, + * if transaction does not exist yet. + * + * @param destination Transaction target address. + * @param value Transaction ether value. + * @param data Transaction data payload. + * + * @return Returns transaction ID. + * */ + function addTransaction( + address destination, + uint256 value, + bytes memory data + ) internal notNull(destination) returns (uint256 transactionId) { + transactionId = transactionCount; + transactions[transactionId] = Transaction({ + destination: destination, + value: value, + data: data, + executed: false + }); + transactionCount += 1; + emit Submission(transactionId); + } + + /* + * Web3 call functions + */ + + /** + * @notice Get the number of confirmations of a transaction. + * @param transactionId Transaction ID. + * @return Number of confirmations. + * */ + function getConfirmationCount(uint256 transactionId) public view returns (uint256 count) { + for (uint256 i = 0; i < owners.length; i++) + if (confirmations[transactionId][owners[i]]) count += 1; + } + + /** + * @notice Get the total number of transactions after filers are applied. + * @param pending Include pending transactions. + * @param executed Include executed transactions. + * @return Total number of transactions after filters are applied. + * */ + function getTransactionCount(bool pending, bool executed) public view returns (uint256 count) { + for (uint256 i = 0; i < transactionCount; i++) + if ((pending && !transactions[i].executed) || (executed && transactions[i].executed)) + count += 1; + } + + /** + * @notice Get the list of owners. + * @return List of owner addresses. + * */ + function getOwners() public view returns (address[] memory) { + return owners; + } + + /** + * @notice Get the array with owner addresses, which confirmed transaction. + * @param transactionId Transaction ID. + * @return Returns array of owner addresses. + * */ + function getConfirmations(uint256 transactionId) + public + view + returns (address[] memory _confirmations) + { + address[] memory confirmationsTemp = new address[](owners.length); + uint256 count = 0; + uint256 i; + for (i = 0; i < owners.length; i++) + if (confirmations[transactionId][owners[i]]) { + confirmationsTemp[count] = owners[i]; + count += 1; + } + _confirmations = new address[](count); + for (i = 0; i < count; i++) _confirmations[i] = confirmationsTemp[i]; + } + + /** + * @notice Get the list of transaction IDs in defined range. + * + * @param from Index start position of transaction array. + * @param to Index end position of transaction array. + * @param pending Include pending transactions. + * @param executed Include executed transactions. + * + * @return Returns array of transaction IDs. + * */ + function getTransactionIds( + uint256 from, + uint256 to, + bool pending, + bool executed + ) public view returns (uint256[] memory _transactionIds) { + uint256[] memory transactionIdsTemp = new uint256[](transactionCount); + uint256 count = 0; + uint256 i; + for (i = 0; i < transactionCount; i++) + if ((pending && !transactions[i].executed) || (executed && transactions[i].executed)) { + transactionIdsTemp[count] = i; + count += 1; + } + _transactionIds = new uint256[](to - from); + for (i = from; i < to; i++) _transactionIds[i - from] = transactionIdsTemp[i]; + } } diff --git a/contracts/openzeppelin/Address.sol b/contracts/openzeppelin/Address.sol index 4b03e3a77..c4825ea35 100644 --- a/contracts/openzeppelin/Address.sol +++ b/contracts/openzeppelin/Address.sol @@ -4,70 +4,70 @@ pragma solidity >=0.5.0 <0.6.0; * @dev Collection of functions related to the address type */ library Address { - /** - * @dev Returns true if `account` is a contract. - * - * [IMPORTANT] - * ==== - * It is unsafe to assume that an address for which this function returns - * false is an externally-owned account (EOA) and not a contract. - * - * Among others, `isContract` will return false for the following - * types of addresses: - * - * - an externally-owned account - * - a contract in construction - * - an address where a contract will be created - * - an address where a contract lived, but was destroyed - * ==== - */ - function isContract(address account) internal view returns (bool) { - // According to EIP-1052, 0x0 is the value returned for not-yet created accounts - // and 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 is returned - // for accounts without code, i.e. `keccak256('')` - bytes32 codehash; - bytes32 accountHash = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470; - // solhint-disable-next-line no-inline-assembly - assembly { - codehash := extcodehash(account) - } - return (codehash != accountHash && codehash != 0x0); - } + /** + * @dev Returns true if `account` is a contract. + * + * [IMPORTANT] + * ==== + * It is unsafe to assume that an address for which this function returns + * false is an externally-owned account (EOA) and not a contract. + * + * Among others, `isContract` will return false for the following + * types of addresses: + * + * - an externally-owned account + * - a contract in construction + * - an address where a contract will be created + * - an address where a contract lived, but was destroyed + * ==== + */ + function isContract(address account) internal view returns (bool) { + // According to EIP-1052, 0x0 is the value returned for not-yet created accounts + // and 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 is returned + // for accounts without code, i.e. `keccak256('')` + bytes32 codehash; + bytes32 accountHash = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470; + // solhint-disable-next-line no-inline-assembly + assembly { + codehash := extcodehash(account) + } + return (codehash != accountHash && codehash != 0x0); + } - /** - * @dev Converts an `address` into `address payable`. Note that this is - * simply a type cast: the actual underlying value is not changed. - * - * _Available since v2.4.0._ - */ - function toPayable(address account) internal pure returns (address payable) { - return address(uint160(account)); - } + /** + * @dev Converts an `address` into `address payable`. Note that this is + * simply a type cast: the actual underlying value is not changed. + * + * _Available since v2.4.0._ + */ + function toPayable(address account) internal pure returns (address payable) { + return address(uint160(account)); + } - /** - * @dev Replacement for Solidity's `transfer`: sends `amount` wei to - * `recipient`, forwarding all available gas and reverting on errors. - * - * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost - * of certain opcodes, possibly making contracts go over the 2300 gas limit - * imposed by `transfer`, making them unable to receive funds via - * `transfer`. {sendValue} removes this limitation. - * - * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more]. - * - * IMPORTANT: because control is transferred to `recipient`, care must be - * taken to not create reentrancy vulnerabilities. Consider using - * {ReentrancyGuard} or the - * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html - * #use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. - * - * _Available since v2.4.0._ - */ - function sendValue(address recipient, uint256 amount) internal { - require(address(this).balance >= amount, "Address: insufficient balance"); + /** + * @dev Replacement for Solidity's `transfer`: sends `amount` wei to + * `recipient`, forwarding all available gas and reverting on errors. + * + * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost + * of certain opcodes, possibly making contracts go over the 2300 gas limit + * imposed by `transfer`, making them unable to receive funds via + * `transfer`. {sendValue} removes this limitation. + * + * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more]. + * + * IMPORTANT: because control is transferred to `recipient`, care must be + * taken to not create reentrancy vulnerabilities. Consider using + * {ReentrancyGuard} or the + * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html + * #use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. + * + * _Available since v2.4.0._ + */ + function sendValue(address recipient, uint256 amount) internal { + require(address(this).balance >= amount, "Address: insufficient balance"); - // solhint-disable-next-line avoid-call-value - (bool success, ) = recipient.call.value(amount)(""); - require(success, "Address: unable to send value, recipient may have reverted"); - } + // solhint-disable-next-line avoid-call-value + (bool success, ) = recipient.call.value(amount)(""); + require(success, "Address: unable to send value, recipient may have reverted"); + } } diff --git a/contracts/openzeppelin/Context.sol b/contracts/openzeppelin/Context.sol index 6328c7046..a0b8aa4eb 100644 --- a/contracts/openzeppelin/Context.sol +++ b/contracts/openzeppelin/Context.sol @@ -11,18 +11,18 @@ pragma solidity >=0.5.0 <0.6.0; * This contract is only required for intermediate, library-like contracts. */ contract Context { - // Empty internal constructor, to prevent people from mistakenly deploying - // an instance of this contract, which should be used via inheritance. - constructor() internal {} + // Empty internal constructor, to prevent people from mistakenly deploying + // an instance of this contract, which should be used via inheritance. + constructor() internal {} - // solhint-disable-previous-line no-empty-blocks + // solhint-disable-previous-line no-empty-blocks - function _msgSender() internal view returns (address payable) { - return msg.sender; - } + function _msgSender() internal view returns (address payable) { + return msg.sender; + } - function _msgData() internal view returns (bytes memory) { - this; // silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691 - return msg.data; - } + function _msgData() internal view returns (bytes memory) { + this; // silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691 + return msg.data; + } } diff --git a/contracts/openzeppelin/ECDSA.sol b/contracts/openzeppelin/ECDSA.sol index a29928e92..22470edd4 100644 --- a/contracts/openzeppelin/ECDSA.sol +++ b/contracts/openzeppelin/ECDSA.sol @@ -7,76 +7,76 @@ pragma solidity >=0.5.0 <0.6.0; * of the private keys of a given address. */ library ECDSA { - /** - * @dev Returns the address that signed a hashed message (`hash`) with - * `signature`. This address can then be used for verification purposes. - * - * The `ecrecover` EVM opcode allows for malleable (non-unique) signatures: - * this function rejects them by requiring the `s` value to be in the lower - * half order, and the `v` value to be either 27 or 28. - * - * NOTE: This call _does not revert_ if the signature is invalid, or - * if the signer is otherwise unable to be retrieved. In those scenarios, - * the zero address is returned. - * - * IMPORTANT: `hash` _must_ be the result of a hash operation for the - * verification to be secure: it is possible to craft signatures that - * recover to arbitrary addresses for non-hashed data. A safe way to ensure - * this is by receiving a hash of the original message (which may otherwise - * be too long), and then calling {toEthSignedMessageHash} on it. - */ - function recover(bytes32 hash, bytes memory signature) internal pure returns (address) { - // Check the signature length - if (signature.length != 65) { - return (address(0)); - } + /** + * @dev Returns the address that signed a hashed message (`hash`) with + * `signature`. This address can then be used for verification purposes. + * + * The `ecrecover` EVM opcode allows for malleable (non-unique) signatures: + * this function rejects them by requiring the `s` value to be in the lower + * half order, and the `v` value to be either 27 or 28. + * + * NOTE: This call _does not revert_ if the signature is invalid, or + * if the signer is otherwise unable to be retrieved. In those scenarios, + * the zero address is returned. + * + * IMPORTANT: `hash` _must_ be the result of a hash operation for the + * verification to be secure: it is possible to craft signatures that + * recover to arbitrary addresses for non-hashed data. A safe way to ensure + * this is by receiving a hash of the original message (which may otherwise + * be too long), and then calling {toEthSignedMessageHash} on it. + */ + function recover(bytes32 hash, bytes memory signature) internal pure returns (address) { + // Check the signature length + if (signature.length != 65) { + return (address(0)); + } - // Divide the signature in r, s and v variables - bytes32 r; - bytes32 s; - uint8 v; + // Divide the signature in r, s and v variables + bytes32 r; + bytes32 s; + uint8 v; - // ecrecover takes the signature parameters, and the only way to get them - // currently is to use assembly. - // solhint-disable-next-line no-inline-assembly - assembly { - r := mload(add(signature, 0x20)) - s := mload(add(signature, 0x40)) - v := byte(0, mload(add(signature, 0x60))) - } + // ecrecover takes the signature parameters, and the only way to get them + // currently is to use assembly. + // solhint-disable-next-line no-inline-assembly + assembly { + r := mload(add(signature, 0x20)) + s := mload(add(signature, 0x40)) + v := byte(0, mload(add(signature, 0x60))) + } - // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature - // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines - // the valid range for s in (281): 0 < s < secp256k1n ÷ 2 + 1, and for v in (282): v ∈ {27, 28}. Most - // signatures from current libraries generate a unique signature with an s-value in the lower half order. - // - // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value - // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or - // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept - // these malleable signatures as well. - if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { - return address(0); - } + // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature + // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines + // the valid range for s in (281): 0 < s < secp256k1n ÷ 2 + 1, and for v in (282): v ∈ {27, 28}. Most + // signatures from current libraries generate a unique signature with an s-value in the lower half order. + // + // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value + // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or + // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept + // these malleable signatures as well. + if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { + return address(0); + } - if (v != 27 && v != 28) { - return address(0); - } + if (v != 27 && v != 28) { + return address(0); + } - // If the signature is valid (and not malleable), return the signer address - return ecrecover(hash, v, r, s); - } + // If the signature is valid (and not malleable), return the signer address + return ecrecover(hash, v, r, s); + } - /** - * @dev Returns an Ethereum Signed Message, created from a `hash`. This - * replicates the behavior of the - * https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign[`eth_sign`] - * JSON-RPC method. - * - * See {recover}. - */ - function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32) { - // 32 is the length in bytes of hash, - // enforced by the type signature above - return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); - } + /** + * @dev Returns an Ethereum Signed Message, created from a `hash`. This + * replicates the behavior of the + * https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign[`eth_sign`] + * JSON-RPC method. + * + * See {recover}. + */ + function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32) { + // 32 is the length in bytes of hash, + // enforced by the type signature above + return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); + } } diff --git a/contracts/openzeppelin/ERC20.sol b/contracts/openzeppelin/ERC20.sol index 31255a468..13304e71d 100644 --- a/contracts/openzeppelin/ERC20.sol +++ b/contracts/openzeppelin/ERC20.sol @@ -29,214 +29,235 @@ import "./SafeMath.sol"; * allowances. See {IERC20-approve}. */ contract ERC20 is Context, IERC20_ { - using SafeMath for uint256; - - mapping(address => uint256) private _balances; - - mapping(address => mapping(address => uint256)) private _allowances; - - uint256 private _totalSupply; - - /** - * @dev See {IERC20-totalSupply}. - */ - function totalSupply() public view returns (uint256) { - return _totalSupply; - } - - /** - * @dev See {IERC20-balanceOf}. - */ - function balanceOf(address account) public view returns (uint256) { - return _balances[account]; - } - - /** - * @dev See {IERC20-transfer}. - * - * Requirements: - * - * - `recipient` cannot be the zero address. - * - the caller must have a balance of at least `amount`. - */ - function transfer(address recipient, uint256 amount) public returns (bool) { - _transfer(_msgSender(), recipient, amount); - return true; - } - - /** - * @dev See {IERC20-allowance}. - */ - function allowance(address owner, address spender) public view returns (uint256) { - return _allowances[owner][spender]; - } - - /** - * @dev See {IERC20-approve}. - * - * Requirements: - * - * - `spender` cannot be the zero address. - */ - function approve(address spender, uint256 amount) public returns (bool) { - _approve(_msgSender(), spender, amount); - return true; - } - - /** - * @dev See {IERC20-transferFrom}. - * - * Emits an {Approval} event indicating the updated allowance. This is not - * required by the EIP. See the note at the beginning of {ERC20}; - * - * Requirements: - * - `sender` and `recipient` cannot be the zero address. - * - `sender` must have a balance of at least `amount`. - * - the caller must have allowance for `sender`'s tokens of at least - * `amount`. - */ - function transferFrom( - address sender, - address recipient, - uint256 amount - ) public returns (bool) { - _transfer(sender, recipient, amount); - _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount, "ERC20: transfer amount exceeds allowance")); - return true; - } - - /** - * @dev Atomically increases the allowance granted to `spender` by the caller. - * - * This is an alternative to {approve} that can be used as a mitigation for - * problems described in {IERC20-approve}. - * - * Emits an {Approval} event indicating the updated allowance. - * - * Requirements: - * - * - `spender` cannot be the zero address. - */ - function increaseAllowance(address spender, uint256 addedValue) public returns (bool) { - _approve(_msgSender(), spender, _allowances[_msgSender()][spender].add(addedValue)); - return true; - } - - /** - * @dev Atomically decreases the allowance granted to `spender` by the caller. - * - * This is an alternative to {approve} that can be used as a mitigation for - * problems described in {IERC20-approve}. - * - * Emits an {Approval} event indicating the updated allowance. - * - * Requirements: - * - * - `spender` cannot be the zero address. - * - `spender` must have allowance for the caller of at least - * `subtractedValue`. - */ - function decreaseAllowance(address spender, uint256 subtractedValue) public returns (bool) { - _approve(_msgSender(), spender, _allowances[_msgSender()][spender].sub(subtractedValue, "ERC20: decreased allowance below zero")); - return true; - } - - /** - * @dev Moves tokens `amount` from `sender` to `recipient`. - * - * This is internal function is equivalent to {transfer}, and can be used to - * e.g. implement automatic token fees, slashing mechanisms, etc. - * - * Emits a {Transfer} event. - * - * Requirements: - * - * - `sender` cannot be the zero address. - * - `recipient` cannot be the zero address. - * - `sender` must have a balance of at least `amount`. - */ - function _transfer( - address sender, - address recipient, - uint256 amount - ) internal { - require(sender != address(0), "ERC20: transfer from the zero address"); - require(recipient != address(0), "ERC20: transfer to the zero address"); - - _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance"); - _balances[recipient] = _balances[recipient].add(amount); - emit Transfer(sender, recipient, amount); - } - - /** @dev Creates `amount` tokens and assigns them to `account`, increasing - * the total supply. - * - * Emits a {Transfer} event with `from` set to the zero address. - * - * Requirements - * - * - `to` cannot be the zero address. - */ - function _mint(address account, uint256 amount) internal { - require(account != address(0), "ERC20: mint to the zero address"); - - _totalSupply = _totalSupply.add(amount); - _balances[account] = _balances[account].add(amount); - emit Transfer(address(0), account, amount); - } - - /** - * @dev Destroys `amount` tokens from `account`, reducing the - * total supply. - * - * Emits a {Transfer} event with `to` set to the zero address. - * - * Requirements - * - * - `account` cannot be the zero address. - * - `account` must have at least `amount` tokens. - */ - function _burn(address account, uint256 amount) internal { - require(account != address(0), "ERC20: burn from the zero address"); - - _balances[account] = _balances[account].sub(amount, "ERC20: burn amount exceeds balance"); - _totalSupply = _totalSupply.sub(amount); - emit Transfer(account, address(0), amount); - } - - /** - * @dev Sets `amount` as the allowance of `spender` over the `owner`s tokens. - * - * This is internal function is equivalent to `approve`, and can be used to - * e.g. set automatic allowances for certain subsystems, etc. - * - * Emits an {Approval} event. - * - * Requirements: - * - * - `owner` cannot be the zero address. - * - `spender` cannot be the zero address. - */ - function _approve( - address owner, - address spender, - uint256 amount - ) internal { - require(owner != address(0), "ERC20: approve from the zero address"); - require(spender != address(0), "ERC20: approve to the zero address"); - - _allowances[owner][spender] = amount; - emit Approval(owner, spender, amount); - } - - /** - * @dev Destroys `amount` tokens from `account`.`amount` is then deducted - * from the caller's allowance. - * - * See {_burn} and {_approve}. - */ - function _burnFrom(address account, uint256 amount) internal { - _burn(account, amount); - _approve(account, _msgSender(), _allowances[account][_msgSender()].sub(amount, "ERC20: burn amount exceeds allowance")); - } + using SafeMath for uint256; + + mapping(address => uint256) private _balances; + + mapping(address => mapping(address => uint256)) private _allowances; + + uint256 private _totalSupply; + + /** + * @dev See {IERC20-totalSupply}. + */ + function totalSupply() public view returns (uint256) { + return _totalSupply; + } + + /** + * @dev See {IERC20-balanceOf}. + */ + function balanceOf(address account) public view returns (uint256) { + return _balances[account]; + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `recipient` cannot be the zero address. + * - the caller must have a balance of at least `amount`. + */ + function transfer(address recipient, uint256 amount) public returns (bool) { + _transfer(_msgSender(), recipient, amount); + return true; + } + + /** + * @dev See {IERC20-allowance}. + */ + function allowance(address owner, address spender) public view returns (uint256) { + return _allowances[owner][spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 amount) public returns (bool) { + _approve(_msgSender(), spender, amount); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Emits an {Approval} event indicating the updated allowance. This is not + * required by the EIP. See the note at the beginning of {ERC20}; + * + * Requirements: + * - `sender` and `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + * - the caller must have allowance for `sender`'s tokens of at least + * `amount`. + */ + function transferFrom( + address sender, + address recipient, + uint256 amount + ) public returns (bool) { + _transfer(sender, recipient, amount); + _approve( + sender, + _msgSender(), + _allowances[sender][_msgSender()].sub( + amount, + "ERC20: transfer amount exceeds allowance" + ) + ); + return true; + } + + /** + * @dev Atomically increases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function increaseAllowance(address spender, uint256 addedValue) public returns (bool) { + _approve(_msgSender(), spender, _allowances[_msgSender()][spender].add(addedValue)); + return true; + } + + /** + * @dev Atomically decreases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `spender` must have allowance for the caller of at least + * `subtractedValue`. + */ + function decreaseAllowance(address spender, uint256 subtractedValue) public returns (bool) { + _approve( + _msgSender(), + spender, + _allowances[_msgSender()][spender].sub( + subtractedValue, + "ERC20: decreased allowance below zero" + ) + ); + return true; + } + + /** + * @dev Moves tokens `amount` from `sender` to `recipient`. + * + * This is internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * Requirements: + * + * - `sender` cannot be the zero address. + * - `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + */ + function _transfer( + address sender, + address recipient, + uint256 amount + ) internal { + require(sender != address(0), "ERC20: transfer from the zero address"); + require(recipient != address(0), "ERC20: transfer to the zero address"); + + _balances[sender] = _balances[sender].sub( + amount, + "ERC20: transfer amount exceeds balance" + ); + _balances[recipient] = _balances[recipient].add(amount); + emit Transfer(sender, recipient, amount); + } + + /** @dev Creates `amount` tokens and assigns them to `account`, increasing + * the total supply. + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * Requirements + * + * - `to` cannot be the zero address. + */ + function _mint(address account, uint256 amount) internal { + require(account != address(0), "ERC20: mint to the zero address"); + + _totalSupply = _totalSupply.add(amount); + _balances[account] = _balances[account].add(amount); + emit Transfer(address(0), account, amount); + } + + /** + * @dev Destroys `amount` tokens from `account`, reducing the + * total supply. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * Requirements + * + * - `account` cannot be the zero address. + * - `account` must have at least `amount` tokens. + */ + function _burn(address account, uint256 amount) internal { + require(account != address(0), "ERC20: burn from the zero address"); + + _balances[account] = _balances[account].sub(amount, "ERC20: burn amount exceeds balance"); + _totalSupply = _totalSupply.sub(amount); + emit Transfer(account, address(0), amount); + } + + /** + * @dev Sets `amount` as the allowance of `spender` over the `owner`s tokens. + * + * This is internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + */ + function _approve( + address owner, + address spender, + uint256 amount + ) internal { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } + + /** + * @dev Destroys `amount` tokens from `account`.`amount` is then deducted + * from the caller's allowance. + * + * See {_burn} and {_approve}. + */ + function _burnFrom(address account, uint256 amount) internal { + _burn(account, amount); + _approve( + account, + _msgSender(), + _allowances[account][_msgSender()].sub(amount, "ERC20: burn amount exceeds allowance") + ); + } } diff --git a/contracts/openzeppelin/ERC20Detailed.sol b/contracts/openzeppelin/ERC20Detailed.sol index 0afdbc530..3cbb38a1b 100644 --- a/contracts/openzeppelin/ERC20Detailed.sol +++ b/contracts/openzeppelin/ERC20Detailed.sol @@ -6,53 +6,53 @@ import "./IERC20_.sol"; * @dev Optional functions from the ERC20 standard. */ contract ERC20Detailed is IERC20_ { - string private _name; - string private _symbol; - uint8 private _decimals; + string private _name; + string private _symbol; + uint8 private _decimals; - /** - * @dev Sets the values for `name`, `symbol`, and `decimals`. All three of - * these values are immutable: they can only be set once during - * construction. - */ - constructor( - string memory name, - string memory symbol, - uint8 decimals - ) public { - _name = name; - _symbol = symbol; - _decimals = decimals; - } + /** + * @dev Sets the values for `name`, `symbol`, and `decimals`. All three of + * these values are immutable: they can only be set once during + * construction. + */ + constructor( + string memory name, + string memory symbol, + uint8 decimals + ) public { + _name = name; + _symbol = symbol; + _decimals = decimals; + } - /** - * @dev Returns the name of the token. - */ - function name() public view returns (string memory) { - return _name; - } + /** + * @dev Returns the name of the token. + */ + function name() public view returns (string memory) { + return _name; + } - /** - * @dev Returns the symbol of the token, usually a shorter version of the - * name. - */ - function symbol() public view returns (string memory) { - return _symbol; - } + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public view returns (string memory) { + return _symbol; + } - /** - * @dev Returns the number of decimals used to get its user representation. - * For example, if `decimals` equals `2`, a balance of `505` tokens should - * be displayed to a user as `5,05` (`505 / 10 ** 2`). - * - * Tokens usually opt for a value of 18, imitating the relationship between - * Ether and Wei. - * - * NOTE: This information is only used for _display_ purposes: it in - * no way affects any of the arithmetic of the contract, including - * {IERC20-balanceOf} and {IERC20-transfer}. - */ - function decimals() public view returns (uint8) { - return _decimals; - } + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `2`, a balance of `505` tokens should + * be displayed to a user as `5,05` (`505 / 10 ** 2`). + * + * Tokens usually opt for a value of 18, imitating the relationship between + * Ether and Wei. + * + * NOTE: This information is only used for _display_ purposes: it in + * no way affects any of the arithmetic of the contract, including + * {IERC20-balanceOf} and {IERC20-transfer}. + */ + function decimals() public view returns (uint8) { + return _decimals; + } } diff --git a/contracts/openzeppelin/IERC20_.sol b/contracts/openzeppelin/IERC20_.sol index 6f1c85e76..b8f18794c 100644 --- a/contracts/openzeppelin/IERC20_.sol +++ b/contracts/openzeppelin/IERC20_.sol @@ -5,76 +5,76 @@ pragma solidity ^0.5.0; * the optional functions; to access them see {ERC20Detailed}. */ interface IERC20_ { - /** - * @dev Returns the amount of tokens in existence. - */ - function totalSupply() external view returns (uint256); + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); - /** - * @dev Returns the amount of tokens owned by `account`. - */ - function balanceOf(address account) external view returns (uint256); + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); - /** - * @dev Moves `amount` tokens from the caller's account to `recipient`. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transfer(address recipient, uint256 amount) external returns (bool); + /** + * @dev Moves `amount` tokens from the caller's account to `recipient`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address recipient, uint256 amount) external returns (bool); - /** - * @dev Returns the remaining number of tokens that `spender` will be - * allowed to spend on behalf of `owner` through {transferFrom}. This is - * zero by default. - * - * This value changes when {approve} or {transferFrom} are called. - */ - function allowance(address owner, address spender) external view returns (uint256); + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); - /** - * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * IMPORTANT: Beware that changing an allowance with this method brings the risk - * that someone may use both the old and the new allowance by unfortunate - * transaction ordering. One possible solution to mitigate this race - * condition is to first reduce the spender's allowance to 0 and set the - * desired value afterwards: - * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 - * - * Emits an {Approval} event. - */ - function approve(address spender, uint256 amount) external returns (bool); + /** + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); - /** - * @dev Moves `amount` tokens from `sender` to `recipient` using the - * allowance mechanism. `amount` is then deducted from the caller's - * allowance. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transferFrom( - address sender, - address recipient, - uint256 amount - ) external returns (bool); + /** + * @dev Moves `amount` tokens from `sender` to `recipient` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom( + address sender, + address recipient, + uint256 amount + ) external returns (bool); - /** - * @dev Emitted when `value` tokens are moved from one account (`from`) to - * another (`to`). - * - * Note that `value` may be zero. - */ - event Transfer(address indexed from, address indexed to, uint256 value); + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); - /** - * @dev Emitted when the allowance of a `spender` for an `owner` is set by - * a call to {approve}. `value` is the new allowance. - */ - event Approval(address indexed owner, address indexed spender, uint256 value); + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); } diff --git a/contracts/openzeppelin/Initializable.sol b/contracts/openzeppelin/Initializable.sol index 706082a35..2cd5c4ac9 100644 --- a/contracts/openzeppelin/Initializable.sol +++ b/contracts/openzeppelin/Initializable.sol @@ -13,32 +13,32 @@ pragma solidity >=0.5.0 <0.6.0; * that all initializers are idempotent. This is not verified automatically as constructors are by Solidity. */ contract Initializable { - /** - * @dev Indicates that the contract has been initialized. - */ - bool private _initialized; + /** + * @dev Indicates that the contract has been initialized. + */ + bool private _initialized; - /** - * @dev Indicates that the contract is in the process of being initialized. - */ - bool private _initializing; + /** + * @dev Indicates that the contract is in the process of being initialized. + */ + bool private _initializing; - /** - * @dev Modifier to protect an initializer function from being invoked twice. - */ - modifier initializer() { - require(_initializing || !_initialized, "Initializable: contract is already initialized"); + /** + * @dev Modifier to protect an initializer function from being invoked twice. + */ + modifier initializer() { + require(_initializing || !_initialized, "Initializable: contract is already initialized"); - bool isTopLevelCall = !_initializing; - if (isTopLevelCall) { - _initializing = true; - _initialized = true; - } + bool isTopLevelCall = !_initializing; + if (isTopLevelCall) { + _initializing = true; + _initialized = true; + } - _; + _; - if (isTopLevelCall) { - _initializing = false; - } - } + if (isTopLevelCall) { + _initializing = false; + } + } } diff --git a/contracts/openzeppelin/Ownable.sol b/contracts/openzeppelin/Ownable.sol index 79f1cc687..68d830f90 100644 --- a/contracts/openzeppelin/Ownable.sol +++ b/contracts/openzeppelin/Ownable.sol @@ -12,55 +12,55 @@ import "./Context.sol"; * the owner. */ contract Ownable is Context { - address private _owner; + address private _owner; - event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); - /** - * @dev Initializes the contract setting the deployer as the initial owner. - */ - constructor() internal { - address msgSender = _msgSender(); - _owner = msgSender; - emit OwnershipTransferred(address(0), msgSender); - } + /** + * @dev Initializes the contract setting the deployer as the initial owner. + */ + constructor() internal { + address msgSender = _msgSender(); + _owner = msgSender; + emit OwnershipTransferred(address(0), msgSender); + } - /** - * @dev Returns the address of the current owner. - */ - function owner() public view returns (address) { - return _owner; - } + /** + * @dev Returns the address of the current owner. + */ + function owner() public view returns (address) { + return _owner; + } - /** - * @dev Throws if called by any account other than the owner. - */ - modifier onlyOwner() { - require(isOwner(), "unauthorized"); - _; - } + /** + * @dev Throws if called by any account other than the owner. + */ + modifier onlyOwner() { + require(isOwner(), "unauthorized"); + _; + } - /** - * @dev Returns true if the caller is the current owner. - */ - function isOwner() public view returns (bool) { - return _msgSender() == _owner; - } + /** + * @dev Returns true if the caller is the current owner. + */ + function isOwner() public view returns (bool) { + return _msgSender() == _owner; + } - /** - * @dev Transfers ownership of the contract to a new account (`newOwner`). - * Can only be called by the current owner. - */ - function transferOwnership(address newOwner) public onlyOwner { - _transferOwnership(newOwner); - } + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + * Can only be called by the current owner. + */ + function transferOwnership(address newOwner) public onlyOwner { + _transferOwnership(newOwner); + } - /** - * @dev Transfers ownership of the contract to a new account (`newOwner`). - */ - function _transferOwnership(address newOwner) internal { - require(newOwner != address(0), "Ownable: new owner is the zero address"); - emit OwnershipTransferred(_owner, newOwner); - _owner = newOwner; - } + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + */ + function _transferOwnership(address newOwner) internal { + require(newOwner != address(0), "Ownable: new owner is the zero address"); + emit OwnershipTransferred(_owner, newOwner); + _owner = newOwner; + } } diff --git a/contracts/openzeppelin/PausableOz.sol b/contracts/openzeppelin/PausableOz.sol index 3e43860b1..e53930a7f 100644 --- a/contracts/openzeppelin/PausableOz.sol +++ b/contracts/openzeppelin/PausableOz.sol @@ -3,56 +3,56 @@ pragma solidity 0.5.17; import "./Ownable.sol"; contract PausableOz is Ownable { - /** - * @dev Emitted when the pause is triggered by the owner (`account`). - */ - event Paused(address account); - - /** - * @dev Emitted when the pause is lifted by the owner (`account`). - */ - event Unpaused(address account); - - bool internal _paused; - - constructor() internal {} - - /** - * @dev Returns true if the contract is paused, and false otherwise. - */ - function paused() public view returns (bool) { - return _paused; - } - - /** - * @dev Modifier to make a function callable only when the contract is not paused. - */ - modifier whenNotPaused() { - require(!_paused, "Pausable: paused"); - _; - } - - /** - * @dev Modifier to make a function callable only when the contract is paused. - */ - modifier whenPaused() { - require(_paused, "Pausable: not paused"); - _; - } - - /** - * @dev Called by the owner to pause, triggers stopped state. - */ - function pause() public onlyOwner whenNotPaused { - _paused = true; - emit Paused(_msgSender()); - } - - /** - * @dev Called by the owner to unpause, returns to normal state. - */ - function unpause() public onlyOwner whenPaused { - _paused = false; - emit Unpaused(_msgSender()); - } + /** + * @dev Emitted when the pause is triggered by the owner (`account`). + */ + event Paused(address account); + + /** + * @dev Emitted when the pause is lifted by the owner (`account`). + */ + event Unpaused(address account); + + bool internal _paused; + + constructor() internal {} + + /** + * @dev Returns true if the contract is paused, and false otherwise. + */ + function paused() public view returns (bool) { + return _paused; + } + + /** + * @dev Modifier to make a function callable only when the contract is not paused. + */ + modifier whenNotPaused() { + require(!_paused, "Pausable: paused"); + _; + } + + /** + * @dev Modifier to make a function callable only when the contract is paused. + */ + modifier whenPaused() { + require(_paused, "Pausable: not paused"); + _; + } + + /** + * @dev Called by the owner to pause, triggers stopped state. + */ + function pause() public onlyOwner whenNotPaused { + _paused = true; + emit Paused(_msgSender()); + } + + /** + * @dev Called by the owner to unpause, returns to normal state. + */ + function unpause() public onlyOwner whenPaused { + _paused = false; + emit Unpaused(_msgSender()); + } } diff --git a/contracts/openzeppelin/ReentrancyGuard.sol b/contracts/openzeppelin/ReentrancyGuard.sol index 1b10584a0..11c8e52b4 100644 --- a/contracts/openzeppelin/ReentrancyGuard.sol +++ b/contracts/openzeppelin/ReentrancyGuard.sol @@ -7,30 +7,30 @@ pragma solidity >=0.5.0 <0.6.0; * mark it `external`. */ contract ReentrancyGuard { - /// @dev Constant for unlocked guard state - non-zero to prevent extra gas costs. - /// See: https://github.com/OpenZeppelin/openzeppelin-solidity/issues/1056 - uint256 internal constant REENTRANCY_GUARD_FREE = 1; + /// @dev Constant for unlocked guard state - non-zero to prevent extra gas costs. + /// See: https://github.com/OpenZeppelin/openzeppelin-solidity/issues/1056 + uint256 internal constant REENTRANCY_GUARD_FREE = 1; - /// @dev Constant for locked guard state - uint256 internal constant REENTRANCY_GUARD_LOCKED = 2; + /// @dev Constant for locked guard state + uint256 internal constant REENTRANCY_GUARD_LOCKED = 2; - /** - * @dev We use a single lock for the whole contract. - */ - uint256 internal reentrancyLock = REENTRANCY_GUARD_FREE; + /** + * @dev We use a single lock for the whole contract. + */ + uint256 internal reentrancyLock = REENTRANCY_GUARD_FREE; - /** - * @dev Prevents a contract from calling itself, directly or indirectly. - * If you mark a function `nonReentrant`, you should also - * mark it `external`. Calling one `nonReentrant` function from - * another is not supported. Instead, you can implement a - * `private` function doing the actual work, and an `external` - * wrapper marked as `nonReentrant`. - */ - modifier nonReentrant() { - require(reentrancyLock == REENTRANCY_GUARD_FREE, "nonReentrant"); - reentrancyLock = REENTRANCY_GUARD_LOCKED; - _; - reentrancyLock = REENTRANCY_GUARD_FREE; - } + /** + * @dev Prevents a contract from calling itself, directly or indirectly. + * If you mark a function `nonReentrant`, you should also + * mark it `external`. Calling one `nonReentrant` function from + * another is not supported. Instead, you can implement a + * `private` function doing the actual work, and an `external` + * wrapper marked as `nonReentrant`. + */ + modifier nonReentrant() { + require(reentrancyLock == REENTRANCY_GUARD_FREE, "nonReentrant"); + reentrancyLock = REENTRANCY_GUARD_LOCKED; + _; + reentrancyLock = REENTRANCY_GUARD_FREE; + } } diff --git a/contracts/openzeppelin/SafeERC20.sol b/contracts/openzeppelin/SafeERC20.sol index 92fd0193c..1e94e27d9 100644 --- a/contracts/openzeppelin/SafeERC20.sol +++ b/contracts/openzeppelin/SafeERC20.sol @@ -14,82 +14,98 @@ import "../interfaces/IERC20.sol"; * which allows you to call the safe operations as `token.safeTransfer(...)`, etc. */ library SafeERC20 { - using SafeMath for uint256; - using Address for address; + 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 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 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 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 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, "SafeERC20: decreased allowance below zero"); - 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, + "SafeERC20: decreased allowance below zero" + ); + 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. + /** + * @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"); + // 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"); + // 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"); - } - } + 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/openzeppelin/SafeMath.sol b/contracts/openzeppelin/SafeMath.sol index c94909fbf..4962f8473 100644 --- a/contracts/openzeppelin/SafeMath.sol +++ b/contracts/openzeppelin/SafeMath.sol @@ -14,185 +14,185 @@ pragma solidity >=0.5.0 <0.6.0; * 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) { - return sub(a, b, "SafeMath: subtraction overflow"); - } - - /** - * @dev Returns the subtraction of two unsigned integers, reverting with custom message on - * overflow (when the result is negative). - * - * Counterpart to Solidity's `-` operator. - * - * Requirements: - * - Subtraction cannot overflow. - * - * _Available since v2.4.0._ - */ - function sub( - uint256 a, - uint256 b, - string memory errorMessage - ) internal pure returns (uint256) { - require(b <= a, errorMessage); - 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) { - return div(a, b, "SafeMath: division by zero"); - } - - /** - * @dev Returns the integer division of two unsigned integers. Reverts with custom message 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. - * - * _Available since v2.4.0._ - */ - function div( - uint256 a, - uint256 b, - string memory errorMessage - ) internal pure returns (uint256) { - // Solidity only automatically asserts when dividing by 0 - require(b != 0, errorMessage); - uint256 c = a / b; - // assert(a == b * c + a % b); // There is no case in which this doesn't hold - - return c; - } - - /** - * @dev Integer division of two numbers, rounding up and truncating the quotient - */ - function divCeil(uint256 a, uint256 b) internal pure returns (uint256) { - return divCeil(a, b, "SafeMath: division by zero"); - } - - /** - * @dev Integer division of two numbers, rounding up and truncating the quotient - */ - function divCeil( - uint256 a, - uint256 b, - string memory errorMessage - ) internal pure returns (uint256) { - // Solidity only automatically asserts when dividing by 0 - require(b != 0, errorMessage); - - if (a == 0) { - return 0; - } - uint256 c = ((a - 1) / b) + 1; - - 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) { - return mod(a, b, "SafeMath: modulo by zero"); - } - - /** - * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), - * Reverts with custom message 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. - * - * _Available since v2.4.0._ - */ - function mod( - uint256 a, - uint256 b, - string memory errorMessage - ) internal pure returns (uint256) { - require(b != 0, errorMessage); - return a % b; - } - - function min256(uint256 _a, uint256 _b) internal pure returns (uint256) { - return _a < _b ? _a : _b; - } + /** + * @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) { + return sub(a, b, "SafeMath: subtraction overflow"); + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting with custom message on + * overflow (when the result is negative). + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * - Subtraction cannot overflow. + * + * _Available since v2.4.0._ + */ + function sub( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + require(b <= a, errorMessage); + 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) { + return div(a, b, "SafeMath: division by zero"); + } + + /** + * @dev Returns the integer division of two unsigned integers. Reverts with custom message 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. + * + * _Available since v2.4.0._ + */ + function div( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + // Solidity only automatically asserts when dividing by 0 + require(b != 0, errorMessage); + uint256 c = a / b; + // assert(a == b * c + a % b); // There is no case in which this doesn't hold + + return c; + } + + /** + * @dev Integer division of two numbers, rounding up and truncating the quotient + */ + function divCeil(uint256 a, uint256 b) internal pure returns (uint256) { + return divCeil(a, b, "SafeMath: division by zero"); + } + + /** + * @dev Integer division of two numbers, rounding up and truncating the quotient + */ + function divCeil( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + // Solidity only automatically asserts when dividing by 0 + require(b != 0, errorMessage); + + if (a == 0) { + return 0; + } + uint256 c = ((a - 1) / b) + 1; + + 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) { + return mod(a, b, "SafeMath: modulo by zero"); + } + + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * Reverts with custom message 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. + * + * _Available since v2.4.0._ + */ + function mod( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + require(b != 0, errorMessage); + return a % b; + } + + function min256(uint256 _a, uint256 _b) internal pure returns (uint256) { + return _a < _b ? _a : _b; + } } diff --git a/contracts/openzeppelin/SignedSafeMath.sol b/contracts/openzeppelin/SignedSafeMath.sol index 7586c1858..e7e1501a2 100644 --- a/contracts/openzeppelin/SignedSafeMath.sol +++ b/contracts/openzeppelin/SignedSafeMath.sol @@ -5,86 +5,86 @@ pragma solidity >=0.5.0 <0.6.0; * @dev Signed math operations with safety checks that revert on error. */ library SignedSafeMath { - int256 private constant _INT256_MIN = -2**255; + int256 private constant _INT256_MIN = -2**255; - /** - * @dev Returns the multiplication of two signed integers, reverting on - * overflow. - * - * Counterpart to Solidity's `*` operator. - * - * Requirements: - * - * - Multiplication cannot overflow. - */ - function mul(int256 a, int256 b) internal pure returns (int256) { - // 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; - } + /** + * @dev Returns the multiplication of two signed integers, reverting on + * overflow. + * + * Counterpart to Solidity's `*` operator. + * + * Requirements: + * + * - Multiplication cannot overflow. + */ + function mul(int256 a, int256 b) internal pure returns (int256) { + // 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; + } - require(!(a == -1 && b == _INT256_MIN), "SignedSafeMath: multiplication overflow"); + require(!(a == -1 && b == _INT256_MIN), "SignedSafeMath: multiplication overflow"); - int256 c = a * b; - require(c / a == b, "SignedSafeMath: multiplication overflow"); + int256 c = a * b; + require(c / a == b, "SignedSafeMath: multiplication overflow"); - return c; - } + return c; + } - /** - * @dev Returns the integer division of two signed 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(int256 a, int256 b) internal pure returns (int256) { - require(b != 0, "SignedSafeMath: division by zero"); - require(!(b == -1 && a == _INT256_MIN), "SignedSafeMath: division overflow"); + /** + * @dev Returns the integer division of two signed 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(int256 a, int256 b) internal pure returns (int256) { + require(b != 0, "SignedSafeMath: division by zero"); + require(!(b == -1 && a == _INT256_MIN), "SignedSafeMath: division overflow"); - int256 c = a / b; + int256 c = a / b; - return c; - } + return c; + } - /** - * @dev Returns the subtraction of two signed integers, reverting on - * overflow. - * - * Counterpart to Solidity's `-` operator. - * - * Requirements: - * - * - Subtraction cannot overflow. - */ - function sub(int256 a, int256 b) internal pure returns (int256) { - int256 c = a - b; - require((b >= 0 && c <= a) || (b < 0 && c > a), "SignedSafeMath: subtraction overflow"); + /** + * @dev Returns the subtraction of two signed integers, reverting on + * overflow. + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * + * - Subtraction cannot overflow. + */ + function sub(int256 a, int256 b) internal pure returns (int256) { + int256 c = a - b; + require((b >= 0 && c <= a) || (b < 0 && c > a), "SignedSafeMath: subtraction overflow"); - return c; - } + return c; + } - /** - * @dev Returns the addition of two signed integers, reverting on - * overflow. - * - * Counterpart to Solidity's `+` operator. - * - * Requirements: - * - * - Addition cannot overflow. - */ - function add(int256 a, int256 b) internal pure returns (int256) { - int256 c = a + b; - require((b >= 0 && c >= a) || (b < 0 && c < a), "SignedSafeMath: addition overflow"); + /** + * @dev Returns the addition of two signed integers, reverting on + * overflow. + * + * Counterpart to Solidity's `+` operator. + * + * Requirements: + * + * - Addition cannot overflow. + */ + function add(int256 a, int256 b) internal pure returns (int256) { + int256 c = a + b; + require((b >= 0 && c >= a) || (b < 0 && c < a), "SignedSafeMath: addition overflow"); - return c; - } + return c; + } } diff --git a/contracts/proxy/Proxy.sol b/contracts/proxy/Proxy.sol index 72a960d7c..b82800b44 100644 --- a/contracts/proxy/Proxy.sol +++ b/contracts/proxy/Proxy.sol @@ -26,100 +26,103 @@ pragma solidity ^0.5.17; * functions. * */ contract Proxy { - bytes32 private constant KEY_IMPLEMENTATION = keccak256("key.implementation"); - bytes32 private constant KEY_OWNER = keccak256("key.proxy.owner"); + bytes32 private constant KEY_IMPLEMENTATION = keccak256("key.implementation"); + bytes32 private constant KEY_OWNER = keccak256("key.proxy.owner"); - event OwnershipTransferred(address indexed _oldOwner, address indexed _newOwner); - event ImplementationChanged(address indexed _oldImplementation, address indexed _newImplementation); + event OwnershipTransferred(address indexed _oldOwner, address indexed _newOwner); + event ImplementationChanged( + address indexed _oldImplementation, + address indexed _newImplementation + ); - /** - * @notice Set sender as an owner. - * */ - constructor() public { - _setProxyOwner(msg.sender); - } + /** + * @notice Set sender as an owner. + * */ + constructor() public { + _setProxyOwner(msg.sender); + } - /** - * @notice Throw error if called not by an owner. - * */ - modifier onlyProxyOwner() { - require(msg.sender == getProxyOwner(), "Proxy:: access denied"); - _; - } + /** + * @notice Throw error if called not by an owner. + * */ + modifier onlyProxyOwner() { + require(msg.sender == getProxyOwner(), "Proxy:: access denied"); + _; + } - /** - * @notice Set address of the implementation. - * @param _implementation Address of the implementation. - * */ - function _setImplementation(address _implementation) internal { - require(_implementation != address(0), "Proxy::setImplementation: invalid address"); - emit ImplementationChanged(getImplementation(), _implementation); + /** + * @notice Set address of the implementation. + * @param _implementation Address of the implementation. + * */ + function _setImplementation(address _implementation) internal { + require(_implementation != address(0), "Proxy::setImplementation: invalid address"); + emit ImplementationChanged(getImplementation(), _implementation); - bytes32 key = KEY_IMPLEMENTATION; - assembly { - sstore(key, _implementation) - } - } + bytes32 key = KEY_IMPLEMENTATION; + assembly { + sstore(key, _implementation) + } + } - /** - * @notice Return address of the implementation. - * @return Address of the implementation. - * */ - function getImplementation() public view returns (address _implementation) { - bytes32 key = KEY_IMPLEMENTATION; - assembly { - _implementation := sload(key) - } - } + /** + * @notice Return address of the implementation. + * @return Address of the implementation. + * */ + function getImplementation() public view returns (address _implementation) { + bytes32 key = KEY_IMPLEMENTATION; + assembly { + _implementation := sload(key) + } + } - /** - * @notice Set address of the owner. - * @param _owner Address of the owner. - * */ - function _setProxyOwner(address _owner) internal { - require(_owner != address(0), "Proxy::setProxyOwner: invalid address"); - emit OwnershipTransferred(getProxyOwner(), _owner); + /** + * @notice Set address of the owner. + * @param _owner Address of the owner. + * */ + function _setProxyOwner(address _owner) internal { + require(_owner != address(0), "Proxy::setProxyOwner: invalid address"); + emit OwnershipTransferred(getProxyOwner(), _owner); - bytes32 key = KEY_OWNER; - assembly { - sstore(key, _owner) - } - } + bytes32 key = KEY_OWNER; + assembly { + sstore(key, _owner) + } + } - /** - * @notice Return address of the owner. - * @return Address of the owner. - * */ - function getProxyOwner() public view returns (address _owner) { - bytes32 key = KEY_OWNER; - assembly { - _owner := sload(key) - } - } + /** + * @notice Return address of the owner. + * @return Address of the owner. + * */ + function getProxyOwner() public view returns (address _owner) { + bytes32 key = KEY_OWNER; + assembly { + _owner := sload(key) + } + } - /** - * @notice Fallback function performs a delegate call - * to the actual implementation address is pointing this proxy. - * Returns whatever the implementation call returns. - * */ - function() external payable { - address implementation = getImplementation(); - require(implementation != address(0), "Proxy::(): implementation not found"); + /** + * @notice Fallback function performs a delegate call + * to the actual implementation address is pointing this proxy. + * Returns whatever the implementation call returns. + * */ + function() external payable { + address implementation = getImplementation(); + require(implementation != address(0), "Proxy::(): implementation not found"); - assembly { - let pointer := mload(0x40) - calldatacopy(pointer, 0, calldatasize) - let result := delegatecall(gas, implementation, pointer, calldatasize, 0, 0) - let size := returndatasize - returndatacopy(pointer, 0, size) + assembly { + let pointer := mload(0x40) + calldatacopy(pointer, 0, calldatasize) + let result := delegatecall(gas, implementation, pointer, calldatasize, 0, 0) + let size := returndatasize + returndatacopy(pointer, 0, size) - switch result - case 0 { - revert(pointer, size) - } - default { - return(pointer, size) - } - } - } + switch result + case 0 { + revert(pointer, size) + } + default { + return(pointer, size) + } + } + } } diff --git a/contracts/proxy/UpgradableProxy.sol b/contracts/proxy/UpgradableProxy.sol index e87533584..0878b2ca1 100644 --- a/contracts/proxy/UpgradableProxy.sol +++ b/contracts/proxy/UpgradableProxy.sol @@ -18,22 +18,22 @@ import "./Proxy.sol"; * updated to reference the new contract address. * */ contract UpgradableProxy is Proxy { - /** - * @notice Set address of the implementation. - * @dev Wrapper for _setImplementation that exposes the function - * as public for owner to be able to set a new version of the - * contract as current pointing implementation. - * @param _implementation Address of the implementation. - * */ - function setImplementation(address _implementation) public onlyProxyOwner { - _setImplementation(_implementation); - } + /** + * @notice Set address of the implementation. + * @dev Wrapper for _setImplementation that exposes the function + * as public for owner to be able to set a new version of the + * contract as current pointing implementation. + * @param _implementation Address of the implementation. + * */ + function setImplementation(address _implementation) public onlyProxyOwner { + _setImplementation(_implementation); + } - /** - * @notice Set address of the owner. - * @param _owner Address of the owner. - * */ - function setProxyOwner(address _owner) public onlyProxyOwner { - _setProxyOwner(_owner); - } + /** + * @notice Set address of the owner. + * @param _owner Address of the owner. + * */ + function setProxyOwner(address _owner) public onlyProxyOwner { + _setProxyOwner(_owner); + } } diff --git a/contracts/rsk/RSKAddrValidator.sol b/contracts/rsk/RSKAddrValidator.sol index 26703d7d2..e80d3b549 100644 --- a/contracts/rsk/RSKAddrValidator.sol +++ b/contracts/rsk/RSKAddrValidator.sol @@ -2,20 +2,22 @@ pragma solidity ^0.5.17; library RSKAddrValidator { - /* - * @param addr it is an address to check that it does not originates from - * signing with PK = ZERO. RSK has a small difference in which @ZERO_PK_ADDR is - * also an address from PK = ZERO. So we check for both of them. - * */ - function checkPKNotZero(address addr) internal pure returns (bool) { - return (addr != 0xdcc703c0E500B653Ca82273B7BFAd8045D85a470 && addr != address(0)); - } + /* + * @param addr it is an address to check that it does not originates from + * signing with PK = ZERO. RSK has a small difference in which @ZERO_PK_ADDR is + * also an address from PK = ZERO. So we check for both of them. + * */ + function checkPKNotZero(address addr) internal pure returns (bool) { + return (addr != 0xdcc703c0E500B653Ca82273B7BFAd8045D85a470 && addr != address(0)); + } - /* - * Safely compares two addresses, checking they do not originate from - * a zero private key. - * */ - function safeEquals(address addr1, address addr2) internal pure returns (bool) { - return (addr1 == addr2 && addr1 != 0xdcc703c0E500B653Ca82273B7BFAd8045D85a470 && addr1 != address(0)); - } + /* + * Safely compares two addresses, checking they do not originate from + * a zero private key. + * */ + function safeEquals(address addr1, address addr2) internal pure returns (bool) { + return (addr1 == addr2 && + addr1 != 0xdcc703c0E500B653Ca82273B7BFAd8045D85a470 && + addr1 != address(0)); + } } diff --git a/contracts/swaps/ISwapsImpl.sol b/contracts/swaps/ISwapsImpl.sol index 84f9a29e5..0601ce87f 100644 --- a/contracts/swaps/ISwapsImpl.sol +++ b/contracts/swaps/ISwapsImpl.sol @@ -6,27 +6,27 @@ pragma solidity 0.5.17; interface ISwapsImpl { - function internalSwap( - address sourceTokenAddress, - address destTokenAddress, - address receiverAddress, - address returnToSenderAddress, - uint256 minSourceTokenAmount, - uint256 maxSourceTokenAmount, - uint256 requiredDestTokenAmount - ) external payable returns (uint256 destTokenAmountReceived, uint256 sourceTokenAmountUsed); + function internalSwap( + address sourceTokenAddress, + address destTokenAddress, + address receiverAddress, + address returnToSenderAddress, + uint256 minSourceTokenAmount, + uint256 maxSourceTokenAmount, + uint256 requiredDestTokenAmount + ) external payable returns (uint256 destTokenAmountReceived, uint256 sourceTokenAmountUsed); - function internalExpectedRate( - address sourceTokenAddress, - address destTokenAddress, - uint256 sourceTokenAmount, - address optionalContractAddress - ) external view returns (uint256); + function internalExpectedRate( + address sourceTokenAddress, + address destTokenAddress, + uint256 sourceTokenAmount, + address optionalContractAddress + ) external view returns (uint256); - function internalExpectedReturn( - address sourceTokenAddress, - address destTokenAddress, - uint256 sourceTokenAmount, - address sovrynSwapContractRegistryAddress - ) external view returns (uint256 expectedReturn); + function internalExpectedReturn( + address sourceTokenAddress, + address destTokenAddress, + uint256 sourceTokenAmount, + address sovrynSwapContractRegistryAddress + ) external view returns (uint256 expectedReturn); } diff --git a/contracts/swaps/SwapsUser.sol b/contracts/swaps/SwapsUser.sol index 757976a23..8fd65f3cc 100644 --- a/contracts/swaps/SwapsUser.sol +++ b/contracts/swaps/SwapsUser.sol @@ -15,265 +15,272 @@ import "./ISwapsImpl.sol"; * @title Perform token swaps for loans and trades. * */ contract SwapsUser is State, SwapsEvents, FeesHelper { - /** - * @notice Internal loan swap. - * - * @param loanId The ID of the loan. - * @param sourceToken The address of the source tokens. - * @param destToken The address of destiny tokens. - * @param user The user address. - * @param minSourceTokenAmount The minimum amount of source tokens to swap. - * @param maxSourceTokenAmount The maximum amount of source tokens to swap. - * @param requiredDestTokenAmount The required amount of destination tokens. - * @param bypassFee To bypass or not the fee. - * @param loanDataBytes The payload for the call. These loan DataBytes are - * additional loan data (not in use for token swaps). - * - * @return destTokenAmountReceived - * @return sourceTokenAmountUsed - * @return sourceToDestSwapRate - * */ - function _loanSwap( - bytes32 loanId, - address sourceToken, - address destToken, - address user, - uint256 minSourceTokenAmount, - uint256 maxSourceTokenAmount, - uint256 requiredDestTokenAmount, - bool bypassFee, - bytes memory loanDataBytes - ) - internal - returns ( - uint256 destTokenAmountReceived, - uint256 sourceTokenAmountUsed, - uint256 sourceToDestSwapRate - ) - { - (destTokenAmountReceived, sourceTokenAmountUsed) = _swapsCall( - [ - sourceToken, - destToken, - address(this), // receiver - address(this), // returnToSender - user - ], - [minSourceTokenAmount, maxSourceTokenAmount, requiredDestTokenAmount], - loanId, - bypassFee, - loanDataBytes, - false // swap external flag, set to false so that it will use the tradingFeePercent - ); + /** + * @notice Internal loan swap. + * + * @param loanId The ID of the loan. + * @param sourceToken The address of the source tokens. + * @param destToken The address of destiny tokens. + * @param user The user address. + * @param minSourceTokenAmount The minimum amount of source tokens to swap. + * @param maxSourceTokenAmount The maximum amount of source tokens to swap. + * @param requiredDestTokenAmount The required amount of destination tokens. + * @param bypassFee To bypass or not the fee. + * @param loanDataBytes The payload for the call. These loan DataBytes are + * additional loan data (not in use for token swaps). + * + * @return destTokenAmountReceived + * @return sourceTokenAmountUsed + * @return sourceToDestSwapRate + * */ + function _loanSwap( + bytes32 loanId, + address sourceToken, + address destToken, + address user, + uint256 minSourceTokenAmount, + uint256 maxSourceTokenAmount, + uint256 requiredDestTokenAmount, + bool bypassFee, + bytes memory loanDataBytes + ) + internal + returns ( + uint256 destTokenAmountReceived, + uint256 sourceTokenAmountUsed, + uint256 sourceToDestSwapRate + ) + { + (destTokenAmountReceived, sourceTokenAmountUsed) = _swapsCall( + [ + sourceToken, + destToken, + address(this), // receiver + address(this), // returnToSender + user + ], + [minSourceTokenAmount, maxSourceTokenAmount, requiredDestTokenAmount], + loanId, + bypassFee, + loanDataBytes, + false // swap external flag, set to false so that it will use the tradingFeePercent + ); - /// Will revert if swap size too large. - _checkSwapSize(sourceToken, sourceTokenAmountUsed); + /// Will revert if swap size too large. + _checkSwapSize(sourceToken, sourceTokenAmountUsed); - /// Will revert if disagreement found. - sourceToDestSwapRate = IPriceFeeds(priceFeeds).checkPriceDisagreement( - sourceToken, - destToken, - sourceTokenAmountUsed, - destTokenAmountReceived, - maxDisagreement - ); + /// Will revert if disagreement found. + sourceToDestSwapRate = IPriceFeeds(priceFeeds).checkPriceDisagreement( + sourceToken, + destToken, + sourceTokenAmountUsed, + destTokenAmountReceived, + maxDisagreement + ); - emit LoanSwap(loanId, sourceToken, destToken, user, sourceTokenAmountUsed, destTokenAmountReceived); - } + emit LoanSwap( + loanId, + sourceToken, + destToken, + user, + sourceTokenAmountUsed, + destTokenAmountReceived + ); + } - /** - * @notice Calculate amount of source and destiny tokens. - * - * @dev Wrapper for _swapsCall_internal function. - * - * @param addrs The array of addresses. - * @param vals The array of values. - * @param loanId The Id of the associated loan. - * @param miscBool True/false to bypassFee. - * @param loanDataBytes Additional loan data (not in use yet). - * - * @return destTokenAmountReceived The amount of destiny tokens received. - * @return sourceTokenAmountUsed The amount of source tokens used. - * */ - function _swapsCall( - address[5] memory addrs, - uint256[3] memory vals, - bytes32 loanId, - bool miscBool, /// bypassFee - bytes memory loanDataBytes, - bool isSwapExternal - ) internal returns (uint256, uint256) { - /// addrs[0]: sourceToken - /// addrs[1]: destToken - /// addrs[2]: receiver - /// addrs[3]: returnToSender - /// addrs[4]: user - /// vals[0]: minSourceTokenAmount - /// vals[1]: maxSourceTokenAmount - /// vals[2]: requiredDestTokenAmount + /** + * @notice Calculate amount of source and destiny tokens. + * + * @dev Wrapper for _swapsCall_internal function. + * + * @param addrs The array of addresses. + * @param vals The array of values. + * @param loanId The Id of the associated loan. + * @param miscBool True/false to bypassFee. + * @param loanDataBytes Additional loan data (not in use yet). + * + * @return destTokenAmountReceived The amount of destiny tokens received. + * @return sourceTokenAmountUsed The amount of source tokens used. + * */ + function _swapsCall( + address[5] memory addrs, + uint256[3] memory vals, + bytes32 loanId, + bool miscBool, /// bypassFee + bytes memory loanDataBytes, + bool isSwapExternal + ) internal returns (uint256, uint256) { + /// addrs[0]: sourceToken + /// addrs[1]: destToken + /// addrs[2]: receiver + /// addrs[3]: returnToSender + /// addrs[4]: user + /// vals[0]: minSourceTokenAmount + /// vals[1]: maxSourceTokenAmount + /// vals[2]: requiredDestTokenAmount - require(vals[0] != 0 || vals[1] != 0, "min or max source token amount needs to be set"); + require(vals[0] != 0 || vals[1] != 0, "min or max source token amount needs to be set"); - if (vals[1] == 0) { - vals[1] = vals[0]; - } - require(vals[0] <= vals[1], "sourceAmount larger than max"); + if (vals[1] == 0) { + vals[1] = vals[0]; + } + require(vals[0] <= vals[1], "sourceAmount larger than max"); - uint256 destTokenAmountReceived; - uint256 sourceTokenAmountUsed; + uint256 destTokenAmountReceived; + uint256 sourceTokenAmountUsed; - uint256 tradingFee; - if (!miscBool) { - /// bypassFee - if (vals[2] == 0) { - /// condition: vals[0] will always be used as sourceAmount + uint256 tradingFee; + if (!miscBool) { + /// bypassFee + if (vals[2] == 0) { + /// condition: vals[0] will always be used as sourceAmount - if (isSwapExternal) { - tradingFee = _getSwapExternalFee(vals[0]); - } else { - tradingFee = _getTradingFee(vals[0]); - } + if (isSwapExternal) { + tradingFee = _getSwapExternalFee(vals[0]); + } else { + tradingFee = _getTradingFee(vals[0]); + } - if (tradingFee != 0) { - _payTradingFee( - addrs[4], /// user - loanId, - addrs[0], /// sourceToken (feeToken) - addrs[1], /// pairToken (used to check if there is any special rebates or not) -- to pay fee reward - tradingFee - ); + if (tradingFee != 0) { + _payTradingFee( + addrs[4], /// user + loanId, + addrs[0], /// sourceToken (feeToken) + addrs[1], /// pairToken (used to check if there is any special rebates or not) -- to pay fee reward + tradingFee + ); - vals[0] = vals[0].sub(tradingFee); - } - } else { - /// Condition: unknown sourceAmount will be used. + vals[0] = vals[0].sub(tradingFee); + } + } else { + /// Condition: unknown sourceAmount will be used. - if (isSwapExternal) { - tradingFee = _getSwapExternalFee(vals[2]); - } else { - tradingFee = _getTradingFee(vals[2]); - } + if (isSwapExternal) { + tradingFee = _getSwapExternalFee(vals[2]); + } else { + tradingFee = _getTradingFee(vals[2]); + } - if (tradingFee != 0) { - vals[2] = vals[2].add(tradingFee); - } - } - } + if (tradingFee != 0) { + vals[2] = vals[2].add(tradingFee); + } + } + } - require(loanDataBytes.length == 0, "invalid state"); + require(loanDataBytes.length == 0, "invalid state"); - (destTokenAmountReceived, sourceTokenAmountUsed) = _swapsCall_internal(addrs, vals); + (destTokenAmountReceived, sourceTokenAmountUsed) = _swapsCall_internal(addrs, vals); - if (vals[2] == 0) { - /// There's no minimum destTokenAmount, but all of vals[0] - /// (minSourceTokenAmount) must be spent. - require(sourceTokenAmountUsed == vals[0], "swap too large to fill"); + if (vals[2] == 0) { + /// There's no minimum destTokenAmount, but all of vals[0] + /// (minSourceTokenAmount) must be spent. + require(sourceTokenAmountUsed == vals[0], "swap too large to fill"); - if (tradingFee != 0) { - sourceTokenAmountUsed = sourceTokenAmountUsed.add(tradingFee); - } - } else { - /// There's a minimum destTokenAmount required, but - /// sourceTokenAmountUsed won't be greater - /// than vals[1] (maxSourceTokenAmount) - require(sourceTokenAmountUsed <= vals[1], "swap fill too large"); - require(destTokenAmountReceived >= vals[2], "insufficient swap liquidity"); + if (tradingFee != 0) { + sourceTokenAmountUsed = sourceTokenAmountUsed.add(tradingFee); + } + } else { + /// There's a minimum destTokenAmount required, but + /// sourceTokenAmountUsed won't be greater + /// than vals[1] (maxSourceTokenAmount) + require(sourceTokenAmountUsed <= vals[1], "swap fill too large"); + require(destTokenAmountReceived >= vals[2], "insufficient swap liquidity"); - if (tradingFee != 0) { - _payTradingFee( - addrs[4], /// user - loanId, /// loanId, - addrs[1], /// destToken (feeToken) - addrs[0], /// pairToken (used to check if there is any special rebates or not) -- to pay fee reward - tradingFee - ); + if (tradingFee != 0) { + _payTradingFee( + addrs[4], /// user + loanId, /// loanId, + addrs[1], /// destToken (feeToken) + addrs[0], /// pairToken (used to check if there is any special rebates or not) -- to pay fee reward + tradingFee + ); - destTokenAmountReceived = destTokenAmountReceived.sub(tradingFee); - } - } + destTokenAmountReceived = destTokenAmountReceived.sub(tradingFee); + } + } - return (destTokenAmountReceived, sourceTokenAmountUsed); - } + return (destTokenAmountReceived, sourceTokenAmountUsed); + } - /** - * @notice Calculate amount of source and destiny tokens. - * - * @dev Calls swapsImpl::internalSwap - * - * @param addrs The array of addresses. - * @param vals The array of values. - * - * @return destTokenAmountReceived The amount of destiny tokens received. - * @return sourceTokenAmountUsed The amount of source tokens used. - * */ - function _swapsCall_internal(address[5] memory addrs, uint256[3] memory vals) - internal - returns (uint256 destTokenAmountReceived, uint256 sourceTokenAmountUsed) - { - bytes memory data = - abi.encodeWithSelector( - ISwapsImpl(swapsImpl).internalSwap.selector, - addrs[0], /// sourceToken - addrs[1], /// destToken - addrs[2], /// receiverAddress - addrs[3], /// returnToSenderAddress - vals[0], /// minSourceTokenAmount - vals[1], /// maxSourceTokenAmount - vals[2] /// requiredDestTokenAmount - ); + /** + * @notice Calculate amount of source and destiny tokens. + * + * @dev Calls swapsImpl::internalSwap + * + * @param addrs The array of addresses. + * @param vals The array of values. + * + * @return destTokenAmountReceived The amount of destiny tokens received. + * @return sourceTokenAmountUsed The amount of source tokens used. + * */ + function _swapsCall_internal(address[5] memory addrs, uint256[3] memory vals) + internal + returns (uint256 destTokenAmountReceived, uint256 sourceTokenAmountUsed) + { + bytes memory data = + abi.encodeWithSelector( + ISwapsImpl(swapsImpl).internalSwap.selector, + addrs[0], /// sourceToken + addrs[1], /// destToken + addrs[2], /// receiverAddress + addrs[3], /// returnToSenderAddress + vals[0], /// minSourceTokenAmount + vals[1], /// maxSourceTokenAmount + vals[2] /// requiredDestTokenAmount + ); - bool success; - (success, data) = swapsImpl.delegatecall(data); - require(success, "swap failed"); + bool success; + (success, data) = swapsImpl.delegatecall(data); + require(success, "swap failed"); - assembly { - destTokenAmountReceived := mload(add(data, 32)) - sourceTokenAmountUsed := mload(add(data, 64)) - } - } + assembly { + destTokenAmountReceived := mload(add(data, 32)) + sourceTokenAmountUsed := mload(add(data, 64)) + } + } - /** - * @notice Calculate expected amount of destiny tokens. - * - * @dev Calls swapsImpl::internalExpectedReturn - * - * @param sourceToken The address of the source tokens. - * @param destToken The address of the destiny tokens. - * @param sourceTokenAmount The amount of the source tokens. - * - * @param destTokenAmount The amount of destiny tokens. - * */ - function _swapsExpectedReturn( - address sourceToken, - address destToken, - uint256 sourceTokenAmount - ) internal view returns (uint256 destTokenAmount) { - destTokenAmount = ISwapsImpl(swapsImpl).internalExpectedReturn( - sourceToken, - destToken, - sourceTokenAmount, - sovrynSwapContractRegistryAddress - ); - } + /** + * @notice Calculate expected amount of destiny tokens. + * + * @dev Calls swapsImpl::internalExpectedReturn + * + * @param sourceToken The address of the source tokens. + * @param destToken The address of the destiny tokens. + * @param sourceTokenAmount The amount of the source tokens. + * + * @param destTokenAmount The amount of destiny tokens. + * */ + function _swapsExpectedReturn( + address sourceToken, + address destToken, + uint256 sourceTokenAmount + ) internal view returns (uint256 destTokenAmount) { + destTokenAmount = ISwapsImpl(swapsImpl).internalExpectedReturn( + sourceToken, + destToken, + sourceTokenAmount, + sovrynSwapContractRegistryAddress + ); + } - /** - * @notice Verify that the amount of tokens are under the swap limit. - * - * @dev Calls priceFeeds::amountInEth - * - * @param tokenAddress The address of the token to calculate price. - * @param amount The amount of tokens to calculate price. - * */ - function _checkSwapSize(address tokenAddress, uint256 amount) internal view { - uint256 _maxSwapSize = maxSwapSize; - if (_maxSwapSize != 0) { - uint256 amountInEth; - if (tokenAddress == address(wrbtcToken)) { - amountInEth = amount; - } else { - amountInEth = IPriceFeeds(priceFeeds).amountInEth(tokenAddress, amount); - } - require(amountInEth <= _maxSwapSize, "swap too large"); - } - } + /** + * @notice Verify that the amount of tokens are under the swap limit. + * + * @dev Calls priceFeeds::amountInEth + * + * @param tokenAddress The address of the token to calculate price. + * @param amount The amount of tokens to calculate price. + * */ + function _checkSwapSize(address tokenAddress, uint256 amount) internal view { + uint256 _maxSwapSize = maxSwapSize; + if (_maxSwapSize != 0) { + uint256 amountInEth; + if (tokenAddress == address(wrbtcToken)) { + amountInEth = amount; + } else { + amountInEth = IPriceFeeds(priceFeeds).amountInEth(tokenAddress, amount); + } + require(amountInEth <= _maxSwapSize, "swap too large"); + } + } } diff --git a/contracts/swaps/connectors/SwapsImplSovrynSwap.sol b/contracts/swaps/connectors/SwapsImplSovrynSwap.sol index b799f0eee..7cf5b3c55 100644 --- a/contracts/swaps/connectors/SwapsImplSovrynSwap.sol +++ b/contracts/swaps/connectors/SwapsImplSovrynSwap.sol @@ -17,206 +17,244 @@ import "./interfaces/IContractRegistry.sol"; * calculations for Sovryn network. * */ contract SwapsImplSovrynSwap is State, ISwapsImpl { - using SafeERC20 for IERC20; - - /// bytes32 contractName = hex"42616e636f724e6574776f726b"; /// "SovrynSwapNetwork" - - /** - * Get the hex name of a contract. - * @param source The name of the contract. - * */ - function getContractHexName(string memory source) public pure returns (bytes32 result) { - assembly { - result := mload(add(source, 32)) - } - } - - /** - * Look up the Sovryn swap network contract registered at the given address. - * @param sovrynSwapRegistryAddress The address of the registry. - * */ - function getSovrynSwapNetworkContract(address sovrynSwapRegistryAddress) public view returns (ISovrynSwapNetwork) { - /// State variable sovrynSwapContractRegistryAddress is part of - /// State.sol and set in ProtocolSettings.sol and this function - /// needs to work without delegate call as well -> therefore pass it. - IContractRegistry contractRegistry = IContractRegistry(sovrynSwapRegistryAddress); - return ISovrynSwapNetwork(contractRegistry.addressOf(getContractHexName("SovrynSwapNetwork"))); - } - - /** - * Swap the source token for the destination token on the oracle based AMM. - * On loan opening: minSourceTokenAmount = maxSourceTokenAmount and requiredDestTokenAmount = 0 - * -> swap the minSourceTokenAmount - * On loan rollover: (swap interest) minSourceTokenAmount = 0, maxSourceTokenAmount = complete collateral and requiredDestTokenAmount > 0 - * -> amount of required source tokens to swap is estimated (want to fill requiredDestTokenAmount, not more). maxSourceTokenAMount is not exceeded. - * On loan closure: minSourceTokenAmount <= maxSourceTokenAmount and requiredDestTokenAmount >= 0 - * -> same as on rollover. minimum amount is not considered at all. - * - * @param sourceTokenAddress The address of the source tokens. - * @param destTokenAddress The address of the destination tokens. - * @param receiverAddress The address who will received the swap token results - * @param returnToSenderAddress The address to return unspent tokens to (when called by the protocol, it's always the protocol contract). - * @param minSourceTokenAmount The minimum amount of source tokens to swapped (only considered if requiredDestTokens == 0). - * @param maxSourceTokenAmount The maximum amount of source tokens to swapped. - * @param requiredDestTokenAmount The required amount of destination tokens. - * */ - function internalSwap( - address sourceTokenAddress, - address destTokenAddress, - address receiverAddress, - address returnToSenderAddress, - uint256 minSourceTokenAmount, - uint256 maxSourceTokenAmount, - uint256 requiredDestTokenAmount - ) public payable returns (uint256 destTokenAmountReceived, uint256 sourceTokenAmountUsed) { - require(sourceTokenAddress != destTokenAddress, "source == dest"); - require(supportedTokens[sourceTokenAddress] && supportedTokens[destTokenAddress], "invalid tokens"); - - ISovrynSwapNetwork sovrynSwapNetwork = getSovrynSwapNetworkContract(sovrynSwapContractRegistryAddress); - IERC20[] memory path = sovrynSwapNetwork.conversionPath(IERC20(sourceTokenAddress), IERC20(destTokenAddress)); - - uint256 minReturn = 0; - sourceTokenAmountUsed = minSourceTokenAmount; - - /// If the required amount of destination tokens is passed, we need to - /// calculate the estimated amount of source tokens regardless of the - /// minimum source token amount (name is misleading). - if (requiredDestTokenAmount > 0) { - sourceTokenAmountUsed = estimateSourceTokenAmount( - sourceTokenAddress, - destTokenAddress, - requiredDestTokenAmount, - maxSourceTokenAmount - ); - /// sovrynSwapNetwork.rateByPath does not return a rate, but instead the amount of destination tokens returned. - require( - sovrynSwapNetwork.rateByPath(path, sourceTokenAmountUsed) >= requiredDestTokenAmount, - "insufficient source tokens provided." - ); - minReturn = requiredDestTokenAmount; - } else if (sourceTokenAmountUsed > 0) { - /// For some reason the Sovryn swap network tends to return a bit less than the expected rate. - minReturn = sovrynSwapNetwork.rateByPath(path, sourceTokenAmountUsed).mul(995).div(1000); - } - - require(sourceTokenAmountUsed > 0, "cannot swap 0 tokens"); - - allowTransfer(sourceTokenAmountUsed, sourceTokenAddress, address(sovrynSwapNetwork)); - - /// @dev Note: the kyber connector uses .call() to interact with kyber - /// to avoid bubbling up. here we allow bubbling up. - destTokenAmountReceived = sovrynSwapNetwork.convertByPath(path, sourceTokenAmountUsed, minReturn, receiverAddress, address(0), 0); - - /// If the sender is not the protocol (calling with delegatecall), - /// return the remainder to the specified address. - /// @dev Note: for the case that the swap is used without the - /// protocol. Not sure if it should, though. needs to be discussed. - if (returnToSenderAddress != address(this)) { - if (sourceTokenAmountUsed < maxSourceTokenAmount) { - /// Send unused source token back. - IERC20(sourceTokenAddress).safeTransfer(returnToSenderAddress, maxSourceTokenAmount - sourceTokenAmountUsed); - } - } - } - - /** - * @notice Check whether the existing allowance suffices to transfer - * the needed amount of tokens. - * If not, allows the transfer of an arbitrary amount of tokens. - * - * @param tokenAmount The amount to transfer. - * @param tokenAddress The address of the token to transfer. - * @param sovrynSwapNetwork The address of the sovrynSwap network contract. - * */ - function allowTransfer( - uint256 tokenAmount, - address tokenAddress, - address sovrynSwapNetwork - ) internal { - uint256 tempAllowance = IERC20(tokenAddress).allowance(address(this), sovrynSwapNetwork); - if (tempAllowance < tokenAmount) { - IERC20(tokenAddress).safeApprove(sovrynSwapNetwork, uint256(-1)); - } - } - - /** - * @notice Calculate the number of source tokens to provide in order to - * obtain the required destination amount. - * - * @param sourceTokenAddress The address of the source token address. - * @param destTokenAddress The address of the destination token address. - * @param requiredDestTokenAmount The number of destination tokens needed. - * @param maxSourceTokenAmount The maximum number of source tokens to spend. - * - * @return The estimated amount of source tokens needed. - * Minimum: minSourceTokenAmount, maximum: maxSourceTokenAmount - * */ - function estimateSourceTokenAmount( - address sourceTokenAddress, - address destTokenAddress, - uint256 requiredDestTokenAmount, - uint256 maxSourceTokenAmount - ) internal view returns (uint256 estimatedSourceAmount) { - uint256 sourceToDestPrecision = IPriceFeeds(priceFeeds).queryPrecision(sourceTokenAddress, destTokenAddress); - if (sourceToDestPrecision == 0) return maxSourceTokenAmount; - - /// Compute the expected rate for the maxSourceTokenAmount -> if spending less, we can't get a worse rate. - uint256 expectedRate = - internalExpectedRate(sourceTokenAddress, destTokenAddress, maxSourceTokenAmount, sovrynSwapContractRegistryAddress); - - /// Compute the source tokens needed to get the required amount with the worst case rate. - estimatedSourceAmount = requiredDestTokenAmount.mul(sourceToDestPrecision).div(expectedRate); - - /// If the actual rate is exactly the same as the worst case rate, we get rounding issues. So, add a small buffer. - /// buffer = min(estimatedSourceAmount/1000 , sourceBuffer) with sourceBuffer = 10000 - uint256 buffer = estimatedSourceAmount.div(1000); - if (buffer > sourceBuffer) buffer = sourceBuffer; - estimatedSourceAmount = estimatedSourceAmount.add(buffer); - - /// Never spend more than the maximum. - if (estimatedSourceAmount == 0 || estimatedSourceAmount > maxSourceTokenAmount) return maxSourceTokenAmount; - } - - /** - * @notice Get the expected rate for 1 source token when exchanging the - * given amount of source tokens. - * - * @param sourceTokenAddress The address of the source token contract. - * @param destTokenAddress The address of the destination token contract. - * @param sourceTokenAmount The amount of source tokens to get the rate for. - * */ - function internalExpectedRate( - address sourceTokenAddress, - address destTokenAddress, - uint256 sourceTokenAmount, - address sovrynSwapContractRegistryAddress - ) public view returns (uint256) { - ISovrynSwapNetwork sovrynSwapNetwork = getSovrynSwapNetworkContract(sovrynSwapContractRegistryAddress); - IERC20[] memory path = sovrynSwapNetwork.conversionPath(IERC20(sourceTokenAddress), IERC20(destTokenAddress)); - /// Is returning the total amount of destination tokens. - uint256 expectedReturn = sovrynSwapNetwork.rateByPath(path, sourceTokenAmount); - - /// Return the rate for 1 token with 18 decimals. - return expectedReturn.mul(10**18).div(sourceTokenAmount); - } - - /** - * @notice Get the expected return amount when exchanging the given - * amount of source tokens. - * - * @param sourceTokenAddress The address of the source token contract. - * @param destTokenAddress The address of the destination token contract. - * @param sourceTokenAmount The amount of source tokens to get the return for. - * */ - function internalExpectedReturn( - address sourceTokenAddress, - address destTokenAddress, - uint256 sourceTokenAmount, - address sovrynSwapContractRegistryAddress - ) public view returns (uint256 expectedReturn) { - ISovrynSwapNetwork sovrynSwapNetwork = getSovrynSwapNetworkContract(sovrynSwapContractRegistryAddress); - IERC20[] memory path = sovrynSwapNetwork.conversionPath(IERC20(sourceTokenAddress), IERC20(destTokenAddress)); - /// Is returning the total amount of destination tokens. - expectedReturn = sovrynSwapNetwork.rateByPath(path, sourceTokenAmount); - } + using SafeERC20 for IERC20; + + /// bytes32 contractName = hex"42616e636f724e6574776f726b"; /// "SovrynSwapNetwork" + + /** + * Get the hex name of a contract. + * @param source The name of the contract. + * */ + function getContractHexName(string memory source) public pure returns (bytes32 result) { + assembly { + result := mload(add(source, 32)) + } + } + + /** + * Look up the Sovryn swap network contract registered at the given address. + * @param sovrynSwapRegistryAddress The address of the registry. + * */ + function getSovrynSwapNetworkContract(address sovrynSwapRegistryAddress) + public + view + returns (ISovrynSwapNetwork) + { + /// State variable sovrynSwapContractRegistryAddress is part of + /// State.sol and set in ProtocolSettings.sol and this function + /// needs to work without delegate call as well -> therefore pass it. + IContractRegistry contractRegistry = IContractRegistry(sovrynSwapRegistryAddress); + return + ISovrynSwapNetwork( + contractRegistry.addressOf(getContractHexName("SovrynSwapNetwork")) + ); + } + + /** + * Swap the source token for the destination token on the oracle based AMM. + * On loan opening: minSourceTokenAmount = maxSourceTokenAmount and requiredDestTokenAmount = 0 + * -> swap the minSourceTokenAmount + * On loan rollover: (swap interest) minSourceTokenAmount = 0, maxSourceTokenAmount = complete collateral and requiredDestTokenAmount > 0 + * -> amount of required source tokens to swap is estimated (want to fill requiredDestTokenAmount, not more). maxSourceTokenAMount is not exceeded. + * On loan closure: minSourceTokenAmount <= maxSourceTokenAmount and requiredDestTokenAmount >= 0 + * -> same as on rollover. minimum amount is not considered at all. + * + * @param sourceTokenAddress The address of the source tokens. + * @param destTokenAddress The address of the destination tokens. + * @param receiverAddress The address who will received the swap token results + * @param returnToSenderAddress The address to return unspent tokens to (when called by the protocol, it's always the protocol contract). + * @param minSourceTokenAmount The minimum amount of source tokens to swapped (only considered if requiredDestTokens == 0). + * @param maxSourceTokenAmount The maximum amount of source tokens to swapped. + * @param requiredDestTokenAmount The required amount of destination tokens. + * */ + function internalSwap( + address sourceTokenAddress, + address destTokenAddress, + address receiverAddress, + address returnToSenderAddress, + uint256 minSourceTokenAmount, + uint256 maxSourceTokenAmount, + uint256 requiredDestTokenAmount + ) public payable returns (uint256 destTokenAmountReceived, uint256 sourceTokenAmountUsed) { + require(sourceTokenAddress != destTokenAddress, "source == dest"); + require( + supportedTokens[sourceTokenAddress] && supportedTokens[destTokenAddress], + "invalid tokens" + ); + + ISovrynSwapNetwork sovrynSwapNetwork = + getSovrynSwapNetworkContract(sovrynSwapContractRegistryAddress); + IERC20[] memory path = + sovrynSwapNetwork.conversionPath(IERC20(sourceTokenAddress), IERC20(destTokenAddress)); + + uint256 minReturn = 0; + sourceTokenAmountUsed = minSourceTokenAmount; + + /// If the required amount of destination tokens is passed, we need to + /// calculate the estimated amount of source tokens regardless of the + /// minimum source token amount (name is misleading). + if (requiredDestTokenAmount > 0) { + sourceTokenAmountUsed = estimateSourceTokenAmount( + sourceTokenAddress, + destTokenAddress, + requiredDestTokenAmount, + maxSourceTokenAmount + ); + /// sovrynSwapNetwork.rateByPath does not return a rate, but instead the amount of destination tokens returned. + require( + sovrynSwapNetwork.rateByPath(path, sourceTokenAmountUsed) >= + requiredDestTokenAmount, + "insufficient source tokens provided." + ); + minReturn = requiredDestTokenAmount; + } else if (sourceTokenAmountUsed > 0) { + /// For some reason the Sovryn swap network tends to return a bit less than the expected rate. + minReturn = sovrynSwapNetwork.rateByPath(path, sourceTokenAmountUsed).mul(995).div( + 1000 + ); + } + + require(sourceTokenAmountUsed > 0, "cannot swap 0 tokens"); + + allowTransfer(sourceTokenAmountUsed, sourceTokenAddress, address(sovrynSwapNetwork)); + + /// @dev Note: the kyber connector uses .call() to interact with kyber + /// to avoid bubbling up. here we allow bubbling up. + destTokenAmountReceived = sovrynSwapNetwork.convertByPath( + path, + sourceTokenAmountUsed, + minReturn, + receiverAddress, + address(0), + 0 + ); + + /// If the sender is not the protocol (calling with delegatecall), + /// return the remainder to the specified address. + /// @dev Note: for the case that the swap is used without the + /// protocol. Not sure if it should, though. needs to be discussed. + if (returnToSenderAddress != address(this)) { + if (sourceTokenAmountUsed < maxSourceTokenAmount) { + /// Send unused source token back. + IERC20(sourceTokenAddress).safeTransfer( + returnToSenderAddress, + maxSourceTokenAmount - sourceTokenAmountUsed + ); + } + } + } + + /** + * @notice Check whether the existing allowance suffices to transfer + * the needed amount of tokens. + * If not, allows the transfer of an arbitrary amount of tokens. + * + * @param tokenAmount The amount to transfer. + * @param tokenAddress The address of the token to transfer. + * @param sovrynSwapNetwork The address of the sovrynSwap network contract. + * */ + function allowTransfer( + uint256 tokenAmount, + address tokenAddress, + address sovrynSwapNetwork + ) internal { + uint256 tempAllowance = IERC20(tokenAddress).allowance(address(this), sovrynSwapNetwork); + if (tempAllowance < tokenAmount) { + IERC20(tokenAddress).safeApprove(sovrynSwapNetwork, uint256(-1)); + } + } + + /** + * @notice Calculate the number of source tokens to provide in order to + * obtain the required destination amount. + * + * @param sourceTokenAddress The address of the source token address. + * @param destTokenAddress The address of the destination token address. + * @param requiredDestTokenAmount The number of destination tokens needed. + * @param maxSourceTokenAmount The maximum number of source tokens to spend. + * + * @return The estimated amount of source tokens needed. + * Minimum: minSourceTokenAmount, maximum: maxSourceTokenAmount + * */ + function estimateSourceTokenAmount( + address sourceTokenAddress, + address destTokenAddress, + uint256 requiredDestTokenAmount, + uint256 maxSourceTokenAmount + ) internal view returns (uint256 estimatedSourceAmount) { + uint256 sourceToDestPrecision = + IPriceFeeds(priceFeeds).queryPrecision(sourceTokenAddress, destTokenAddress); + if (sourceToDestPrecision == 0) return maxSourceTokenAmount; + + /// Compute the expected rate for the maxSourceTokenAmount -> if spending less, we can't get a worse rate. + uint256 expectedRate = + internalExpectedRate( + sourceTokenAddress, + destTokenAddress, + maxSourceTokenAmount, + sovrynSwapContractRegistryAddress + ); + + /// Compute the source tokens needed to get the required amount with the worst case rate. + estimatedSourceAmount = requiredDestTokenAmount.mul(sourceToDestPrecision).div( + expectedRate + ); + + /// If the actual rate is exactly the same as the worst case rate, we get rounding issues. So, add a small buffer. + /// buffer = min(estimatedSourceAmount/1000 , sourceBuffer) with sourceBuffer = 10000 + uint256 buffer = estimatedSourceAmount.div(1000); + if (buffer > sourceBuffer) buffer = sourceBuffer; + estimatedSourceAmount = estimatedSourceAmount.add(buffer); + + /// Never spend more than the maximum. + if (estimatedSourceAmount == 0 || estimatedSourceAmount > maxSourceTokenAmount) + return maxSourceTokenAmount; + } + + /** + * @notice Get the expected rate for 1 source token when exchanging the + * given amount of source tokens. + * + * @param sourceTokenAddress The address of the source token contract. + * @param destTokenAddress The address of the destination token contract. + * @param sourceTokenAmount The amount of source tokens to get the rate for. + * */ + function internalExpectedRate( + address sourceTokenAddress, + address destTokenAddress, + uint256 sourceTokenAmount, + address sovrynSwapContractRegistryAddress + ) public view returns (uint256) { + ISovrynSwapNetwork sovrynSwapNetwork = + getSovrynSwapNetworkContract(sovrynSwapContractRegistryAddress); + IERC20[] memory path = + sovrynSwapNetwork.conversionPath(IERC20(sourceTokenAddress), IERC20(destTokenAddress)); + /// Is returning the total amount of destination tokens. + uint256 expectedReturn = sovrynSwapNetwork.rateByPath(path, sourceTokenAmount); + + /// Return the rate for 1 token with 18 decimals. + return expectedReturn.mul(10**18).div(sourceTokenAmount); + } + + /** + * @notice Get the expected return amount when exchanging the given + * amount of source tokens. + * + * @param sourceTokenAddress The address of the source token contract. + * @param destTokenAddress The address of the destination token contract. + * @param sourceTokenAmount The amount of source tokens to get the return for. + * */ + function internalExpectedReturn( + address sourceTokenAddress, + address destTokenAddress, + uint256 sourceTokenAmount, + address sovrynSwapContractRegistryAddress + ) public view returns (uint256 expectedReturn) { + ISovrynSwapNetwork sovrynSwapNetwork = + getSovrynSwapNetworkContract(sovrynSwapContractRegistryAddress); + IERC20[] memory path = + sovrynSwapNetwork.conversionPath(IERC20(sourceTokenAddress), IERC20(destTokenAddress)); + /// Is returning the total amount of destination tokens. + expectedReturn = sovrynSwapNetwork.rateByPath(path, sourceTokenAmount); + } } diff --git a/contracts/swaps/connectors/interfaces/IContractRegistry.sol b/contracts/swaps/connectors/interfaces/IContractRegistry.sol index 4dfc19c25..cbd1d8b8c 100644 --- a/contracts/swaps/connectors/interfaces/IContractRegistry.sol +++ b/contracts/swaps/connectors/interfaces/IContractRegistry.sol @@ -1,5 +1,5 @@ pragma solidity 0.5.17; contract IContractRegistry { - function addressOf(bytes32 contractName) public view returns (address); + function addressOf(bytes32 contractName) public view returns (address); } diff --git a/contracts/swaps/connectors/interfaces/ISovrynSwapNetwork.sol b/contracts/swaps/connectors/interfaces/ISovrynSwapNetwork.sol index 44fd1dbe8..802fb70ba 100644 --- a/contracts/swaps/connectors/interfaces/ISovrynSwapNetwork.sol +++ b/contracts/swaps/connectors/interfaces/ISovrynSwapNetwork.sol @@ -3,16 +3,19 @@ pragma solidity >=0.5.8 <=0.5.17; import "../../../interfaces/IERC20.sol"; contract ISovrynSwapNetwork { - function convertByPath( - IERC20[] calldata _path, - uint256 _amount, - uint256 _minReturn, - address _beneficiary, - address _affiliateAccount, - uint256 _affiliateFee - ) external payable returns (uint256); + function convertByPath( + IERC20[] calldata _path, + uint256 _amount, + uint256 _minReturn, + address _beneficiary, + address _affiliateAccount, + uint256 _affiliateFee + ) external payable returns (uint256); - function rateByPath(IERC20[] calldata _path, uint256 _amount) external view returns (uint256); + function rateByPath(IERC20[] calldata _path, uint256 _amount) external view returns (uint256); - function conversionPath(IERC20 _sourceToken, IERC20 _targetToken) external view returns (IERC20[] memory); + function conversionPath(IERC20 _sourceToken, IERC20 _targetToken) + external + view + returns (IERC20[] memory); } diff --git a/contracts/swaps/connectors/testnet/SwapsImplLocal.sol b/contracts/swaps/connectors/testnet/SwapsImplLocal.sol index 6b6d1f4a4..3c8503e20 100644 --- a/contracts/swaps/connectors/testnet/SwapsImplLocal.sol +++ b/contracts/swaps/connectors/testnet/SwapsImplLocal.sol @@ -20,91 +20,97 @@ import "../../../testhelpers/TestToken.sol"; * This contract contains the implementation of swap process and rate calculations. * */ contract SwapsImplLocal is State, ISwapsImpl { - using SafeERC20 for IERC20; + using SafeERC20 for IERC20; - /** - * @notice Swap two tokens. - * - * @param sourceTokenAddress The address of the source tokens. - * @param destTokenAddress The address of the destiny tokens. - * - * @return destTokenAmountReceived The amount of destiny tokens sent. - * @return sourceTokenAmountUsed The amount of source tokens spent. - * */ - function internalSwap( - address sourceTokenAddress, - address destTokenAddress, - address, /*receiverAddress*/ - address returnToSenderAddress, - uint256 minSourceTokenAmount, - uint256 maxSourceTokenAmount, - uint256 requiredDestTokenAmount - ) public payable returns (uint256 destTokenAmountReceived, uint256 sourceTokenAmountUsed) { - require(sourceTokenAddress != destTokenAddress, "source == dest"); + /** + * @notice Swap two tokens. + * + * @param sourceTokenAddress The address of the source tokens. + * @param destTokenAddress The address of the destiny tokens. + * + * @return destTokenAmountReceived The amount of destiny tokens sent. + * @return sourceTokenAmountUsed The amount of source tokens spent. + * */ + function internalSwap( + address sourceTokenAddress, + address destTokenAddress, + address, /*receiverAddress*/ + address returnToSenderAddress, + uint256 minSourceTokenAmount, + uint256 maxSourceTokenAmount, + uint256 requiredDestTokenAmount + ) public payable returns (uint256 destTokenAmountReceived, uint256 sourceTokenAmountUsed) { + require(sourceTokenAddress != destTokenAddress, "source == dest"); - (uint256 tradeRate, uint256 precision) = IPriceFeeds(priceFeeds).queryRate(sourceTokenAddress, destTokenAddress); + (uint256 tradeRate, uint256 precision) = + IPriceFeeds(priceFeeds).queryRate(sourceTokenAddress, destTokenAddress); - if (requiredDestTokenAmount == 0) { - sourceTokenAmountUsed = minSourceTokenAmount; - destTokenAmountReceived = minSourceTokenAmount.mul(tradeRate).div(precision); - } else { - destTokenAmountReceived = requiredDestTokenAmount; - sourceTokenAmountUsed = requiredDestTokenAmount.mul(precision).div(tradeRate); - require(sourceTokenAmountUsed <= minSourceTokenAmount, "destAmount too great"); - } + if (requiredDestTokenAmount == 0) { + sourceTokenAmountUsed = minSourceTokenAmount; + destTokenAmountReceived = minSourceTokenAmount.mul(tradeRate).div(precision); + } else { + destTokenAmountReceived = requiredDestTokenAmount; + sourceTokenAmountUsed = requiredDestTokenAmount.mul(precision).div(tradeRate); + require(sourceTokenAmountUsed <= minSourceTokenAmount, "destAmount too great"); + } - TestToken(sourceTokenAddress).burn(address(this), sourceTokenAmountUsed); - TestToken(destTokenAddress).mint(address(this), destTokenAmountReceived); + TestToken(sourceTokenAddress).burn(address(this), sourceTokenAmountUsed); + TestToken(destTokenAddress).mint(address(this), destTokenAmountReceived); - if (returnToSenderAddress != address(this)) { - if (sourceTokenAmountUsed < maxSourceTokenAmount) { - /// Send unused source token back. - IERC20(sourceTokenAddress).safeTransfer(returnToSenderAddress, maxSourceTokenAmount - sourceTokenAmountUsed); - } - } - } + if (returnToSenderAddress != address(this)) { + if (sourceTokenAmountUsed < maxSourceTokenAmount) { + /// Send unused source token back. + IERC20(sourceTokenAddress).safeTransfer( + returnToSenderAddress, + maxSourceTokenAmount - sourceTokenAmountUsed + ); + } + } + } - /** - * @notice Calculate the expected price rate of swapping a given amount - * of tokens. - * - * @param sourceTokenAddress The address of the source tokens. - * @param destTokenAddress The address of the destiny tokens. - * @param sourceTokenAmount The amount of source tokens. - * @param unused Fourth parameter ignored. - * - * @return precision The expected price rate. - * */ - function internalExpectedRate( - address sourceTokenAddress, - address destTokenAddress, - uint256 sourceTokenAmount, - address unused - ) public view returns (uint256) { - (uint256 sourceToDestRate, uint256 sourceToDestPrecision) = IPriceFeeds(priceFeeds).queryRate(sourceTokenAddress, destTokenAddress); + /** + * @notice Calculate the expected price rate of swapping a given amount + * of tokens. + * + * @param sourceTokenAddress The address of the source tokens. + * @param destTokenAddress The address of the destiny tokens. + * @param sourceTokenAmount The amount of source tokens. + * @param unused Fourth parameter ignored. + * + * @return precision The expected price rate. + * */ + function internalExpectedRate( + address sourceTokenAddress, + address destTokenAddress, + uint256 sourceTokenAmount, + address unused + ) public view returns (uint256) { + (uint256 sourceToDestRate, uint256 sourceToDestPrecision) = + IPriceFeeds(priceFeeds).queryRate(sourceTokenAddress, destTokenAddress); - return sourceTokenAmount.mul(sourceToDestRate).div(sourceToDestPrecision); - } + return sourceTokenAmount.mul(sourceToDestRate).div(sourceToDestPrecision); + } - /** - * @notice Calculate the expected return of swapping a given amount - * of tokens. - * - * @param sourceTokenAddress The address of the source tokens. - * @param destTokenAddress The address of the destiny tokens. - * @param sourceTokenAmount The amount of source tokens. - * @param unused Fourth parameter ignored. - * - * @return precision The expected return. - * */ - function internalExpectedReturn( - address sourceTokenAddress, - address destTokenAddress, - uint256 sourceTokenAmount, - address unused - ) public view returns (uint256) { - (uint256 sourceToDestRate, uint256 sourceToDestPrecision) = IPriceFeeds(priceFeeds).queryRate(sourceTokenAddress, destTokenAddress); + /** + * @notice Calculate the expected return of swapping a given amount + * of tokens. + * + * @param sourceTokenAddress The address of the source tokens. + * @param destTokenAddress The address of the destiny tokens. + * @param sourceTokenAmount The amount of source tokens. + * @param unused Fourth parameter ignored. + * + * @return precision The expected return. + * */ + function internalExpectedReturn( + address sourceTokenAddress, + address destTokenAddress, + uint256 sourceTokenAmount, + address unused + ) public view returns (uint256) { + (uint256 sourceToDestRate, uint256 sourceToDestPrecision) = + IPriceFeeds(priceFeeds).queryRate(sourceTokenAddress, destTokenAddress); - return sourceTokenAmount.mul(sourceToDestRate).div(sourceToDestPrecision); - } + return sourceTokenAmount.mul(sourceToDestRate).div(sourceToDestPrecision); + } } diff --git a/contracts/testhelpers/FlashLoanerTest.sol b/contracts/testhelpers/FlashLoanerTest.sol index 37990fb04..65a8af207 100644 --- a/contracts/testhelpers/FlashLoanerTest.sol +++ b/contracts/testhelpers/FlashLoanerTest.sol @@ -7,68 +7,77 @@ import "../openzeppelin/Ownable.sol"; import "./ITokenFlashLoanTest.sol"; contract FlashLoanerTest is Ownable { - function initiateFlashLoanTest( - address loanToken, - address iToken, - uint256 flashLoanAmount - ) internal returns (bytes memory success) { - ITokenFlashLoanTest iTokenContract = ITokenFlashLoanTest(iToken); - return - iTokenContract.flashBorrow( - flashLoanAmount, - address(this), - address(this), - "", - abi.encodeWithSignature("executeOperation(address,address,uint256)", loanToken, iToken, flashLoanAmount) - ); - } + function initiateFlashLoanTest( + address loanToken, + address iToken, + uint256 flashLoanAmount + ) internal returns (bytes memory success) { + ITokenFlashLoanTest iTokenContract = ITokenFlashLoanTest(iToken); + return + iTokenContract.flashBorrow( + flashLoanAmount, + address(this), + address(this), + "", + abi.encodeWithSignature( + "executeOperation(address,address,uint256)", + loanToken, + iToken, + flashLoanAmount + ) + ); + } - function repayFlashLoan( - address loanToken, - address iToken, - uint256 loanAmount - ) internal { - IERC20(loanToken).transfer(iToken, loanAmount); - } + function repayFlashLoan( + address loanToken, + address iToken, + uint256 loanAmount + ) internal { + IERC20(loanToken).transfer(iToken, loanAmount); + } - function executeOperation( - address loanToken, - address iToken, - uint256 loanAmount - ) external returns (bytes memory success) { - emit BalanceOf(IERC20(loanToken).balanceOf(address(this))); - emit ExecuteOperation(loanToken, iToken, loanAmount); - repayFlashLoan(loanToken, iToken, loanAmount); - return bytes("1"); - } + function executeOperation( + address loanToken, + address iToken, + uint256 loanAmount + ) external returns (bytes memory success) { + emit BalanceOf(IERC20(loanToken).balanceOf(address(this))); + emit ExecuteOperation(loanToken, iToken, loanAmount); + repayFlashLoan(loanToken, iToken, loanAmount); + return bytes("1"); + } - function doStuffWithFlashLoan( - address token, - address iToken, - uint256 amount - ) external onlyOwner { - bytes memory result; - emit BalanceOf(IERC20(token).balanceOf(address(this))); + function doStuffWithFlashLoan( + address token, + address iToken, + uint256 amount + ) external onlyOwner { + bytes memory result; + emit BalanceOf(IERC20(token).balanceOf(address(this))); - result = initiateFlashLoanTest(token, iToken, amount); + result = initiateFlashLoanTest(token, iToken, amount); - emit BalanceOf(IERC20(token).balanceOf(address(this))); + emit BalanceOf(IERC20(token).balanceOf(address(this))); - // after loan checks and what not. - if (hashCompareWithLengthCheck(bytes("1"), result)) { - revert("failed executeOperation"); - } - } + // after loan checks and what not. + if (hashCompareWithLengthCheck(bytes("1"), result)) { + revert("failed executeOperation"); + } + } - function hashCompareWithLengthCheck(bytes memory a, bytes memory b) internal pure returns (bool) { - if (a.length != b.length) { - return false; - } else { - return keccak256(a) == keccak256(b); - } - } + function hashCompareWithLengthCheck(bytes memory a, bytes memory b) + internal + pure + returns (bool) + { + if (a.length != b.length) { + return false; + } else { + return keccak256(a) == keccak256(b); + } + } - event ExecuteOperation(address loanToken, address iToken, uint256 loanAmount); + event ExecuteOperation(address loanToken, address iToken, uint256 loanAmount); - event BalanceOf(uint256 balance); + event BalanceOf(uint256 balance); } diff --git a/contracts/testhelpers/ITokenFlashLoanTest.sol b/contracts/testhelpers/ITokenFlashLoanTest.sol index b9920703a..4a767fd97 100644 --- a/contracts/testhelpers/ITokenFlashLoanTest.sol +++ b/contracts/testhelpers/ITokenFlashLoanTest.sol @@ -4,11 +4,11 @@ pragma experimental ABIEncoderV2; // "SPDX-License-Identifier: Apache-2.0" interface ITokenFlashLoanTest { - function flashBorrow( - uint256 borrowAmount, - address borrower, - address target, - string calldata signature, - bytes calldata data - ) external payable returns (bytes memory); + function flashBorrow( + uint256 borrowAmount, + address borrower, + address target, + string calldata signature, + bytes calldata data + ) external payable returns (bytes memory); } diff --git a/contracts/testhelpers/LoanTokenLogicTest.sol b/contracts/testhelpers/LoanTokenLogicTest.sol index 8825754fb..91d2ed65b 100644 --- a/contracts/testhelpers/LoanTokenLogicTest.sol +++ b/contracts/testhelpers/LoanTokenLogicTest.sol @@ -4,7 +4,11 @@ pragma experimental ABIEncoderV2; import "../connectors/loantoken/modules/beaconLogicLM/LoanTokenLogicLM.sol"; contract LoanTokenLogicTest is LoanTokenLogicLM { - function getMarginBorrowAmountAndRate(uint256 leverageAmount, uint256 depositAmount) public view returns (uint256, uint256) { - return _getMarginBorrowAmountAndRate(leverageAmount, depositAmount); - } + function getMarginBorrowAmountAndRate(uint256 leverageAmount, uint256 depositAmount) + public + view + returns (uint256, uint256) + { + return _getMarginBorrowAmountAndRate(leverageAmount, depositAmount); + } } diff --git a/contracts/testhelpers/TestCoverage.sol b/contracts/testhelpers/TestCoverage.sol index dc0536d45..85b804449 100644 --- a/contracts/testhelpers/TestCoverage.sol +++ b/contracts/testhelpers/TestCoverage.sol @@ -12,113 +12,119 @@ import "../mixins/VaultController.sol"; import "../connectors/loantoken/AdvancedToken.sol"; import "../connectors/loantoken/LoanTokenLogicStorage.sol"; -contract TestCoverage is Pausable, SafeMath96, VaultController, AdvancedToken, LoanTokenLogicStorage { - /// @dev Pausable is currently an unused contract that still is operative - /// because margin trade flashloan functionality has been commented out. - /// In case it were restored, contract would become used again, so for a - /// complete test coverage it is required to test it. - - function dummyPausableFunction() external pausable(msg.sig) { - /// @dev do nothing, just to check if modifier is working - } - - /// @dev This function should be located on Pausable contract in the case - /// it has to be used again by flashloan restoration. - function togglePause( - string memory funcId, // example: "mint(uint256,uint256)" - bool isPaused - ) public { - /// keccak256("Pausable_FunctionPause") - bytes32 slot = - keccak256( - abi.encodePacked( - bytes4(keccak256(abi.encodePacked(funcId))), - uint256(0xa7143c84d793a15503da6f19bf9119a2dac94448ca45d77c8bf08f57b2e91047) - ) - ); - - // solhint-disable-next-line no-inline-assembly - assembly { - sstore(slot, isPaused) - } - } - - /// @dev Testing internal functions of governance/Staking/SafeMath96.sol - function testSafeMath96_safe32(uint256 n) public pure returns (uint32) { - // Public wrapper for SafeMath96 internal function - return safe32(n, "overflow"); - } - - function testSafeMath96_safe64(uint256 n) public pure returns (uint64) { - // Public wrapper for SafeMath96 internal function - return safe64(n, "overflow"); - } - - function testSafeMath96_safe96(uint256 n) public pure returns (uint96) { - // Public wrapper for SafeMath96 internal function - return safe96(n, "overflow"); - } - - function testSafeMath96_sub96(uint96 a, uint96 b) public pure returns (uint96) { - // Public wrapper for SafeMath96 internal function - return sub96(a, b, "underflow"); - } - - function testSafeMath96_mul96(uint96 a, uint96 b) public pure returns (uint96) { - // Public wrapper for SafeMath96 internal function - return mul96(a, b, "overflow"); - } - - function testSafeMath96_div96(uint96 a, uint96 b) public pure returns (uint96) { - // Public wrapper for SafeMath96 internal function - return div96(a, b, "division by 0"); - } - - using EnumerableBytes32Set for EnumerableBytes32Set.Bytes32Set; - EnumerableBytes32Set.Bytes32Set internal aSet; - - function testEnum_AddRemove(bytes32 a, bytes32 b) public returns (bool) { - aSet.addBytes32(a); - return aSet.removeBytes32(b); - } - - function testEnum_AddAddress(address a, address b) public returns (bool) { - aSet.addAddress(a); - return aSet.containsAddress(b); - } - - function testEnum_AddAddressesAndEnumerate( - address a, - address b, - uint256 start, - uint256 count - ) public returns (bytes32[] memory) { - aSet.addAddress(a); - aSet.addAddress(b); - return aSet.enumerate(start, count); - } - - /// @dev Wrapper to test internal function never called along current codebase - function testVaultController_vaultApprove( - address token, - address to, - uint256 value - ) public { - vaultApprove(token, to, value); - } - - /// @dev mint wrapper w/o previous checks - function testMint( - address _to, - uint256 _tokenAmount, - uint256 _assetAmount, - uint256 _price - ) public { - _mint(_to, _tokenAmount, _assetAmount, _price); - } - - /// @dev wrapper for a function unreachable to tests - function testStringToBytes32(string memory source) public pure returns (bytes32 result) { - return stringToBytes32(source); - } +contract TestCoverage is + Pausable, + SafeMath96, + VaultController, + AdvancedToken, + LoanTokenLogicStorage +{ + /// @dev Pausable is currently an unused contract that still is operative + /// because margin trade flashloan functionality has been commented out. + /// In case it were restored, contract would become used again, so for a + /// complete test coverage it is required to test it. + + function dummyPausableFunction() external pausable(msg.sig) { + /// @dev do nothing, just to check if modifier is working + } + + /// @dev This function should be located on Pausable contract in the case + /// it has to be used again by flashloan restoration. + function togglePause( + string memory funcId, // example: "mint(uint256,uint256)" + bool isPaused + ) public { + /// keccak256("Pausable_FunctionPause") + bytes32 slot = + keccak256( + abi.encodePacked( + bytes4(keccak256(abi.encodePacked(funcId))), + uint256(0xa7143c84d793a15503da6f19bf9119a2dac94448ca45d77c8bf08f57b2e91047) + ) + ); + + // solhint-disable-next-line no-inline-assembly + assembly { + sstore(slot, isPaused) + } + } + + /// @dev Testing internal functions of governance/Staking/SafeMath96.sol + function testSafeMath96_safe32(uint256 n) public pure returns (uint32) { + // Public wrapper for SafeMath96 internal function + return safe32(n, "overflow"); + } + + function testSafeMath96_safe64(uint256 n) public pure returns (uint64) { + // Public wrapper for SafeMath96 internal function + return safe64(n, "overflow"); + } + + function testSafeMath96_safe96(uint256 n) public pure returns (uint96) { + // Public wrapper for SafeMath96 internal function + return safe96(n, "overflow"); + } + + function testSafeMath96_sub96(uint96 a, uint96 b) public pure returns (uint96) { + // Public wrapper for SafeMath96 internal function + return sub96(a, b, "underflow"); + } + + function testSafeMath96_mul96(uint96 a, uint96 b) public pure returns (uint96) { + // Public wrapper for SafeMath96 internal function + return mul96(a, b, "overflow"); + } + + function testSafeMath96_div96(uint96 a, uint96 b) public pure returns (uint96) { + // Public wrapper for SafeMath96 internal function + return div96(a, b, "division by 0"); + } + + using EnumerableBytes32Set for EnumerableBytes32Set.Bytes32Set; + EnumerableBytes32Set.Bytes32Set internal aSet; + + function testEnum_AddRemove(bytes32 a, bytes32 b) public returns (bool) { + aSet.addBytes32(a); + return aSet.removeBytes32(b); + } + + function testEnum_AddAddress(address a, address b) public returns (bool) { + aSet.addAddress(a); + return aSet.containsAddress(b); + } + + function testEnum_AddAddressesAndEnumerate( + address a, + address b, + uint256 start, + uint256 count + ) public returns (bytes32[] memory) { + aSet.addAddress(a); + aSet.addAddress(b); + return aSet.enumerate(start, count); + } + + /// @dev Wrapper to test internal function never called along current codebase + function testVaultController_vaultApprove( + address token, + address to, + uint256 value + ) public { + vaultApprove(token, to, value); + } + + /// @dev mint wrapper w/o previous checks + function testMint( + address _to, + uint256 _tokenAmount, + uint256 _assetAmount, + uint256 _price + ) public { + _mint(_to, _tokenAmount, _assetAmount, _price); + } + + /// @dev wrapper for a function unreachable to tests + function testStringToBytes32(string memory source) public pure returns (bytes32 result) { + return stringToBytes32(source); + } } diff --git a/contracts/testhelpers/TestLibraries.sol b/contracts/testhelpers/TestLibraries.sol index 4a9c61c92..866cd1665 100644 --- a/contracts/testhelpers/TestLibraries.sol +++ b/contracts/testhelpers/TestLibraries.sol @@ -4,20 +4,20 @@ import "../rsk/RSKAddrValidator.sol"; // contract for testing libraries contract TestLibraries { - /* - * @param addr it is an address to check that it does not originates from - * signing with PK = ZERO. RSK has a small difference in which @ZERO_PK_ADDR is - * also an address from PK = ZERO. So we check for both of them. - */ - function RSKAddrValidator_checkPKNotZero(address addr) public pure returns (bool) { - return (RSKAddrValidator.checkPKNotZero(addr)); - } + /* + * @param addr it is an address to check that it does not originates from + * signing with PK = ZERO. RSK has a small difference in which @ZERO_PK_ADDR is + * also an address from PK = ZERO. So we check for both of them. + */ + function RSKAddrValidator_checkPKNotZero(address addr) public pure returns (bool) { + return (RSKAddrValidator.checkPKNotZero(addr)); + } - /* - * Safely compares two addresses, checking they do not originate from - * a zero private key - */ - function RSKAddrValidator_safeEquals(address addr1, address addr2) public pure returns (bool) { - return (RSKAddrValidator.safeEquals(addr1, addr2)); - } + /* + * Safely compares two addresses, checking they do not originate from + * a zero private key + */ + function RSKAddrValidator_safeEquals(address addr1, address addr2) public pure returns (bool) { + return (RSKAddrValidator.safeEquals(addr1, addr2)); + } } diff --git a/contracts/testhelpers/TestSovrynSwap.sol b/contracts/testhelpers/TestSovrynSwap.sol index 447549c21..932d91ee5 100644 --- a/contracts/testhelpers/TestSovrynSwap.sol +++ b/contracts/testhelpers/TestSovrynSwap.sol @@ -10,62 +10,68 @@ import "./TestToken.sol"; import "../openzeppelin/SafeMath.sol"; contract TestSovrynSwap { - using SafeERC20 for IERC20; - using SafeMath for uint256; + using SafeERC20 for IERC20; + using SafeMath for uint256; - address public priceFeeds; + address public priceFeeds; - constructor(address feed) public { - priceFeeds = feed; - } + constructor(address feed) public { + priceFeeds = feed; + } - /** - * simulating the contract registry. always returns the address of this contract - * */ - function addressOf(bytes32 contractName) public view returns (address) { - return address(this); - } + /** + * simulating the contract registry. always returns the address of this contract + * */ + function addressOf(bytes32 contractName) public view returns (address) { + return address(this); + } - /** - * calculates the return tokens when swapping _amount, makes sure the return is bigger than _minReturn, - * mints and burns the test tokens accordingly. - * */ - function convertByPath( - IERC20[] calldata _path, - uint256 _amount, - uint256 _minReturn, - address _beneficiary, - address _affiliateAccount, - uint256 _affiliateFee - ) external payable returns (uint256) { - //compute the return for the amount of tokens provided - (uint256 sourceToDestRate, uint256 sourceToDestPrecision) = IPriceFeeds(priceFeeds).queryRate(address(_path[0]), address(_path[1])); - uint256 actualReturn = _amount.mul(sourceToDestRate).div(sourceToDestPrecision); + /** + * calculates the return tokens when swapping _amount, makes sure the return is bigger than _minReturn, + * mints and burns the test tokens accordingly. + * */ + function convertByPath( + IERC20[] calldata _path, + uint256 _amount, + uint256 _minReturn, + address _beneficiary, + address _affiliateAccount, + uint256 _affiliateFee + ) external payable returns (uint256) { + //compute the return for the amount of tokens provided + (uint256 sourceToDestRate, uint256 sourceToDestPrecision) = + IPriceFeeds(priceFeeds).queryRate(address(_path[0]), address(_path[1])); + uint256 actualReturn = _amount.mul(sourceToDestRate).div(sourceToDestPrecision); - require(actualReturn >= _minReturn, "insufficient source tokens provided"); + require(actualReturn >= _minReturn, "insufficient source tokens provided"); - TestToken(address(_path[0])).burn(address(msg.sender), _amount); - TestToken(address(_path[1])).mint(address(_beneficiary), actualReturn); - return actualReturn; - } + TestToken(address(_path[0])).burn(address(msg.sender), _amount); + TestToken(address(_path[1])).mint(address(_beneficiary), actualReturn); + return actualReturn; + } - /** - * queries the rate from the Price Feed contract and computes the expected return amount based on the - * amout of source tokens to be swapped. - * */ - function rateByPath(IERC20[] calldata _path, uint256 _amount) external view returns (uint256) { - (uint256 sourceToDestRate, uint256 sourceToDestPrecision) = IPriceFeeds(priceFeeds).queryRate(address(_path[0]), address(_path[1])); + /** + * queries the rate from the Price Feed contract and computes the expected return amount based on the + * amout of source tokens to be swapped. + * */ + function rateByPath(IERC20[] calldata _path, uint256 _amount) external view returns (uint256) { + (uint256 sourceToDestRate, uint256 sourceToDestPrecision) = + IPriceFeeds(priceFeeds).queryRate(address(_path[0]), address(_path[1])); - return _amount.mul(sourceToDestRate).div(sourceToDestPrecision); - } + return _amount.mul(sourceToDestRate).div(sourceToDestPrecision); + } - /** - * returns the conversion path -> always a direct path - * */ - function conversionPath(IERC20 _sourceToken, IERC20 _targetToken) external view returns (IERC20[] memory) { - IERC20[] memory path = new IERC20[](2); - path[0] = _sourceToken; - path[1] = _targetToken; - return path; - } + /** + * returns the conversion path -> always a direct path + * */ + function conversionPath(IERC20 _sourceToken, IERC20 _targetToken) + external + view + returns (IERC20[] memory) + { + IERC20[] memory path = new IERC20[](2); + path[0] = _sourceToken; + path[1] = _targetToken; + return path; + } } diff --git a/contracts/testhelpers/TestToken.sol b/contracts/testhelpers/TestToken.sol index 009350e09..4b3485e15 100644 --- a/contracts/testhelpers/TestToken.sol +++ b/contracts/testhelpers/TestToken.sol @@ -8,103 +8,111 @@ pragma solidity 0.5.17; import "../openzeppelin/SafeMath.sol"; contract TestToken { - using SafeMath for uint256; - - event Transfer(address indexed from, address indexed to, uint256 value); - event Approval(address indexed owner, address indexed spender, uint256 value); - event AllowanceUpdate(address indexed owner, address indexed spender, uint256 valueBefore, uint256 valueAfter); - event Mint(address indexed minter, uint256 value); - event Burn(address indexed burner, uint256 value); - - string public name; - string public symbol; - uint8 public decimals; - - mapping(address => uint256) internal balances; - mapping(address => mapping(address => uint256)) internal allowed; - uint256 internal totalSupply_; - - constructor( - string memory _name, - string memory _symbol, - uint8 _decimals, - uint256 _initialAmount - ) public { - name = _name; - symbol = _symbol; - decimals = _decimals; - - if (_initialAmount != 0) { - mint(msg.sender, _initialAmount); - } - } - - function approve(address _spender, uint256 _value) public returns (bool) { - allowed[msg.sender][_spender] = _value; - emit Approval(msg.sender, _spender, _value); - return true; - } - - function transfer(address _to, uint256 _value) public returns (bool) { - require(_value <= balances[msg.sender] && _to != address(0), "invalid transfer"); - - balances[msg.sender] = balances[msg.sender].sub(_value); - balances[_to] = balances[_to].add(_value); - - emit Transfer(msg.sender, _to, _value); - return true; - } - - function transferFrom( - address _from, - address _to, - uint256 _value - ) public returns (bool) { - uint256 allowanceAmount = allowed[_from][msg.sender]; - require(_value <= balances[_from] && _value <= allowanceAmount && _to != address(0), "invalid transfer"); - - balances[_from] = balances[_from].sub(_value); - balances[_to] = balances[_to].add(_value); - if (allowanceAmount < uint256(-1)) { - allowed[_from][msg.sender] = allowanceAmount.sub(_value); - /// @dev Allowance mapping update requires an event log - emit AllowanceUpdate(_from, msg.sender, allowanceAmount, allowed[_from][msg.sender]); - } - - emit Transfer(_from, _to, _value); - return true; - } - - function mint(address _to, uint256 _value) public { - require(_to != address(0), "no burn allowed"); - totalSupply_ = totalSupply_.add(_value); - balances[_to] = balances[_to].add(_value); - - emit Mint(_to, _value); - emit Transfer(address(0), _to, _value); - } - - function burn(address _who, uint256 _value) public { - require(_value <= balances[_who], "balance too low"); - // no need to require _value <= totalSupply, since that would imply the - // sender's balance is greater than the totalSupply, which *should* be an assertion failure - - balances[_who] = balances[_who].sub(_value); - totalSupply_ = totalSupply_.sub(_value); - - emit Burn(_who, _value); - emit Transfer(_who, address(0), _value); - } - - function totalSupply() public view returns (uint256) { - return totalSupply_; - } - - function balanceOf(address _owner) public view returns (uint256) { - return balances[_owner]; - } - - function allowance(address _owner, address _spender) public view returns (uint256) { - return allowed[_owner][_spender]; - } + using SafeMath for uint256; + + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); + event AllowanceUpdate( + address indexed owner, + address indexed spender, + uint256 valueBefore, + uint256 valueAfter + ); + event Mint(address indexed minter, uint256 value); + event Burn(address indexed burner, uint256 value); + + string public name; + string public symbol; + uint8 public decimals; + + mapping(address => uint256) internal balances; + mapping(address => mapping(address => uint256)) internal allowed; + uint256 internal totalSupply_; + + constructor( + string memory _name, + string memory _symbol, + uint8 _decimals, + uint256 _initialAmount + ) public { + name = _name; + symbol = _symbol; + decimals = _decimals; + + if (_initialAmount != 0) { + mint(msg.sender, _initialAmount); + } + } + + function approve(address _spender, uint256 _value) public returns (bool) { + allowed[msg.sender][_spender] = _value; + emit Approval(msg.sender, _spender, _value); + return true; + } + + function transfer(address _to, uint256 _value) public returns (bool) { + require(_value <= balances[msg.sender] && _to != address(0), "invalid transfer"); + + balances[msg.sender] = balances[msg.sender].sub(_value); + balances[_to] = balances[_to].add(_value); + + emit Transfer(msg.sender, _to, _value); + return true; + } + + function transferFrom( + address _from, + address _to, + uint256 _value + ) public returns (bool) { + uint256 allowanceAmount = allowed[_from][msg.sender]; + require( + _value <= balances[_from] && _value <= allowanceAmount && _to != address(0), + "invalid transfer" + ); + + balances[_from] = balances[_from].sub(_value); + balances[_to] = balances[_to].add(_value); + if (allowanceAmount < uint256(-1)) { + allowed[_from][msg.sender] = allowanceAmount.sub(_value); + /// @dev Allowance mapping update requires an event log + emit AllowanceUpdate(_from, msg.sender, allowanceAmount, allowed[_from][msg.sender]); + } + + emit Transfer(_from, _to, _value); + return true; + } + + function mint(address _to, uint256 _value) public { + require(_to != address(0), "no burn allowed"); + totalSupply_ = totalSupply_.add(_value); + balances[_to] = balances[_to].add(_value); + + emit Mint(_to, _value); + emit Transfer(address(0), _to, _value); + } + + function burn(address _who, uint256 _value) public { + require(_value <= balances[_who], "balance too low"); + // no need to require _value <= totalSupply, since that would imply the + // sender's balance is greater than the totalSupply, which *should* be an assertion failure + + balances[_who] = balances[_who].sub(_value); + totalSupply_ = totalSupply_.sub(_value); + + emit Burn(_who, _value); + emit Transfer(_who, address(0), _value); + } + + function totalSupply() public view returns (uint256) { + return totalSupply_; + } + + function balanceOf(address _owner) public view returns (uint256) { + return balances[_owner]; + } + + function allowance(address _owner, address _spender) public view returns (uint256) { + return allowed[_owner][_spender]; + } } diff --git a/contracts/testhelpers/TestWrbtc.sol b/contracts/testhelpers/TestWrbtc.sol index 2847b8d04..fd67d186b 100644 --- a/contracts/testhelpers/TestWrbtc.sol +++ b/contracts/testhelpers/TestWrbtc.sol @@ -16,88 +16,88 @@ pragma solidity 0.5.17; contract TestWrbtc { - string public name = "Wrapped BTC"; - string public symbol = "WRBTC"; - uint8 public decimals = 18; - - event Approval(address indexed src, address indexed guy, uint256 wad); - event Transfer(address indexed src, address indexed dst, uint256 wad); - event Deposit(address indexed dst, uint256 wad); - event Withdrawal(address indexed src, uint256 wad); - - mapping(address => uint256) public balanceOf; - mapping(address => mapping(address => uint256)) public allowance; - - function() external payable { - deposit(); - } - - function deposit() public payable { - balanceOf[msg.sender] += msg.value; - emit Deposit(msg.sender, msg.value); - } - - function withdraw(uint256 wad) public { - require(balanceOf[msg.sender] >= wad); - balanceOf[msg.sender] -= wad; - msg.sender.transfer(wad); - emit Withdrawal(msg.sender, wad); - } - - function totalSupply() public view returns (uint256) { - return address(this).balance; - } - - function approve(address guy, uint256 wad) public returns (bool) { - allowance[msg.sender][guy] = wad; - emit Approval(msg.sender, guy, wad); - return true; - } - - function transfer(address dst, uint256 wad) public returns (bool) { - return transferFrom(msg.sender, dst, wad); - } - - function transferFrom( - address src, - address dst, - uint256 wad - ) public returns (bool) { - require(balanceOf[src] >= wad); - - if (src != msg.sender && allowance[src][msg.sender] != uint256(-1)) { - require(allowance[src][msg.sender] >= wad); - allowance[src][msg.sender] -= wad; - } - - balanceOf[src] -= wad; - balanceOf[dst] += wad; - - emit Transfer(src, dst, wad); - - return true; - } - - /** - * added for local swap implementation - * */ - function mint(address _to, uint256 _value) public { - require(_to != address(0), "no burn allowed"); - balanceOf[_to] = balanceOf[_to] + _value; - emit Transfer(address(0), _to, _value); - } - - /** - * added for local swap implementation - * */ - function burn(address _who, uint256 _value) public { - require(_value <= balanceOf[_who], "balance too low"); - // no need to require _value <= totalSupply, since that would imply the - // sender's balance is greater than the totalSupply, which *should* be an assertion failure - - balanceOf[_who] = balanceOf[_who] - _value; - emit Transfer(_who, address(0), _value); - } + string public name = "Wrapped BTC"; + string public symbol = "WRBTC"; + uint8 public decimals = 18; + + event Approval(address indexed src, address indexed guy, uint256 wad); + event Transfer(address indexed src, address indexed dst, uint256 wad); + event Deposit(address indexed dst, uint256 wad); + event Withdrawal(address indexed src, uint256 wad); + + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + + function() external payable { + deposit(); + } + + function deposit() public payable { + balanceOf[msg.sender] += msg.value; + emit Deposit(msg.sender, msg.value); + } + + function withdraw(uint256 wad) public { + require(balanceOf[msg.sender] >= wad); + balanceOf[msg.sender] -= wad; + msg.sender.transfer(wad); + emit Withdrawal(msg.sender, wad); + } + + function totalSupply() public view returns (uint256) { + return address(this).balance; + } + + function approve(address guy, uint256 wad) public returns (bool) { + allowance[msg.sender][guy] = wad; + emit Approval(msg.sender, guy, wad); + return true; + } + + function transfer(address dst, uint256 wad) public returns (bool) { + return transferFrom(msg.sender, dst, wad); + } + + function transferFrom( + address src, + address dst, + uint256 wad + ) public returns (bool) { + require(balanceOf[src] >= wad); + + if (src != msg.sender && allowance[src][msg.sender] != uint256(-1)) { + require(allowance[src][msg.sender] >= wad); + allowance[src][msg.sender] -= wad; + } + + balanceOf[src] -= wad; + balanceOf[dst] += wad; + + emit Transfer(src, dst, wad); + + return true; + } + + /** + * added for local swap implementation + * */ + function mint(address _to, uint256 _value) public { + require(_to != address(0), "no burn allowed"); + balanceOf[_to] = balanceOf[_to] + _value; + emit Transfer(address(0), _to, _value); + } + + /** + * added for local swap implementation + * */ + function burn(address _who, uint256 _value) public { + require(_value <= balanceOf[_who], "balance too low"); + // no need to require _value <= totalSupply, since that would imply the + // sender's balance is greater than the totalSupply, which *should* be an assertion failure + + balanceOf[_who] = balanceOf[_who] - _value; + emit Transfer(_who, address(0), _value); + } } /* diff --git a/contracts/testhelpers/WRBTC.sol b/contracts/testhelpers/WRBTC.sol index 04f23d208..db71222b0 100644 --- a/contracts/testhelpers/WRBTC.sol +++ b/contracts/testhelpers/WRBTC.sol @@ -16,67 +16,67 @@ pragma solidity 0.5.17; contract WRBTC { - string public name = "Wrapped BTC"; - string public symbol = "WRBTC"; - uint8 public decimals = 18; - - event Approval(address indexed src, address indexed guy, uint256 wad); - event Transfer(address indexed src, address indexed dst, uint256 wad); - event Deposit(address indexed dst, uint256 wad); - event Withdrawal(address indexed src, uint256 wad); - - mapping(address => uint256) public balanceOf; - mapping(address => mapping(address => uint256)) public allowance; - - function() external payable { - deposit(); - } - - function deposit() public payable { - balanceOf[msg.sender] += msg.value; - emit Deposit(msg.sender, msg.value); - } - - function withdraw(uint256 wad) public { - require(balanceOf[msg.sender] >= wad); - balanceOf[msg.sender] -= wad; - msg.sender.transfer(wad); - emit Withdrawal(msg.sender, wad); - } - - function totalSupply() public view returns (uint256) { - return address(this).balance; - } - - function approve(address guy, uint256 wad) public returns (bool) { - allowance[msg.sender][guy] = wad; - emit Approval(msg.sender, guy, wad); - return true; - } - - function transfer(address dst, uint256 wad) public returns (bool) { - return transferFrom(msg.sender, dst, wad); - } - - function transferFrom( - address src, - address dst, - uint256 wad - ) public returns (bool) { - require(balanceOf[src] >= wad); - - if (src != msg.sender && allowance[src][msg.sender] != uint256(-1)) { - require(allowance[src][msg.sender] >= wad); - allowance[src][msg.sender] -= wad; - } - - balanceOf[src] -= wad; - balanceOf[dst] += wad; - - emit Transfer(src, dst, wad); - - return true; - } + string public name = "Wrapped BTC"; + string public symbol = "WRBTC"; + uint8 public decimals = 18; + + event Approval(address indexed src, address indexed guy, uint256 wad); + event Transfer(address indexed src, address indexed dst, uint256 wad); + event Deposit(address indexed dst, uint256 wad); + event Withdrawal(address indexed src, uint256 wad); + + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + + function() external payable { + deposit(); + } + + function deposit() public payable { + balanceOf[msg.sender] += msg.value; + emit Deposit(msg.sender, msg.value); + } + + function withdraw(uint256 wad) public { + require(balanceOf[msg.sender] >= wad); + balanceOf[msg.sender] -= wad; + msg.sender.transfer(wad); + emit Withdrawal(msg.sender, wad); + } + + function totalSupply() public view returns (uint256) { + return address(this).balance; + } + + function approve(address guy, uint256 wad) public returns (bool) { + allowance[msg.sender][guy] = wad; + emit Approval(msg.sender, guy, wad); + return true; + } + + function transfer(address dst, uint256 wad) public returns (bool) { + return transferFrom(msg.sender, dst, wad); + } + + function transferFrom( + address src, + address dst, + uint256 wad + ) public returns (bool) { + require(balanceOf[src] >= wad); + + if (src != msg.sender && allowance[src][msg.sender] != uint256(-1)) { + require(allowance[src][msg.sender] >= wad); + allowance[src][msg.sender] -= wad; + } + + balanceOf[src] -= wad; + balanceOf[dst] += wad; + + emit Transfer(src, dst, wad); + + return true; + } } /* diff --git a/contracts/token/IApproveAndCall.sol b/contracts/token/IApproveAndCall.sol index 55c728401..e4f33c8ec 100644 --- a/contracts/token/IApproveAndCall.sol +++ b/contracts/token/IApproveAndCall.sol @@ -5,17 +5,17 @@ pragma solidity ^0.5.17; * @dev Interfaces are used to cast a contract address into a callable instance. */ interface IApproveAndCall { - /** - * @notice Receives approval from SOV token. - * @param _sender The sender of SOV.approveAndCall function. - * @param _amount The amount was approved. - * @param _token The address of token. - * @param _data The data will be used for low level call. - * */ - function receiveApproval( - address _sender, - uint256 _amount, - address _token, - bytes calldata _data - ) external; + /** + * @notice Receives approval from SOV token. + * @param _sender The sender of SOV.approveAndCall function. + * @param _amount The amount was approved. + * @param _token The address of token. + * @param _data The data will be used for low level call. + * */ + function receiveApproval( + address _sender, + uint256 _amount, + address _token, + bytes calldata _data + ) external; } diff --git a/contracts/token/SOV.sol b/contracts/token/SOV.sol index 1a5827f33..6e903d57f 100644 --- a/contracts/token/SOV.sol +++ b/contracts/token/SOV.sol @@ -15,46 +15,46 @@ import "./IApproveAndCall.sol"; * based upon previous governance voting and approval. * */ contract SOV is ERC20, ERC20Detailed, Ownable { - string constant NAME = "Sovryn Token"; - string constant SYMBOL = "SOV"; - uint8 constant DECIMALS = 18; + string constant NAME = "Sovryn Token"; + string constant SYMBOL = "SOV"; + uint8 constant DECIMALS = 18; - /** - * @notice Constructor called on deployment, initiates the contract. - * @dev On deployment, some amount of tokens will be minted for the owner. - * @param _initialAmount The amount of tokens to be minted on contract creation. - * */ - constructor(uint256 _initialAmount) public ERC20Detailed(NAME, SYMBOL, DECIMALS) { - if (_initialAmount != 0) { - _mint(msg.sender, _initialAmount); - } - } + /** + * @notice Constructor called on deployment, initiates the contract. + * @dev On deployment, some amount of tokens will be minted for the owner. + * @param _initialAmount The amount of tokens to be minted on contract creation. + * */ + constructor(uint256 _initialAmount) public ERC20Detailed(NAME, SYMBOL, DECIMALS) { + if (_initialAmount != 0) { + _mint(msg.sender, _initialAmount); + } + } - /** - * @notice Creates new tokens and sends them to the recipient. - * @dev Don't create more than 2^96/10 tokens before updating the governance first. - * @param _account The recipient address to get the minted tokens. - * @param _amount The amount of tokens to be minted. - * */ - function mint(address _account, uint256 _amount) public onlyOwner { - _mint(_account, _amount); - } + /** + * @notice Creates new tokens and sends them to the recipient. + * @dev Don't create more than 2^96/10 tokens before updating the governance first. + * @param _account The recipient address to get the minted tokens. + * @param _amount The amount of tokens to be minted. + * */ + function mint(address _account, uint256 _amount) public onlyOwner { + _mint(_account, _amount); + } - /** - * @notice Approves and then calls the receiving contract. - * Useful to encapsulate sending tokens to a contract in one call. - * Solidity has no native way to send tokens to contracts. - * ERC-20 tokens require approval to be spent by third parties, such as a contract in this case. - * @param _spender The contract address to spend the tokens. - * @param _amount The amount of tokens to be sent. - * @param _data Parameters for the contract call, such as endpoint signature. - * */ - function approveAndCall( - address _spender, - uint256 _amount, - bytes memory _data - ) public { - approve(_spender, _amount); - IApproveAndCall(_spender).receiveApproval(msg.sender, _amount, address(this), _data); - } + /** + * @notice Approves and then calls the receiving contract. + * Useful to encapsulate sending tokens to a contract in one call. + * Solidity has no native way to send tokens to contracts. + * ERC-20 tokens require approval to be spent by third parties, such as a contract in this case. + * @param _spender The contract address to spend the tokens. + * @param _amount The amount of tokens to be sent. + * @param _data Parameters for the contract call, such as endpoint signature. + * */ + function approveAndCall( + address _spender, + uint256 _amount, + bytes memory _data + ) public { + approve(_spender, _amount); + IApproveAndCall(_spender).receiveApproval(msg.sender, _amount, address(this), _data); + } } diff --git a/contracts/utils/AdminRole.sol b/contracts/utils/AdminRole.sol index b3693ad23..fae35a988 100644 --- a/contracts/utils/AdminRole.sol +++ b/contracts/utils/AdminRole.sol @@ -3,36 +3,36 @@ pragma solidity 0.5.17; import "../openzeppelin/Ownable.sol"; contract AdminRole is Ownable { - /// @dev user => flag whether user has admin role. - mapping(address => bool) public admins; + /// @dev user => flag whether user has admin role. + mapping(address => bool) public admins; - event AdminAdded(address admin); - event AdminRemoved(address admin); + event AdminAdded(address admin); + event AdminRemoved(address admin); - /** - * @dev Throws if called by any account other than the owner or admin. - * or on our own overriding sovrynOwnable. - */ - modifier onlyAuthorized() { - require(isOwner() || admins[msg.sender], "unauthorized"); - _; - } + /** + * @dev Throws if called by any account other than the owner or admin. + * or on our own overriding sovrynOwnable. + */ + modifier onlyAuthorized() { + require(isOwner() || admins[msg.sender], "unauthorized"); + _; + } - /** - * @notice Add account to ACL. - * @param _admin The addresses of the account to grant permissions. - * */ - function addAdmin(address _admin) public onlyOwner { - admins[_admin] = true; - emit AdminAdded(_admin); - } + /** + * @notice Add account to ACL. + * @param _admin The addresses of the account to grant permissions. + * */ + function addAdmin(address _admin) public onlyOwner { + admins[_admin] = true; + emit AdminAdded(_admin); + } - /** - * @notice Remove account from ACL. - * @param _admin The addresses of the account to revoke permissions. - * */ - function removeAdmin(address _admin) public onlyOwner { - admins[_admin] = false; - emit AdminRemoved(_admin); - } + /** + * @notice Remove account from ACL. + * @param _admin The addresses of the account to revoke permissions. + * */ + function removeAdmin(address _admin) public onlyOwner { + admins[_admin] = false; + emit AdminRemoved(_admin); + } } diff --git a/interfaces/ISovrynBrownie.sol b/interfaces/ISovrynBrownie.sol index 70f1d94d9..eedbb0b76 100644 --- a/interfaces/ISovrynBrownie.sol +++ b/interfaces/ISovrynBrownie.sol @@ -19,442 +19,466 @@ import "../contracts/events/SwapsEvents.sol"; import "../contracts/events/AffiliatesEvents.sol"; contract ISovrynBrownie is - State, - ProtocolSettingsEvents, - LoanSettingsEvents, - LoanOpeningsEvents, - LoanMaintenanceEvents, - LoanClosingsEvents, - SwapsEvents, - AffiliatesEvents, - FeesEvents + State, + ProtocolSettingsEvents, + LoanSettingsEvents, + LoanOpeningsEvents, + LoanMaintenanceEvents, + LoanClosingsEvents, + SwapsEvents, + AffiliatesEvents, + FeesEvents { - ////// Protocol ////// + ////// Protocol ////// - function replaceContract(address target) external; + function replaceContract(address target) external; - function setTargets(string[] calldata sigsArr, address[] calldata targetsArr) external; + function setTargets(string[] calldata sigsArr, address[] calldata targetsArr) external; - function getTarget(string calldata sig) external view returns (address); + function getTarget(string calldata sig) external view returns (address); - ////// Protocol Settings ////// + ////// Protocol Settings ////// - function setSovrynProtocolAddress(address newProtocolAddress) external; + function setSovrynProtocolAddress(address newProtocolAddress) external; - function setSOVTokenAddress(address newSovTokenAddress) external; + function setSOVTokenAddress(address newSovTokenAddress) external; - function setLockedSOVAddress(address newLockedSOVAddress) external; + function setLockedSOVAddress(address newLockedSOVAddress) external; - function setMinReferralsToPayoutAffiliates(uint256 newMinReferrals) external; + function setMinReferralsToPayoutAffiliates(uint256 newMinReferrals) external; - function setPriceFeedContract(address newContract) external; + function setPriceFeedContract(address newContract) external; - function setSwapsImplContract(address newContract) external; + function setSwapsImplContract(address newContract) external; - function setLoanPool(address[] calldata pools, address[] calldata assets) external; + function setLoanPool(address[] calldata pools, address[] calldata assets) external; - function setSupportedTokens(address[] calldata addrs, bool[] calldata toggles) external; + function setSupportedTokens(address[] calldata addrs, bool[] calldata toggles) external; - function setLendingFeePercent(uint256 newValue) external; + function setLendingFeePercent(uint256 newValue) external; - function setTradingFeePercent(uint256 newValue) external; + function setTradingFeePercent(uint256 newValue) external; - function setBorrowingFeePercent(uint256 newValue) external; + function setBorrowingFeePercent(uint256 newValue) external; - function setSwapExternalFeePercent(uint256 newValue) external; + function setSwapExternalFeePercent(uint256 newValue) external; - function setAffiliateFeePercent(uint256 newValue) external; + function setAffiliateFeePercent(uint256 newValue) external; - function setAffiliateTradingTokenFeePercent(uint256 newValue) external; + function setAffiliateTradingTokenFeePercent(uint256 newValue) external; - function setLiquidationIncentivePercent(uint256 newAmount) external; + function setLiquidationIncentivePercent(uint256 newAmount) external; - function setMaxDisagreement(uint256 newAmount) external; + function setMaxDisagreement(uint256 newAmount) external; - function setSourceBuffer(uint256 newAmount) external; + function setSourceBuffer(uint256 newAmount) external; - function setMaxSwapSize(uint256 newAmount) external; + function setMaxSwapSize(uint256 newAmount) external; - function setFeesController(address newController) external; + function setFeesController(address newController) external; - function withdrawFees(address[] calldata tokens, address receiver) external returns (uint256 totalWRBTCWithdrawn); + function withdrawFees(address[] calldata tokens, address receiver) + external + returns (uint256 totalWRBTCWithdrawn); - function withdrawLendingFees( - address token, - address receiver, - uint256 amount - ) external returns (bool); + function withdrawLendingFees( + address token, + address receiver, + uint256 amount + ) external returns (bool); - function withdrawTradingFees( - address token, - address receiver, - uint256 amount - ) external returns (bool); + function withdrawTradingFees( + address token, + address receiver, + uint256 amount + ) external returns (bool); - function withdrawBorrowingFees( - address token, - address receiver, - uint256 amount - ) external returns (bool); + function withdrawBorrowingFees( + address token, + address receiver, + uint256 amount + ) external returns (bool); - function withdrawProtocolToken(address receiver, uint256 amount) external returns (address, bool); + function withdrawProtocolToken(address receiver, uint256 amount) + external + returns (address, bool); - function depositProtocolToken(uint256 amount) external; + function depositProtocolToken(uint256 amount) external; - function getLoanPoolsList(uint256 start, uint256 count) external; + function getLoanPoolsList(uint256 start, uint256 count) external; - function isLoanPool(address loanPool) external view returns (bool); + function isLoanPool(address loanPool) external view returns (bool); - function setWrbtcToken(address wrbtcTokenAddress) external; + function setWrbtcToken(address wrbtcTokenAddress) external; - function setSovrynSwapContractRegistryAddress(address registryAddress) external; + function setSovrynSwapContractRegistryAddress(address registryAddress) external; - function setProtocolTokenAddress(address _protocolTokenAddress) external; + function setProtocolTokenAddress(address _protocolTokenAddress) external; - function setRolloverBaseReward(uint256 transactionCost) external; + function setRolloverBaseReward(uint256 transactionCost) external; - function setRebatePercent(uint256 rebatePercent) external; + function setRebatePercent(uint256 rebatePercent) external; - function setSpecialRebates( - address sourceToken, - address destToken, - uint256 specialRebatesPercent - ) external; + function setSpecialRebates( + address sourceToken, + address destToken, + uint256 specialRebatesPercent + ) external; - function getSpecialRebates(address sourceToken, address destToken) external view returns (uint256 specialRebatesPercent); + function getSpecialRebates(address sourceToken, address destToken) + external + view + returns (uint256 specialRebatesPercent); - function togglePaused(bool paused) external; + function togglePaused(bool paused) external; - function isProtocolPaused() external view returns (bool); + function isProtocolPaused() external view returns (bool); - ////// Loan Settings ////// + ////// Loan Settings ////// - function setupLoanParams(LoanParams[] calldata loanParamsList) external returns (bytes32[] memory loanParamsIdList); + function setupLoanParams(LoanParams[] calldata loanParamsList) + external + returns (bytes32[] memory loanParamsIdList); - // Deactivates LoanParams for future loans. Active loans using it are unaffected. - function disableLoanParams(bytes32[] calldata loanParamsIdList) external; + // Deactivates LoanParams for future loans. Active loans using it are unaffected. + function disableLoanParams(bytes32[] calldata loanParamsIdList) external; - function getLoanParams(bytes32[] calldata loanParamsIdList) external view returns (LoanParams[] memory loanParamsList); + function getLoanParams(bytes32[] calldata loanParamsIdList) + external + view + returns (LoanParams[] memory loanParamsList); + + function getLoanParamsList( + address owner, + uint256 start, + uint256 count + ) external view returns (bytes32[] memory loanParamsList); + + function getTotalPrincipal(address lender, address loanToken) external view returns (uint256); + + function minInitialMargin(bytes32 loanParamsId) external view returns (uint256); + + ////// Loan Openings ////// + + function borrowOrTradeFromPool( + bytes32 loanParamsId, + bytes32 loanId, // if 0, start a new loan + bool isTorqueLoan, + uint256 initialMargin, + address[4] calldata sentAddresses, + // lender: must match loan if loanId provided + // borrower: must match loan if loanId provided + // receiver: receiver of funds (address(0) assumes borrower address) + // manager: delegated manager of loan unless address(0) + uint256[5] calldata sentValues, + // newRate: new loan interest rate + // newPrincipal: new loan size (borrowAmount + any borrowed interest) + // torqueInterest: new amount of interest to escrow for Torque loan (determines initial loan length) + // loanTokenReceived: total loanToken deposit (amount not sent to borrower in the case of Torque loans) + // collateralTokenReceived: total collateralToken deposit + bytes calldata loanDataBytes + ) external payable returns (uint256); + + function setDelegatedManager( + bytes32 loanId, + address delegated, + bool toggle + ) external; + + function getEstimatedMarginExposure( + address loanToken, + address collateralToken, + uint256 loanTokenSent, + uint256 collateralTokenSent, + uint256 interestRate, + uint256 newPrincipal + ) external view returns (uint256); + + function getRequiredCollateral( + address loanToken, + address collateralToken, + uint256 newPrincipal, + uint256 marginAmount, + bool isTorqueLoan + ) external view returns (uint256 collateralAmountRequired); + + function getBorrowAmount( + address loanToken, + address collateralToken, + uint256 collateralTokenAmount, + uint256 marginAmount, + bool isTorqueLoan + ) external view returns (uint256 borrowAmount); + + ////// Loan Closings ////// + + function liquidate( + bytes32 loanId, + address receiver, + uint256 closeAmount // denominated in loanToken + ) + external + payable + returns ( + uint256 loanCloseAmount, + uint256 seizedAmount, + address seizedToken + ); + + function rollover(bytes32 loanId, bytes calldata loanDataBytes) external; + + function closeWithDeposit( + bytes32 loanId, + address receiver, + uint256 depositAmount // denominated in loanToken + ) + external + payable + returns ( + uint256 loanCloseAmount, + uint256 withdrawAmount, + address withdrawToken + ); + + function closeWithSwap( + bytes32 loanId, + address receiver, + uint256 swapAmount, // denominated in collateralToken + bool returnTokenIsCollateral, // true: withdraws collateralToken, false: withdraws loanToken + bytes calldata loanDataBytes + ) + external + returns ( + uint256 loanCloseAmount, + uint256 withdrawAmount, + address withdrawToken + ); + + ////// Loan Maintenance ////// + + function depositCollateral( + bytes32 loanId, + uint256 depositAmount // must match msg.value if ether is sent + ) external payable; + + function withdrawCollateral( + bytes32 loanId, + address receiver, + uint256 withdrawAmount + ) external returns (uint256 actualWithdrawAmount); + + function extendLoanByInterest( + bytes32 loanId, + address payer, + uint256 depositAmount, + bool useCollateral, + bytes calldata loanDataBytes + ) external payable returns (uint256 secondsExtended); + + function reduceLoanByInterest( + bytes32 loanId, + address receiver, + uint256 withdrawAmount + ) external returns (uint256 secondsReduced); + + function withdrawAccruedInterest(address loanToken) external; + + function getLenderInterestData(address lender, address loanToken) + external + view + returns ( + uint256 interestPaid, + uint256 interestPaidDate, + uint256 interestOwedPerDay, + uint256 interestUnPaid, + uint256 interestFeePercent, + uint256 principalTotal + ); + + function getLoanInterestData(bytes32 loanId) + external + view + returns ( + address loanToken, + uint256 interestOwedPerDay, + uint256 interestDepositTotal, + uint256 interestDepositRemaining + ); + + struct LoanReturnData { + bytes32 loanId; + address loanToken; + address collateralToken; + uint256 principal; + uint256 collateral; + uint256 interestOwedPerDay; + uint256 interestDepositRemaining; + uint256 startRate; // collateralToLoanRate + uint256 startMargin; + uint256 maintenanceMargin; + uint256 currentMargin; + uint256 maxLoanTerm; + uint256 endTimestamp; + uint256 maxLiquidatable; + uint256 maxSeizable; + } + + struct LoanReturnDataV2 { + bytes32 loanId; + address loanToken; + address collateralToken; + address borrower; + uint256 principal; + uint256 collateral; + uint256 interestOwedPerDay; + uint256 interestDepositRemaining; + uint256 startRate; /// collateralToLoanRate + uint256 startMargin; + uint256 maintenanceMargin; + uint256 currentMargin; + uint256 maxLoanTerm; + uint256 endTimestamp; + uint256 maxLiquidatable; + uint256 maxSeizable; + uint256 creationTimestamp; + } + + function getUserLoans( + address user, + uint256 start, + uint256 count, + uint256 loanType, + bool isLender, + bool unsafeOnly + ) external view returns (LoanReturnData[] memory loansData); + + function getUserLoansV2( + address user, + uint256 start, + uint256 count, + uint256 loanType, + bool isLender, + bool unsafeOnly + ) external view returns (LoanReturnDataV2[] memory loansDataV2); + + function getLoan(bytes32 loanId) external view returns (LoanReturnData memory loanData); + + function getLoanV2(bytes32 loanId) external view returns (LoanReturnDataV2 memory loanDataV2); + + function getActiveLoans( + uint256 start, + uint256 count, + bool unsafeOnly + ) external view returns (LoanReturnData[] memory loansData); + + function getActiveLoansV2( + uint256 start, + uint256 count, + bool unsafeOnly + ) external view returns (LoanReturnDataV2[] memory loansDataV2); + + ////// Protocol Migration ////// + + function setLegacyOracles(address[] calldata refs, address[] calldata oracles) external; + + function getLegacyOracle(address ref) external view returns (address); + + ////// Affiliates Module ////// + function getUserNotFirstTradeFlag(address user) external view returns (bool); + + function setUserNotFirstTradeFlag(address user) external view returns (bool); + + function payTradingFeeToAffiliatesReferrer( + address referrer, + address trader, + address token, + uint256 tradingFeeTokenBaseAmount + ) external returns (uint256 affiliatesBonusSOVAmount, uint256 affiliatesBonusTokenAmount); + + function setAffiliatesReferrer(address user, address referrer) external; //onlyCallableByLoanPools + + function getReferralsList(address referrer) external view returns (address[] memory refList); + + function getAffiliatesReferrerBalances(address referrer) + external + view + returns (address[] memory referrerTokensList, uint256[] memory referrerTokensBalances); + + function getAffiliatesReferrerTokensList(address referrer) + external + view + returns (address[] memory tokensList); + + function getAffiliatesReferrerTokenBalance(address referrer, address token) + external + view + returns (uint256); + + function withdrawAffiliatesReferrerTokenFees( + address token, + address receiver, + uint256 amount + ) external returns (uint256 withdrawAmount); + + function withdrawAllAffiliatesReferrerTokenFees(address receiver) external; + + // function getAffiliatesUserReferrer(address user) external returns ; //AUDIT: do we need it to be public? + + function getProtocolAddress() external view returns (address); + + function getSovTokenAddress() external view returns (address); + + function getLockedSOVAddress() external view returns (address); + + function getFeeRebatePercent() external view returns (uint256); - function getLoanParamsList( - address owner, - uint256 start, - uint256 count - ) external view returns (bytes32[] memory loanParamsList); + function getMinReferralsToPayout() external view returns (uint256); - function getTotalPrincipal(address lender, address loanToken) external view returns (uint256); + function getAffiliatesUserReferrer(address user) external view returns (address referrer); - function minInitialMargin(bytes32 loanParamsId) external view returns (uint256); + function getAffiliateRewardsHeld(address referrer) external view returns (uint256); - ////// Loan Openings ////// + function getAffiliateTradingTokenFeePercent() + external + view + returns (uint256 affiliateTradingTokenFeePercent); - function borrowOrTradeFromPool( - bytes32 loanParamsId, - bytes32 loanId, // if 0, start a new loan - bool isTorqueLoan, - uint256 initialMargin, - address[4] calldata sentAddresses, - // lender: must match loan if loanId provided - // borrower: must match loan if loanId provided - // receiver: receiver of funds (address(0) assumes borrower address) - // manager: delegated manager of loan unless address(0) - uint256[5] calldata sentValues, - // newRate: new loan interest rate - // newPrincipal: new loan size (borrowAmount + any borrowed interest) - // torqueInterest: new amount of interest to escrow for Torque loan (determines initial loan length) - // loanTokenReceived: total loanToken deposit (amount not sent to borrower in the case of Torque loans) - // collateralTokenReceived: total collateralToken deposit - bytes calldata loanDataBytes - ) external payable returns (uint256); - - function setDelegatedManager( - bytes32 loanId, - address delegated, - bool toggle - ) external; - - function getEstimatedMarginExposure( - address loanToken, - address collateralToken, - uint256 loanTokenSent, - uint256 collateralTokenSent, - uint256 interestRate, - uint256 newPrincipal - ) external view returns (uint256); - - function getRequiredCollateral( - address loanToken, - address collateralToken, - uint256 newPrincipal, - uint256 marginAmount, - bool isTorqueLoan - ) external view returns (uint256 collateralAmountRequired); - - function getBorrowAmount( - address loanToken, - address collateralToken, - uint256 collateralTokenAmount, - uint256 marginAmount, - bool isTorqueLoan - ) external view returns (uint256 borrowAmount); - - ////// Loan Closings ////// - - function liquidate( - bytes32 loanId, - address receiver, - uint256 closeAmount // denominated in loanToken - ) - external - payable - returns ( - uint256 loanCloseAmount, - uint256 seizedAmount, - address seizedToken - ); - - function rollover(bytes32 loanId, bytes calldata loanDataBytes) external; - - function closeWithDeposit( - bytes32 loanId, - address receiver, - uint256 depositAmount // denominated in loanToken - ) - external - payable - returns ( - uint256 loanCloseAmount, - uint256 withdrawAmount, - address withdrawToken - ); - - function closeWithSwap( - bytes32 loanId, - address receiver, - uint256 swapAmount, // denominated in collateralToken - bool returnTokenIsCollateral, // true: withdraws collateralToken, false: withdraws loanToken - bytes calldata loanDataBytes - ) - external - returns ( - uint256 loanCloseAmount, - uint256 withdrawAmount, - address withdrawToken - ); - - ////// Loan Maintenance ////// - - function depositCollateral( - bytes32 loanId, - uint256 depositAmount // must match msg.value if ether is sent - ) external payable; - - function withdrawCollateral( - bytes32 loanId, - address receiver, - uint256 withdrawAmount - ) external returns (uint256 actualWithdrawAmount); - - function extendLoanByInterest( - bytes32 loanId, - address payer, - uint256 depositAmount, - bool useCollateral, - bytes calldata loanDataBytes - ) external payable returns (uint256 secondsExtended); - - function reduceLoanByInterest( - bytes32 loanId, - address receiver, - uint256 withdrawAmount - ) external returns (uint256 secondsReduced); - - function withdrawAccruedInterest(address loanToken) external; - - function getLenderInterestData(address lender, address loanToken) - external - view - returns ( - uint256 interestPaid, - uint256 interestPaidDate, - uint256 interestOwedPerDay, - uint256 interestUnPaid, - uint256 interestFeePercent, - uint256 principalTotal - ); - - function getLoanInterestData(bytes32 loanId) - external - view - returns ( - address loanToken, - uint256 interestOwedPerDay, - uint256 interestDepositTotal, - uint256 interestDepositRemaining - ); - - struct LoanReturnData { - bytes32 loanId; - address loanToken; - address collateralToken; - uint256 principal; - uint256 collateral; - uint256 interestOwedPerDay; - uint256 interestDepositRemaining; - uint256 startRate; // collateralToLoanRate - uint256 startMargin; - uint256 maintenanceMargin; - uint256 currentMargin; - uint256 maxLoanTerm; - uint256 endTimestamp; - uint256 maxLiquidatable; - uint256 maxSeizable; - } - - struct LoanReturnDataV2 { - bytes32 loanId; - address loanToken; - address collateralToken; - address borrower; - uint256 principal; - uint256 collateral; - uint256 interestOwedPerDay; - uint256 interestDepositRemaining; - uint256 startRate; /// collateralToLoanRate - uint256 startMargin; - uint256 maintenanceMargin; - uint256 currentMargin; - uint256 maxLoanTerm; - uint256 endTimestamp; - uint256 maxLiquidatable; - uint256 maxSeizable; - uint256 creationTimestamp; - } - - function getUserLoans( - address user, - uint256 start, - uint256 count, - uint256 loanType, - bool isLender, - bool unsafeOnly - ) external view returns (LoanReturnData[] memory loansData); - - function getUserLoansV2( - address user, - uint256 start, - uint256 count, - uint256 loanType, - bool isLender, - bool unsafeOnly - ) external view returns (LoanReturnDataV2[] memory loansDataV2); - - function getLoan(bytes32 loanId) external view returns (LoanReturnData memory loanData); - - function getLoanV2(bytes32 loanId) external view returns (LoanReturnDataV2 memory loanDataV2); - - function getActiveLoans( - uint256 start, - uint256 count, - bool unsafeOnly - ) external view returns (LoanReturnData[] memory loansData); - - function getActiveLoansV2( - uint256 start, - uint256 count, - bool unsafeOnly - ) external view returns (LoanReturnDataV2[] memory loansDataV2); - - ////// Protocol Migration ////// - - function setLegacyOracles(address[] calldata refs, address[] calldata oracles) external; - - function getLegacyOracle(address ref) external view returns (address); - - ////// Affiliates Module ////// - function getUserNotFirstTradeFlag(address user) external view returns (bool); - - function setUserNotFirstTradeFlag(address user) external view returns (bool); - - function payTradingFeeToAffiliatesReferrer( - address referrer, - address trader, - address token, - uint256 tradingFeeTokenBaseAmount - ) external returns (uint256 affiliatesBonusSOVAmount, uint256 affiliatesBonusTokenAmount); - - function setAffiliatesReferrer(address user, address referrer) external; //onlyCallableByLoanPools - - function getReferralsList(address referrer) external view returns (address[] memory refList); - - function getAffiliatesReferrerBalances(address referrer) - external - view - returns (address[] memory referrerTokensList, uint256[] memory referrerTokensBalances); - - function getAffiliatesReferrerTokensList(address referrer) external view returns (address[] memory tokensList); - - function getAffiliatesReferrerTokenBalance(address referrer, address token) external view returns (uint256); - - function withdrawAffiliatesReferrerTokenFees( - address token, - address receiver, - uint256 amount - ) external returns (uint256 withdrawAmount); - - function withdrawAllAffiliatesReferrerTokenFees(address receiver) external; + function getAffiliatesTokenRewardsValueInRbtc(address referrer) + external + view + returns (uint256 rbtcTotalAmount); - // function getAffiliatesUserReferrer(address user) external returns ; //AUDIT: do we need it to be public? + function getSwapExternalFeePercent() external view returns (uint256 swapExternalFeePercent); - function getProtocolAddress() external view returns (address); + function swapExternal( + address sourceToken, + address destToken, + address receiver, + address returnToSender, + uint256 sourceTokenAmount, + uint256 requiredDestTokenAmount, + uint256 minReturn, + bytes calldata swapData + ) external returns (uint256 destTokenAmountReceived, uint256 sourceTokenAmountUsed); - function getSovTokenAddress() external view returns (address); + function getSwapExpectedReturn( + address sourceToken, + address destToken, + uint256 sourceTokenAmount + ) external view returns (uint256); - function getLockedSOVAddress() external view returns (address); + function checkPriceDivergence( + address sourceToken, + address destToken, + uint256 sourceTokenAmount, + uint256 minReturn + ) external view; - function getFeeRebatePercent() external view returns (uint256); + function setTradingRebateRewardsBasisPoint(uint256 newBasisPoint) external; - function getMinReferralsToPayout() external view returns (uint256); + function getTradingRebateRewardsBasisPoint() external view returns (uint256); - function getAffiliatesUserReferrer(address user) external view returns (address referrer); + function getDedicatedSOVRebate() external view returns (uint256); - function getAffiliateRewardsHeld(address referrer) external view returns (uint256); - - function getAffiliateTradingTokenFeePercent() external view returns (uint256 affiliateTradingTokenFeePercent); - - function getAffiliatesTokenRewardsValueInRbtc(address referrer) external view returns (uint256 rbtcTotalAmount); - - function getSwapExternalFeePercent() external view returns (uint256 swapExternalFeePercent); - - function swapExternal( - address sourceToken, - address destToken, - address receiver, - address returnToSender, - uint256 sourceTokenAmount, - uint256 requiredDestTokenAmount, - uint256 minReturn, - bytes calldata swapData - ) external returns (uint256 destTokenAmountReceived, uint256 sourceTokenAmountUsed); - - function getSwapExpectedReturn( - address sourceToken, - address destToken, - uint256 sourceTokenAmount - ) external view returns (uint256); - - function checkPriceDivergence( - address sourceToken, - address destToken, - uint256 sourceTokenAmount, - uint256 minReturn - ) external view; - - function setTradingRebateRewardsBasisPoint(uint256 newBasisPoint) external; - - function getTradingRebateRewardsBasisPoint() external view returns (uint256); - - function getDedicatedSOVRebate() external view returns (uint256); - - function setRolloverFlexFeePercent(uint256 newRolloverFlexFeePercent) external; + function setRolloverFlexFeePercent(uint256 newRolloverFlexFeePercent) external; } diff --git a/interfaces/IUniswapV2Router01.sol b/interfaces/IUniswapV2Router01.sol index 80cfc5fde..95dc69081 100644 --- a/interfaces/IUniswapV2Router01.sol +++ b/interfaces/IUniswapV2Router01.sol @@ -2,154 +2,160 @@ pragma solidity ^0.5.17; interface IUniswapV2Router01 { - function factory() external pure returns (address); - - function WETH() external pure returns (address); - - function addLiquidity( - address tokenA, - address tokenB, - uint256 amountADesired, - uint256 amountBDesired, - uint256 amountAMin, - uint256 amountBMin, - address to, - uint256 deadline - ) - external - returns ( - uint256 amountA, - uint256 amountB, - uint256 liquidity - ); - - function addLiquidityETH( - address token, - uint256 amountTokenDesired, - uint256 amountTokenMin, - uint256 amountETHMin, - address to, - uint256 deadline - ) - external - payable - returns ( - uint256 amountToken, - uint256 amountETH, - uint256 liquidity - ); - - function removeLiquidity( - address tokenA, - address tokenB, - uint256 liquidity, - uint256 amountAMin, - uint256 amountBMin, - address to, - uint256 deadline - ) external returns (uint256 amountA, uint256 amountB); - - function removeLiquidityETH( - address token, - uint256 liquidity, - uint256 amountTokenMin, - uint256 amountETHMin, - address to, - uint256 deadline - ) external returns (uint256 amountToken, uint256 amountETH); - - function removeLiquidityWithPermit( - address tokenA, - address tokenB, - uint256 liquidity, - uint256 amountAMin, - uint256 amountBMin, - address to, - uint256 deadline, - bool approveMax, - uint8 v, - bytes32 r, - bytes32 s - ) external returns (uint256 amountA, uint256 amountB); - - function removeLiquidityETHWithPermit( - address token, - uint256 liquidity, - uint256 amountTokenMin, - uint256 amountETHMin, - address to, - uint256 deadline, - bool approveMax, - uint8 v, - bytes32 r, - bytes32 s - ) external returns (uint256 amountToken, uint256 amountETH); - - function swapExactTokensForTokens( - uint256 amountIn, - uint256 amountOutMin, - address[] calldata path, - address to, - uint256 deadline - ) external returns (uint256[] memory amounts); - - function swapTokensForExactTokens( - uint256 amountOut, - uint256 amountInMax, - address[] calldata path, - address to, - uint256 deadline - ) external returns (uint256[] memory amounts); - - function swapExactETHForTokens( - uint256 amountOutMin, - address[] calldata path, - address to, - uint256 deadline - ) external payable returns (uint256[] memory amounts); - - function swapTokensForExactETH( - uint256 amountOut, - uint256 amountInMax, - address[] calldata path, - address to, - uint256 deadline - ) external returns (uint256[] memory amounts); - - function swapExactTokensForETH( - uint256 amountIn, - uint256 amountOutMin, - address[] calldata path, - address to, - uint256 deadline - ) external returns (uint256[] memory amounts); - - function swapETHForExactTokens( - uint256 amountOut, - address[] calldata path, - address to, - uint256 deadline - ) external payable returns (uint256[] memory amounts); - - function quote( - uint256 amountA, - uint256 reserveA, - uint256 reserveB - ) external pure returns (uint256 amountB); - - function getAmountOut( - uint256 amountIn, - uint256 reserveIn, - uint256 reserveOut - ) external pure returns (uint256 amountOut); - - function getAmountIn( - uint256 amountOut, - uint256 reserveIn, - uint256 reserveOut - ) external pure returns (uint256 amountIn); - - function getAmountsOut(uint256 amountIn, address[] calldata path) external view returns (uint256[] memory amounts); - - function getAmountsIn(uint256 amountOut, address[] calldata path) external view returns (uint256[] memory amounts); + function factory() external pure returns (address); + + function WETH() external pure returns (address); + + function addLiquidity( + address tokenA, + address tokenB, + uint256 amountADesired, + uint256 amountBDesired, + uint256 amountAMin, + uint256 amountBMin, + address to, + uint256 deadline + ) + external + returns ( + uint256 amountA, + uint256 amountB, + uint256 liquidity + ); + + function addLiquidityETH( + address token, + uint256 amountTokenDesired, + uint256 amountTokenMin, + uint256 amountETHMin, + address to, + uint256 deadline + ) + external + payable + returns ( + uint256 amountToken, + uint256 amountETH, + uint256 liquidity + ); + + function removeLiquidity( + address tokenA, + address tokenB, + uint256 liquidity, + uint256 amountAMin, + uint256 amountBMin, + address to, + uint256 deadline + ) external returns (uint256 amountA, uint256 amountB); + + function removeLiquidityETH( + address token, + uint256 liquidity, + uint256 amountTokenMin, + uint256 amountETHMin, + address to, + uint256 deadline + ) external returns (uint256 amountToken, uint256 amountETH); + + function removeLiquidityWithPermit( + address tokenA, + address tokenB, + uint256 liquidity, + uint256 amountAMin, + uint256 amountBMin, + address to, + uint256 deadline, + bool approveMax, + uint8 v, + bytes32 r, + bytes32 s + ) external returns (uint256 amountA, uint256 amountB); + + function removeLiquidityETHWithPermit( + address token, + uint256 liquidity, + uint256 amountTokenMin, + uint256 amountETHMin, + address to, + uint256 deadline, + bool approveMax, + uint8 v, + bytes32 r, + bytes32 s + ) external returns (uint256 amountToken, uint256 amountETH); + + function swapExactTokensForTokens( + uint256 amountIn, + uint256 amountOutMin, + address[] calldata path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + + function swapTokensForExactTokens( + uint256 amountOut, + uint256 amountInMax, + address[] calldata path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + + function swapExactETHForTokens( + uint256 amountOutMin, + address[] calldata path, + address to, + uint256 deadline + ) external payable returns (uint256[] memory amounts); + + function swapTokensForExactETH( + uint256 amountOut, + uint256 amountInMax, + address[] calldata path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + + function swapExactTokensForETH( + uint256 amountIn, + uint256 amountOutMin, + address[] calldata path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + + function swapETHForExactTokens( + uint256 amountOut, + address[] calldata path, + address to, + uint256 deadline + ) external payable returns (uint256[] memory amounts); + + function quote( + uint256 amountA, + uint256 reserveA, + uint256 reserveB + ) external pure returns (uint256 amountB); + + function getAmountOut( + uint256 amountIn, + uint256 reserveIn, + uint256 reserveOut + ) external pure returns (uint256 amountOut); + + function getAmountIn( + uint256 amountOut, + uint256 reserveIn, + uint256 reserveOut + ) external pure returns (uint256 amountIn); + + function getAmountsOut(uint256 amountIn, address[] calldata path) + external + view + returns (uint256[] memory amounts); + + function getAmountsIn(uint256 amountOut, address[] calldata path) + external + view + returns (uint256[] memory amounts); } diff --git a/interfaces/IUniswapV2Router02.sol b/interfaces/IUniswapV2Router02.sol index 565e12c99..3ea886744 100644 --- a/interfaces/IUniswapV2Router02.sol +++ b/interfaces/IUniswapV2Router02.sol @@ -4,48 +4,48 @@ pragma solidity ^0.5.17; import "./IUniswapV2Router01.sol"; contract IUniswapV2Router02 is IUniswapV2Router01 { - function removeLiquidityETHSupportingFeeOnTransferTokens( - address token, - uint256 liquidity, - uint256 amountTokenMin, - uint256 amountETHMin, - address to, - uint256 deadline - ) external returns (uint256 amountETH); + function removeLiquidityETHSupportingFeeOnTransferTokens( + address token, + uint256 liquidity, + uint256 amountTokenMin, + uint256 amountETHMin, + address to, + uint256 deadline + ) external returns (uint256 amountETH); - function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens( - address token, - uint256 liquidity, - uint256 amountTokenMin, - uint256 amountETHMin, - address to, - uint256 deadline, - bool approveMax, - uint8 v, - bytes32 r, - bytes32 s - ) external returns (uint256 amountETH); + function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens( + address token, + uint256 liquidity, + uint256 amountTokenMin, + uint256 amountETHMin, + address to, + uint256 deadline, + bool approveMax, + uint8 v, + bytes32 r, + bytes32 s + ) external returns (uint256 amountETH); - function swapExactTokensForTokensSupportingFeeOnTransferTokens( - uint256 amountIn, - uint256 amountOutMin, - address[] calldata path, - address to, - uint256 deadline - ) external; + function swapExactTokensForTokensSupportingFeeOnTransferTokens( + uint256 amountIn, + uint256 amountOutMin, + address[] calldata path, + address to, + uint256 deadline + ) external; - function swapExactETHForTokensSupportingFeeOnTransferTokens( - uint256 amountOutMin, - address[] calldata path, - address to, - uint256 deadline - ) external payable; + function swapExactETHForTokensSupportingFeeOnTransferTokens( + uint256 amountOutMin, + address[] calldata path, + address to, + uint256 deadline + ) external payable; - function swapExactTokensForETHSupportingFeeOnTransferTokens( - uint256 amountIn, - uint256 amountOutMin, - address[] calldata path, - address to, - uint256 deadline - ) external; + function swapExactTokensForETHSupportingFeeOnTransferTokens( + uint256 amountIn, + uint256 amountOutMin, + address[] calldata path, + address to, + uint256 deadline + ) external; } diff --git a/scripts/contractInteraction/ABIs/AMMPriceOracle.json b/scripts/contractInteraction/ABIs/AMMPriceOracle.json index 3bcf73bde..b385524f1 100644 --- a/scripts/contractInteraction/ABIs/AMMPriceOracle.json +++ b/scripts/contractInteraction/ABIs/AMMPriceOracle.json @@ -1,107 +1,107 @@ [ - { - "constant": true, - "inputs": [], - "name": "tokenA", - "outputs": [{ "name": "", "type": "address" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "tokenB", - "outputs": [{ "name": "", "type": "address" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [{ "name": "", "type": "address" }], - "name": "tokenDecimals", - "outputs": [{ "name": "", "type": "uint8" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "name": "_tokenA", "type": "address" }, - { "name": "_tokenB", "type": "address" } - ], - "name": "latestRate", - "outputs": [ - { "name": "", "type": "uint256" }, - { "name": "", "type": "uint256" } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "name": "_tokenA", "type": "address" }, - { "name": "_tokenB", "type": "address" } - ], - "name": "latestRateAndUpdateTime", - "outputs": [ - { "name": "", "type": "uint256" }, - { "name": "", "type": "uint256" }, - { "name": "", "type": "uint256" } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "tokenAOracle", - "outputs": [{ "name": "", "type": "address" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "lastUpdateTime", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [{ "name": "", "type": "address" }], - "name": "tokensToOracles", - "outputs": [{ "name": "", "type": "address" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "tokenBOracle", - "outputs": [{ "name": "", "type": "address" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "name": "_tokenA", "type": "address" }, - { "name": "_tokenB", "type": "address" }, - { "name": "_tokenAOracle", "type": "address" }, - { "name": "_tokenBOracle", "type": "address" } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "constructor" - } + { + "constant": true, + "inputs": [], + "name": "tokenA", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "tokenB", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "name": "", "type": "address" }], + "name": "tokenDecimals", + "outputs": [{ "name": "", "type": "uint8" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "name": "_tokenA", "type": "address" }, + { "name": "_tokenB", "type": "address" } + ], + "name": "latestRate", + "outputs": [ + { "name": "", "type": "uint256" }, + { "name": "", "type": "uint256" } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "name": "_tokenA", "type": "address" }, + { "name": "_tokenB", "type": "address" } + ], + "name": "latestRateAndUpdateTime", + "outputs": [ + { "name": "", "type": "uint256" }, + { "name": "", "type": "uint256" }, + { "name": "", "type": "uint256" } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "tokenAOracle", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "lastUpdateTime", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "name": "", "type": "address" }], + "name": "tokensToOracles", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "tokenBOracle", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "name": "_tokenA", "type": "address" }, + { "name": "_tokenB", "type": "address" }, + { "name": "_tokenAOracle", "type": "address" }, + { "name": "_tokenBOracle", "type": "address" } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + } ] diff --git a/scripts/contractInteraction/ABIs/FastBTC.json b/scripts/contractInteraction/ABIs/FastBTC.json index 2840b833e..30c4a037b 100644 --- a/scripts/contractInteraction/ABIs/FastBTC.json +++ b/scripts/contractInteraction/ABIs/FastBTC.json @@ -1,129 +1,129 @@ [ - { - "inputs": [ - { - "internalType": "address", - "name": "_admin", - "type": "address" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "previousOwner", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "OwnershipTransferred", - "type": "event" - }, - { - "inputs": [], - "name": "admin", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function", - "constant": true - }, - { - "inputs": [], - "name": "owner", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function", - "constant": true - }, - { - "inputs": [], - "name": "renounceOwnership", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "transferOwnership", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "stateMutability": "payable", - "type": "receive", - "payable": true - }, - { - "inputs": [ - { - "internalType": "address", - "name": "newAdmin", - "type": "address" - } - ], - "name": "changeAdmin", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "withdraw", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address payable", - "name": "receiver", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "withdrawAdmin", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - } + { + "inputs": [ + { + "internalType": "address", + "name": "_admin", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "inputs": [], + "name": "admin", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive", + "payable": true + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "changeAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address payable", + "name": "receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "withdrawAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } ] diff --git a/scripts/contractInteraction/ABIs/FastBTCBiDi.json b/scripts/contractInteraction/ABIs/FastBTCBiDi.json index 38923c3b6..7b37d0cf4 100644 --- a/scripts/contractInteraction/ABIs/FastBTCBiDi.json +++ b/scripts/contractInteraction/ABIs/FastBTCBiDi.json @@ -1,1108 +1,1108 @@ [ - { - "inputs": [ - { - "internalType": "address", - "name": "accessControl", - "type": "address" - }, - { - "internalType": "contract IBTCAddressValidator", - "name": "newBtcAddressValidator", - "type": "address" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "bytes32", - "name": "bitcoinTxHash", - "type": "bytes32" - }, - { - "indexed": false, - "internalType": "uint8", - "name": "transferBatchSize", - "type": "uint8" - } - ], - "name": "BitcoinTransferBatchSending", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "baseFeeSatoshi", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "dynamicFee", - "type": "uint256" - } - ], - "name": "BitcoinTransferFeeChanged", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "bytes32", - "name": "transferId", - "type": "bytes32" - }, - { - "indexed": false, - "internalType": "enum FastBTCBridge.BitcoinTransferStatus", - "name": "newStatus", - "type": "uint8" - } - ], - "name": "BitcoinTransferStatusUpdated", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "Frozen", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "bytes32", - "name": "transferId", - "type": "bytes32" - }, - { - "indexed": false, - "internalType": "string", - "name": "btcAddress", - "type": "string" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "nonce", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amountSatoshi", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "feeSatoshi", - "type": "uint256" - }, - { - "indexed": true, - "internalType": "address", - "name": "rskAddress", - "type": "address" - } - ], - "name": "NewBitcoinTransfer", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "Paused", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "Unfrozen", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "Unpaused", - "type": "event" - }, - { - "inputs": [], - "name": "DYNAMIC_FEE_DIVISOR", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "MAXIMUM_VALID_NONCE", - "outputs": [ - { - "internalType": "uint8", - "name": "", - "type": "uint8" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "MAX_BASE_FEE_SATOSHI", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "SATOSHI_DIVISOR", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "accessControl", - "outputs": [ - { - "internalType": "contract IFastBTCAccessControl", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "feeStructureIndex", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "newBaseFeeSatoshi", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "newDynamicFee", - "type": "uint256" - } - ], - "name": "addFeeStructure", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "baseFeeSatoshi", - "outputs": [ - { - "internalType": "uint32", - "name": "", - "type": "uint32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "btcAddressValidator", - "outputs": [ - { - "internalType": "contract IBTCAddressValidator", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "amountSatoshi", - "type": "uint256" - } - ], - "name": "calculateCurrentFeeSatoshi", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "amountWei", - "type": "uint256" - } - ], - "name": "calculateCurrentFeeWei", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "currentFeeStructureIndex", - "outputs": [ - { - "internalType": "uint8", - "name": "", - "type": "uint8" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes", - "name": "userData", - "type": "bytes" - } - ], - "name": "decodeBridgeUserData", - "outputs": [ - { - "internalType": "address", - "name": "rskAddress", - "type": "address" - }, - { - "internalType": "string", - "name": "btcAddress", - "type": "string" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [], - "name": "dynamicFee", - "outputs": [ - { - "internalType": "uint16", - "name": "", - "type": "uint16" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "rskAddress", - "type": "address" - }, - { - "internalType": "string", - "name": "btcAddress", - "type": "string" - } - ], - "name": "encodeBridgeUserData", - "outputs": [ - { - "internalType": "bytes", - "name": "userData", - "type": "bytes" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [], - "name": "federators", - "outputs": [ - { - "internalType": "address[]", - "name": "addresses", - "type": "address[]" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "name": "feeStructures", - "outputs": [ - { - "internalType": "uint32", - "name": "baseFeeSatoshi", - "type": "uint32" - }, - { - "internalType": "uint16", - "name": "dynamicFee", - "type": "uint16" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "freeze", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "frozen", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "string", - "name": "btcAddress", - "type": "string" - } - ], - "name": "getNextNonce", - "outputs": [ - { - "internalType": "uint8", - "name": "", - "type": "uint8" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "string", - "name": "btcAddress", - "type": "string" - }, - { - "internalType": "uint8", - "name": "nonce", - "type": "uint8" - } - ], - "name": "getTransfer", - "outputs": [ - { - "components": [ - { - "internalType": "address", - "name": "rskAddress", - "type": "address" - }, - { - "internalType": "enum FastBTCBridge.BitcoinTransferStatus", - "name": "status", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "nonce", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "feeStructureIndex", - "type": "uint8" - }, - { - "internalType": "uint32", - "name": "blockNumber", - "type": "uint32" - }, - { - "internalType": "uint40", - "name": "totalAmountSatoshi", - "type": "uint40" - }, - { - "internalType": "string", - "name": "btcAddress", - "type": "string" - } - ], - "internalType": "struct FastBTCBridge.BitcoinTransfer", - "name": "transfer", - "type": "tuple" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32[]", - "name": "transferIds", - "type": "bytes32[]" - }, - { - "internalType": "enum FastBTCBridge.BitcoinTransferStatus", - "name": "newStatus", - "type": "uint8" - } - ], - "name": "getTransferBatchUpdateHash", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "bitcoinTxHash", - "type": "bytes32" - }, - { - "internalType": "bytes32[]", - "name": "transferIds", - "type": "bytes32[]" - }, - { - "internalType": "enum FastBTCBridge.BitcoinTransferStatus", - "name": "newStatus", - "type": "uint8" - } - ], - "name": "getTransferBatchUpdateHashWithTxHash", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "transferId", - "type": "bytes32" - } - ], - "name": "getTransferByTransferId", - "outputs": [ - { - "components": [ - { - "internalType": "address", - "name": "rskAddress", - "type": "address" - }, - { - "internalType": "enum FastBTCBridge.BitcoinTransferStatus", - "name": "status", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "nonce", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "feeStructureIndex", - "type": "uint8" - }, - { - "internalType": "uint32", - "name": "blockNumber", - "type": "uint32" - }, - { - "internalType": "uint40", - "name": "totalAmountSatoshi", - "type": "uint40" - }, - { - "internalType": "string", - "name": "btcAddress", - "type": "string" - } - ], - "internalType": "struct FastBTCBridge.BitcoinTransfer", - "name": "transfer", - "type": "tuple" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "string", - "name": "btcAddress", - "type": "string" - }, - { - "internalType": "uint256", - "name": "nonce", - "type": "uint256" - } - ], - "name": "getTransferId", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "string[]", - "name": "btcAddresses", - "type": "string[]" - }, - { - "internalType": "uint8[]", - "name": "nonces", - "type": "uint8[]" - } - ], - "name": "getTransfers", - "outputs": [ - { - "components": [ - { - "internalType": "address", - "name": "rskAddress", - "type": "address" - }, - { - "internalType": "enum FastBTCBridge.BitcoinTransferStatus", - "name": "status", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "nonce", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "feeStructureIndex", - "type": "uint8" - }, - { - "internalType": "uint32", - "name": "blockNumber", - "type": "uint32" - }, - { - "internalType": "uint40", - "name": "totalAmountSatoshi", - "type": "uint40" - }, - { - "internalType": "string", - "name": "btcAddress", - "type": "string" - } - ], - "internalType": "struct FastBTCBridge.BitcoinTransfer[]", - "name": "ret", - "type": "tuple[]" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32[]", - "name": "transferIds", - "type": "bytes32[]" - } - ], - "name": "getTransfersByTransferId", - "outputs": [ - { - "components": [ - { - "internalType": "address", - "name": "rskAddress", - "type": "address" - }, - { - "internalType": "enum FastBTCBridge.BitcoinTransferStatus", - "name": "status", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "nonce", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "feeStructureIndex", - "type": "uint8" - }, - { - "internalType": "uint32", - "name": "blockNumber", - "type": "uint32" - }, - { - "internalType": "uint40", - "name": "totalAmountSatoshi", - "type": "uint40" - }, - { - "internalType": "string", - "name": "btcAddress", - "type": "string" - } - ], - "internalType": "struct FastBTCBridge.BitcoinTransfer[]", - "name": "ret", - "type": "tuple[]" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "string", - "name": "btcAddress", - "type": "string" - } - ], - "name": "isValidBtcAddress", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32[]", - "name": "transferIds", - "type": "bytes32[]" - }, - { - "internalType": "bytes[]", - "name": "signatures", - "type": "bytes[]" - } - ], - "name": "markTransfersAsMined", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "bitcoinTxHash", - "type": "bytes32" - }, - { - "internalType": "bytes32[]", - "name": "transferIds", - "type": "bytes32[]" - }, - { - "internalType": "bytes[]", - "name": "signatures", - "type": "bytes[]" - } - ], - "name": "markTransfersAsSending", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "maxTransferSatoshi", - "outputs": [ - { - "internalType": "uint40", - "name": "", - "type": "uint40" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "minTransferSatoshi", - "outputs": [ - { - "internalType": "uint40", - "name": "", - "type": "uint40" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "name": "nextNonces", - "outputs": [ - { - "internalType": "uint8", - "name": "", - "type": "uint8" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "pause", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "paused", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes", - "name": "userData", - "type": "bytes" - } - ], - "name": "receiveEthFromBridge", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32[]", - "name": "transferIds", - "type": "bytes32[]" - }, - { - "internalType": "bytes[]", - "name": "signatures", - "type": "bytes[]" - } - ], - "name": "refundTransfers", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "contract IBTCAddressValidator", - "name": "newBtcAddressValidator", - "type": "address" - } - ], - "name": "setBtcAddressValidator", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "feeStructureIndex", - "type": "uint256" - } - ], - "name": "setCurrentFeeStructure", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "newMaxTransferSatoshi", - "type": "uint256" - } - ], - "name": "setMaxTransferSatoshi", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "newMinTransferSatoshi", - "type": "uint256" - } - ], - "name": "setMinTransferSatoshi", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "string", - "name": "btcAddress", - "type": "string" - } - ], - "name": "transferToBtc", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "name": "transfers", - "outputs": [ - { - "internalType": "address", - "name": "rskAddress", - "type": "address" - }, - { - "internalType": "enum FastBTCBridge.BitcoinTransferStatus", - "name": "status", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "nonce", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "feeStructureIndex", - "type": "uint8" - }, - { - "internalType": "uint32", - "name": "blockNumber", - "type": "uint32" - }, - { - "internalType": "uint40", - "name": "totalAmountSatoshi", - "type": "uint40" - }, - { - "internalType": "string", - "name": "btcAddress", - "type": "string" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "unfreeze", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "unpause", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, - { - "internalType": "address payable", - "name": "receiver", - "type": "address" - } - ], - "name": "withdrawRbtc", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "contract IERC20", - "name": "token", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, - { - "internalType": "address", - "name": "receiver", - "type": "address" - } - ], - "name": "withdrawTokens", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - } + { + "inputs": [ + { + "internalType": "address", + "name": "accessControl", + "type": "address" + }, + { + "internalType": "contract IBTCAddressValidator", + "name": "newBtcAddressValidator", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes32", + "name": "bitcoinTxHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "transferBatchSize", + "type": "uint8" + } + ], + "name": "BitcoinTransferBatchSending", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "baseFeeSatoshi", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "dynamicFee", + "type": "uint256" + } + ], + "name": "BitcoinTransferFeeChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "transferId", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "enum FastBTCBridge.BitcoinTransferStatus", + "name": "newStatus", + "type": "uint8" + } + ], + "name": "BitcoinTransferStatusUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Frozen", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "transferId", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "string", + "name": "btcAddress", + "type": "string" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountSatoshi", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "feeSatoshi", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "rskAddress", + "type": "address" + } + ], + "name": "NewBitcoinTransfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unfrozen", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "inputs": [], + "name": "DYNAMIC_FEE_DIVISOR", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAXIMUM_VALID_NONCE", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAX_BASE_FEE_SATOSHI", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "SATOSHI_DIVISOR", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "accessControl", + "outputs": [ + { + "internalType": "contract IFastBTCAccessControl", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "feeStructureIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "newBaseFeeSatoshi", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "newDynamicFee", + "type": "uint256" + } + ], + "name": "addFeeStructure", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "baseFeeSatoshi", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "btcAddressValidator", + "outputs": [ + { + "internalType": "contract IBTCAddressValidator", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountSatoshi", + "type": "uint256" + } + ], + "name": "calculateCurrentFeeSatoshi", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountWei", + "type": "uint256" + } + ], + "name": "calculateCurrentFeeWei", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "currentFeeStructureIndex", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "decodeBridgeUserData", + "outputs": [ + { + "internalType": "address", + "name": "rskAddress", + "type": "address" + }, + { + "internalType": "string", + "name": "btcAddress", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "dynamicFee", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "rskAddress", + "type": "address" + }, + { + "internalType": "string", + "name": "btcAddress", + "type": "string" + } + ], + "name": "encodeBridgeUserData", + "outputs": [ + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "federators", + "outputs": [ + { + "internalType": "address[]", + "name": "addresses", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "feeStructures", + "outputs": [ + { + "internalType": "uint32", + "name": "baseFeeSatoshi", + "type": "uint32" + }, + { + "internalType": "uint16", + "name": "dynamicFee", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "freeze", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "frozen", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "btcAddress", + "type": "string" + } + ], + "name": "getNextNonce", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "btcAddress", + "type": "string" + }, + { + "internalType": "uint8", + "name": "nonce", + "type": "uint8" + } + ], + "name": "getTransfer", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "rskAddress", + "type": "address" + }, + { + "internalType": "enum FastBTCBridge.BitcoinTransferStatus", + "name": "status", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "nonce", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "feeStructureIndex", + "type": "uint8" + }, + { + "internalType": "uint32", + "name": "blockNumber", + "type": "uint32" + }, + { + "internalType": "uint40", + "name": "totalAmountSatoshi", + "type": "uint40" + }, + { + "internalType": "string", + "name": "btcAddress", + "type": "string" + } + ], + "internalType": "struct FastBTCBridge.BitcoinTransfer", + "name": "transfer", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32[]", + "name": "transferIds", + "type": "bytes32[]" + }, + { + "internalType": "enum FastBTCBridge.BitcoinTransferStatus", + "name": "newStatus", + "type": "uint8" + } + ], + "name": "getTransferBatchUpdateHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "bitcoinTxHash", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "transferIds", + "type": "bytes32[]" + }, + { + "internalType": "enum FastBTCBridge.BitcoinTransferStatus", + "name": "newStatus", + "type": "uint8" + } + ], + "name": "getTransferBatchUpdateHashWithTxHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "transferId", + "type": "bytes32" + } + ], + "name": "getTransferByTransferId", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "rskAddress", + "type": "address" + }, + { + "internalType": "enum FastBTCBridge.BitcoinTransferStatus", + "name": "status", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "nonce", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "feeStructureIndex", + "type": "uint8" + }, + { + "internalType": "uint32", + "name": "blockNumber", + "type": "uint32" + }, + { + "internalType": "uint40", + "name": "totalAmountSatoshi", + "type": "uint40" + }, + { + "internalType": "string", + "name": "btcAddress", + "type": "string" + } + ], + "internalType": "struct FastBTCBridge.BitcoinTransfer", + "name": "transfer", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "btcAddress", + "type": "string" + }, + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + } + ], + "name": "getTransferId", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string[]", + "name": "btcAddresses", + "type": "string[]" + }, + { + "internalType": "uint8[]", + "name": "nonces", + "type": "uint8[]" + } + ], + "name": "getTransfers", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "rskAddress", + "type": "address" + }, + { + "internalType": "enum FastBTCBridge.BitcoinTransferStatus", + "name": "status", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "nonce", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "feeStructureIndex", + "type": "uint8" + }, + { + "internalType": "uint32", + "name": "blockNumber", + "type": "uint32" + }, + { + "internalType": "uint40", + "name": "totalAmountSatoshi", + "type": "uint40" + }, + { + "internalType": "string", + "name": "btcAddress", + "type": "string" + } + ], + "internalType": "struct FastBTCBridge.BitcoinTransfer[]", + "name": "ret", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32[]", + "name": "transferIds", + "type": "bytes32[]" + } + ], + "name": "getTransfersByTransferId", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "rskAddress", + "type": "address" + }, + { + "internalType": "enum FastBTCBridge.BitcoinTransferStatus", + "name": "status", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "nonce", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "feeStructureIndex", + "type": "uint8" + }, + { + "internalType": "uint32", + "name": "blockNumber", + "type": "uint32" + }, + { + "internalType": "uint40", + "name": "totalAmountSatoshi", + "type": "uint40" + }, + { + "internalType": "string", + "name": "btcAddress", + "type": "string" + } + ], + "internalType": "struct FastBTCBridge.BitcoinTransfer[]", + "name": "ret", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "btcAddress", + "type": "string" + } + ], + "name": "isValidBtcAddress", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32[]", + "name": "transferIds", + "type": "bytes32[]" + }, + { + "internalType": "bytes[]", + "name": "signatures", + "type": "bytes[]" + } + ], + "name": "markTransfersAsMined", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "bitcoinTxHash", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "transferIds", + "type": "bytes32[]" + }, + { + "internalType": "bytes[]", + "name": "signatures", + "type": "bytes[]" + } + ], + "name": "markTransfersAsSending", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "maxTransferSatoshi", + "outputs": [ + { + "internalType": "uint40", + "name": "", + "type": "uint40" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "minTransferSatoshi", + "outputs": [ + { + "internalType": "uint40", + "name": "", + "type": "uint40" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "name": "nextNonces", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "receiveEthFromBridge", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32[]", + "name": "transferIds", + "type": "bytes32[]" + }, + { + "internalType": "bytes[]", + "name": "signatures", + "type": "bytes[]" + } + ], + "name": "refundTransfers", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IBTCAddressValidator", + "name": "newBtcAddressValidator", + "type": "address" + } + ], + "name": "setBtcAddressValidator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "feeStructureIndex", + "type": "uint256" + } + ], + "name": "setCurrentFeeStructure", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "newMaxTransferSatoshi", + "type": "uint256" + } + ], + "name": "setMaxTransferSatoshi", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "newMinTransferSatoshi", + "type": "uint256" + } + ], + "name": "setMinTransferSatoshi", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "btcAddress", + "type": "string" + } + ], + "name": "transferToBtc", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "transfers", + "outputs": [ + { + "internalType": "address", + "name": "rskAddress", + "type": "address" + }, + { + "internalType": "enum FastBTCBridge.BitcoinTransferStatus", + "name": "status", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "nonce", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "feeStructureIndex", + "type": "uint8" + }, + { + "internalType": "uint32", + "name": "blockNumber", + "type": "uint32" + }, + { + "internalType": "uint40", + "name": "totalAmountSatoshi", + "type": "uint40" + }, + { + "internalType": "string", + "name": "btcAddress", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "unfreeze", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address payable", + "name": "receiver", + "type": "address" + } + ], + "name": "withdrawRbtc", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "withdrawTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } ] diff --git a/scripts/contractInteraction/ABIs/LiquidityPoolV1Converter.json b/scripts/contractInteraction/ABIs/LiquidityPoolV1Converter.json index e30728a29..8658742e8 100644 --- a/scripts/contractInteraction/ABIs/LiquidityPoolV1Converter.json +++ b/scripts/contractInteraction/ABIs/LiquidityPoolV1Converter.json @@ -1,1295 +1,1295 @@ [ - { - "constant": false, - "inputs": [ - { - "name": "_onlyOwnerCanUpdateRegistry", - "type": "bool" - } - ], - "name": "restrictRegistryUpdate", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "address" - } - ], - "name": "protocolFeeTokensHeld", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "reserveRatio", - "outputs": [ - { - "name": "", - "type": "uint32" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "_address", - "type": "address" - } - ], - "name": "connectors", - "outputs": [ - { - "name": "", - "type": "uint256" - }, - { - "name": "", - "type": "uint32" - }, - { - "name": "", - "type": "bool" - }, - { - "name": "", - "type": "bool" - }, - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "hasETHReserve", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "receiver", - "type": "address" - } - ], - "name": "withdrawFees", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "_index", - "type": "uint256" - } - ], - "name": "connectorTokens", - "outputs": [ - { - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "_reserveToken", - "type": "address" - } - ], - "name": "reserveWeight", - "outputs": [ - { - "name": "", - "type": "uint32" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "_sourceToken", - "type": "address" - }, - { - "name": "_targetToken", - "type": "address" - }, - { - "name": "_amount", - "type": "uint256" - } - ], - "name": "getReturn", - "outputs": [ - { - "name": "", - "type": "uint256" - }, - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_newOwner", - "type": "address" - } - ], - "name": "transferTokenOwnership", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "isActive", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "onlyOwnerCanUpdateRegistry", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "token1Decimal", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "_token", - "type": "address" - } - ], - "name": "getProtocolFeeTokensHeld", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "acceptTokenOwnership", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_token", - "type": "address" - }, - { - "name": "_to", - "type": "address" - }, - { - "name": "_amount", - "type": "uint256" - } - ], - "name": "withdrawFromAnchor", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "updateRegistry", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_whitelist", - "type": "address" - } - ], - "name": "setConversionWhitelist", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "version", - "outputs": [ - { - "name": "", - "type": "uint16" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "conversionFee", - "outputs": [ - { - "name": "", - "type": "uint32" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_token", - "type": "address" - }, - { - "name": "_to", - "type": "address" - }, - { - "name": "_amount", - "type": "uint256" - } - ], - "name": "withdrawTokens", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "prevRegistry", - "outputs": [ - { - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_newOwner", - "type": "address" - } - ], - "name": "transferAnchorOwnership", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_to", - "type": "address" - } - ], - "name": "withdrawETH", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "connectorTokenCount", - "outputs": [ - { - "name": "", - "type": "uint16" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "acceptOwnership", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "registry", - "outputs": [ - { - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "oracle", - "outputs": [ - { - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "owner", - "outputs": [ - { - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "DENOMINATOR", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "maxConversionFee", - "outputs": [ - { - "name": "", - "type": "uint32" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "reserveTokenCount", - "outputs": [ - { - "name": "", - "type": "uint16" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "token0Decimal", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "restoreRegistry", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "conversionsEnabled", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "conversionWhitelist", - "outputs": [ - { - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "uint256" - } - ], - "name": "reserveTokens", - "outputs": [ - { - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "isV28OrHigher", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "pure", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "anchor", - "outputs": [ - { - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "newOwner", - "outputs": [ - { - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "upgrade", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "address" - } - ], - "name": "reserves", - "outputs": [ - { - "name": "balance", - "type": "uint256" - }, - { - "name": "weight", - "type": "uint32" - }, - { - "name": "deprecated1", - "type": "bool" - }, - { - "name": "deprecated2", - "type": "bool" - }, - { - "name": "isSet", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "_connectorToken", - "type": "address" - } - ], - "name": "getConnectorBalance", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "_reserveToken", - "type": "address" - } - ], - "name": "reserveBalance", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_sourceToken", - "type": "address" - }, - { - "name": "_targetToken", - "type": "address" - }, - { - "name": "_amount", - "type": "uint256" - }, - { - "name": "_trader", - "type": "address" - }, - { - "name": "_beneficiary", - "type": "address" - } - ], - "name": "convert", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": true, - "stateMutability": "payable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_conversionFee", - "type": "uint32" - } - ], - "name": "setConversionFee", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_newOwner", - "type": "address" - } - ], - "name": "transferOwnership", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "token", - "outputs": [ - { - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "name": "_token", - "type": "address" - }, - { - "name": "_registry", - "type": "address" - }, - { - "name": "_maxConversionFee", - "type": "uint32" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "payable": true, - "stateMutability": "payable", - "type": "fallback" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "name": "_connectorToken", - "type": "address" - }, - { - "indexed": false, - "name": "_tokenSupply", - "type": "uint256" - }, - { - "indexed": false, - "name": "_connectorBalance", - "type": "uint256" - }, - { - "indexed": false, - "name": "_connectorWeight", - "type": "uint32" - } - ], - "name": "PriceDataUpdate", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "name": "_provider", - "type": "address" - }, - { - "indexed": true, - "name": "_reserveToken", - "type": "address" - }, - { - "indexed": false, - "name": "_amount", - "type": "uint256" - }, - { - "indexed": false, - "name": "_newBalance", - "type": "uint256" - }, - { - "indexed": false, - "name": "_newSupply", - "type": "uint256" - } - ], - "name": "LiquidityAdded", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "name": "_provider", - "type": "address" - }, - { - "indexed": true, - "name": "_reserveToken", - "type": "address" - }, - { - "indexed": false, - "name": "_amount", - "type": "uint256" - }, - { - "indexed": false, - "name": "_newBalance", - "type": "uint256" - }, - { - "indexed": false, - "name": "_newSupply", - "type": "uint256" - } - ], - "name": "LiquidityRemoved", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "name": "_type", - "type": "uint16" - }, - { - "indexed": true, - "name": "_anchor", - "type": "address" - }, - { - "indexed": true, - "name": "_activated", - "type": "bool" - } - ], - "name": "Activation", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "name": "_fromToken", - "type": "address" - }, - { - "indexed": true, - "name": "_toToken", - "type": "address" - }, - { - "indexed": true, - "name": "_trader", - "type": "address" - }, - { - "indexed": false, - "name": "_amount", - "type": "uint256" - }, - { - "indexed": false, - "name": "_return", - "type": "uint256" - }, - { - "indexed": false, - "name": "_conversionFee", - "type": "int256" - }, - { - "indexed": false, - "name": "_protocolFee", - "type": "int256" - } - ], - "name": "Conversion", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "name": "_token1", - "type": "address" - }, - { - "indexed": true, - "name": "_token2", - "type": "address" - }, - { - "indexed": false, - "name": "_rateN", - "type": "uint256" - }, - { - "indexed": false, - "name": "_rateD", - "type": "uint256" - } - ], - "name": "TokenRateUpdate", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "name": "_prevFee", - "type": "uint32" - }, - { - "indexed": false, - "name": "_newFee", - "type": "uint32" - } - ], - "name": "ConversionFeeUpdate", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "name": "sender", - "type": "address" - }, - { - "indexed": true, - "name": "receiver", - "type": "address" - }, - { - "indexed": false, - "name": "token", - "type": "address" - }, - { - "indexed": false, - "name": "protocolFeeAmount", - "type": "uint256" - }, - { - "indexed": false, - "name": "wRBTCConverted", - "type": "uint256" - } - ], - "name": "WithdrawFees", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "name": "_prevOwner", - "type": "address" - }, - { - "indexed": true, - "name": "_newOwner", - "type": "address" - } - ], - "name": "OwnerUpdate", - "type": "event" - }, - { - "constant": true, - "inputs": [], - "name": "converterType", - "outputs": [ - { - "name": "", - "type": "uint16" - } - ], - "payable": false, - "stateMutability": "pure", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_oracle", - "type": "address" - } - ], - "name": "setOracle", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "acceptAnchorOwnership", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_token", - "type": "address" - }, - { - "name": "_weight", - "type": "uint32" - } - ], - "name": "addReserve", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "_sourceToken", - "type": "address" - }, - { - "name": "_targetToken", - "type": "address" - }, - { - "name": "_amount", - "type": "uint256" - } - ], - "name": "targetAmountAndFee", - "outputs": [ - { - "name": "", - "type": "uint256" - }, - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_reserveTokens", - "type": "address[]" - }, - { - "name": "_reserveAmounts", - "type": "uint256[]" - }, - { - "name": "_minReturn", - "type": "uint256" - } - ], - "name": "addLiquidity", - "outputs": [], - "payable": true, - "stateMutability": "payable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_amount", - "type": "uint256" - }, - { - "name": "_reserveTokens", - "type": "address[]" - }, - { - "name": "_reserveMinReturnAmounts", - "type": "uint256[]" - } - ], - "name": "removeLiquidity", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_amount", - "type": "uint256" - } - ], - "name": "fund", - "outputs": [], - "payable": true, - "stateMutability": "payable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_amount", - "type": "uint256" - } - ], - "name": "liquidate", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "lpTokens", - "type": "uint256" - } - ], - "name": "getExpectedOutAmount", - "outputs": [ - { - "name": "amountOut", - "type": "uint256[2]" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "_x", - "type": "uint256" - } - ], - "name": "decimalLength", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "pure", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "_n", - "type": "uint256" - }, - { - "name": "_d", - "type": "uint256" - } - ], - "name": "roundDiv", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "pure", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "_values", - "type": "uint256[]" - } - ], - "name": "geometricMean", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "pure", - "type": "function" - } + { + "constant": false, + "inputs": [ + { + "name": "_onlyOwnerCanUpdateRegistry", + "type": "bool" + } + ], + "name": "restrictRegistryUpdate", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "address" + } + ], + "name": "protocolFeeTokensHeld", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "reserveRatio", + "outputs": [ + { + "name": "", + "type": "uint32" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_address", + "type": "address" + } + ], + "name": "connectors", + "outputs": [ + { + "name": "", + "type": "uint256" + }, + { + "name": "", + "type": "uint32" + }, + { + "name": "", + "type": "bool" + }, + { + "name": "", + "type": "bool" + }, + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "hasETHReserve", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "receiver", + "type": "address" + } + ], + "name": "withdrawFees", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_index", + "type": "uint256" + } + ], + "name": "connectorTokens", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_reserveToken", + "type": "address" + } + ], + "name": "reserveWeight", + "outputs": [ + { + "name": "", + "type": "uint32" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_sourceToken", + "type": "address" + }, + { + "name": "_targetToken", + "type": "address" + }, + { + "name": "_amount", + "type": "uint256" + } + ], + "name": "getReturn", + "outputs": [ + { + "name": "", + "type": "uint256" + }, + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_newOwner", + "type": "address" + } + ], + "name": "transferTokenOwnership", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "isActive", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "onlyOwnerCanUpdateRegistry", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "token1Decimal", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_token", + "type": "address" + } + ], + "name": "getProtocolFeeTokensHeld", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "acceptTokenOwnership", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_token", + "type": "address" + }, + { + "name": "_to", + "type": "address" + }, + { + "name": "_amount", + "type": "uint256" + } + ], + "name": "withdrawFromAnchor", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "updateRegistry", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_whitelist", + "type": "address" + } + ], + "name": "setConversionWhitelist", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "version", + "outputs": [ + { + "name": "", + "type": "uint16" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "conversionFee", + "outputs": [ + { + "name": "", + "type": "uint32" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_token", + "type": "address" + }, + { + "name": "_to", + "type": "address" + }, + { + "name": "_amount", + "type": "uint256" + } + ], + "name": "withdrawTokens", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "prevRegistry", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_newOwner", + "type": "address" + } + ], + "name": "transferAnchorOwnership", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_to", + "type": "address" + } + ], + "name": "withdrawETH", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "connectorTokenCount", + "outputs": [ + { + "name": "", + "type": "uint16" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "registry", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "oracle", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "owner", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "DENOMINATOR", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "maxConversionFee", + "outputs": [ + { + "name": "", + "type": "uint32" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "reserveTokenCount", + "outputs": [ + { + "name": "", + "type": "uint16" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "token0Decimal", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "restoreRegistry", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "conversionsEnabled", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "conversionWhitelist", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "uint256" + } + ], + "name": "reserveTokens", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "isV28OrHigher", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "pure", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "anchor", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "newOwner", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "upgrade", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "address" + } + ], + "name": "reserves", + "outputs": [ + { + "name": "balance", + "type": "uint256" + }, + { + "name": "weight", + "type": "uint32" + }, + { + "name": "deprecated1", + "type": "bool" + }, + { + "name": "deprecated2", + "type": "bool" + }, + { + "name": "isSet", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_connectorToken", + "type": "address" + } + ], + "name": "getConnectorBalance", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_reserveToken", + "type": "address" + } + ], + "name": "reserveBalance", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_sourceToken", + "type": "address" + }, + { + "name": "_targetToken", + "type": "address" + }, + { + "name": "_amount", + "type": "uint256" + }, + { + "name": "_trader", + "type": "address" + }, + { + "name": "_beneficiary", + "type": "address" + } + ], + "name": "convert", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": true, + "stateMutability": "payable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_conversionFee", + "type": "uint32" + } + ], + "name": "setConversionFee", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "token", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "name": "_token", + "type": "address" + }, + { + "name": "_registry", + "type": "address" + }, + { + "name": "_maxConversionFee", + "type": "uint32" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "payable": true, + "stateMutability": "payable", + "type": "fallback" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "_connectorToken", + "type": "address" + }, + { + "indexed": false, + "name": "_tokenSupply", + "type": "uint256" + }, + { + "indexed": false, + "name": "_connectorBalance", + "type": "uint256" + }, + { + "indexed": false, + "name": "_connectorWeight", + "type": "uint32" + } + ], + "name": "PriceDataUpdate", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "_provider", + "type": "address" + }, + { + "indexed": true, + "name": "_reserveToken", + "type": "address" + }, + { + "indexed": false, + "name": "_amount", + "type": "uint256" + }, + { + "indexed": false, + "name": "_newBalance", + "type": "uint256" + }, + { + "indexed": false, + "name": "_newSupply", + "type": "uint256" + } + ], + "name": "LiquidityAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "_provider", + "type": "address" + }, + { + "indexed": true, + "name": "_reserveToken", + "type": "address" + }, + { + "indexed": false, + "name": "_amount", + "type": "uint256" + }, + { + "indexed": false, + "name": "_newBalance", + "type": "uint256" + }, + { + "indexed": false, + "name": "_newSupply", + "type": "uint256" + } + ], + "name": "LiquidityRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "_type", + "type": "uint16" + }, + { + "indexed": true, + "name": "_anchor", + "type": "address" + }, + { + "indexed": true, + "name": "_activated", + "type": "bool" + } + ], + "name": "Activation", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "_fromToken", + "type": "address" + }, + { + "indexed": true, + "name": "_toToken", + "type": "address" + }, + { + "indexed": true, + "name": "_trader", + "type": "address" + }, + { + "indexed": false, + "name": "_amount", + "type": "uint256" + }, + { + "indexed": false, + "name": "_return", + "type": "uint256" + }, + { + "indexed": false, + "name": "_conversionFee", + "type": "int256" + }, + { + "indexed": false, + "name": "_protocolFee", + "type": "int256" + } + ], + "name": "Conversion", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "_token1", + "type": "address" + }, + { + "indexed": true, + "name": "_token2", + "type": "address" + }, + { + "indexed": false, + "name": "_rateN", + "type": "uint256" + }, + { + "indexed": false, + "name": "_rateD", + "type": "uint256" + } + ], + "name": "TokenRateUpdate", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "_prevFee", + "type": "uint32" + }, + { + "indexed": false, + "name": "_newFee", + "type": "uint32" + } + ], + "name": "ConversionFeeUpdate", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "sender", + "type": "address" + }, + { + "indexed": true, + "name": "receiver", + "type": "address" + }, + { + "indexed": false, + "name": "token", + "type": "address" + }, + { + "indexed": false, + "name": "protocolFeeAmount", + "type": "uint256" + }, + { + "indexed": false, + "name": "wRBTCConverted", + "type": "uint256" + } + ], + "name": "WithdrawFees", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "_prevOwner", + "type": "address" + }, + { + "indexed": true, + "name": "_newOwner", + "type": "address" + } + ], + "name": "OwnerUpdate", + "type": "event" + }, + { + "constant": true, + "inputs": [], + "name": "converterType", + "outputs": [ + { + "name": "", + "type": "uint16" + } + ], + "payable": false, + "stateMutability": "pure", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_oracle", + "type": "address" + } + ], + "name": "setOracle", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "acceptAnchorOwnership", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_token", + "type": "address" + }, + { + "name": "_weight", + "type": "uint32" + } + ], + "name": "addReserve", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_sourceToken", + "type": "address" + }, + { + "name": "_targetToken", + "type": "address" + }, + { + "name": "_amount", + "type": "uint256" + } + ], + "name": "targetAmountAndFee", + "outputs": [ + { + "name": "", + "type": "uint256" + }, + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_reserveTokens", + "type": "address[]" + }, + { + "name": "_reserveAmounts", + "type": "uint256[]" + }, + { + "name": "_minReturn", + "type": "uint256" + } + ], + "name": "addLiquidity", + "outputs": [], + "payable": true, + "stateMutability": "payable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_amount", + "type": "uint256" + }, + { + "name": "_reserveTokens", + "type": "address[]" + }, + { + "name": "_reserveMinReturnAmounts", + "type": "uint256[]" + } + ], + "name": "removeLiquidity", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_amount", + "type": "uint256" + } + ], + "name": "fund", + "outputs": [], + "payable": true, + "stateMutability": "payable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_amount", + "type": "uint256" + } + ], + "name": "liquidate", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "lpTokens", + "type": "uint256" + } + ], + "name": "getExpectedOutAmount", + "outputs": [ + { + "name": "amountOut", + "type": "uint256[2]" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_x", + "type": "uint256" + } + ], + "name": "decimalLength", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "pure", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_n", + "type": "uint256" + }, + { + "name": "_d", + "type": "uint256" + } + ], + "name": "roundDiv", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "pure", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_values", + "type": "uint256[]" + } + ], + "name": "geometricMean", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "pure", + "type": "function" + } ] diff --git a/scripts/contractInteraction/ABIs/LiquidityPoolV2Converter.json b/scripts/contractInteraction/ABIs/LiquidityPoolV2Converter.json index 17ae1a00b..41efd4e66 100644 --- a/scripts/contractInteraction/ABIs/LiquidityPoolV2Converter.json +++ b/scripts/contractInteraction/ABIs/LiquidityPoolV2Converter.json @@ -1,798 +1,798 @@ [ - { - "constant": true, - "inputs": [{ "name": "_reserveToken", "type": "address" }], - "name": "reserveStakedBalance", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [{ "name": "_onlyOwnerCanUpdateRegistry", "type": "bool" }], - "name": "restrictRegistryUpdate", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "primaryReserveToken", - "outputs": [{ "name": "", "type": "address" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "maxStakedBalanceEnabled", - "outputs": [{ "name": "", "type": "bool" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "reserveRatio", - "outputs": [{ "name": "", "type": "uint32" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [{ "name": "_address", "type": "address" }], - "name": "connectors", - "outputs": [ - { "name": "", "type": "uint256" }, - { "name": "", "type": "uint32" }, - { "name": "", "type": "bool" }, - { "name": "", "type": "bool" }, - { "name": "", "type": "bool" } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "name": "_primaryReserveToken", "type": "address" }, - { "name": "_primaryReserveOracle", "type": "address" }, - { "name": "_secondaryReserveOracle", "type": "address" } - ], - "name": "activate", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "hasETHReserve", - "outputs": [{ "name": "", "type": "bool" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "disableMaxStakedBalances", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [{ "name": "_index", "type": "uint256" }], - "name": "connectorTokens", - "outputs": [{ "name": "", "type": "address" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [{ "name": "_reserveToken", "type": "address" }], - "name": "reserveWeight", - "outputs": [{ "name": "", "type": "uint32" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "name": "_sourceToken", "type": "address" }, - { "name": "_targetToken", "type": "address" }, - { "name": "_amount", "type": "uint256" } - ], - "name": "getReturn", - "outputs": [ - { "name": "", "type": "uint256" }, - { "name": "", "type": "uint256" } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [{ "name": "_newOwner", "type": "address" }], - "name": "transferTokenOwnership", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "isActive", - "outputs": [{ "name": "", "type": "bool" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "priceOracle", - "outputs": [{ "name": "", "type": "address" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [{ "name": "_reserveToken", "type": "address" }], - "name": "reserveAmplifiedBalance", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [{ "name": "_poolToken", "type": "address" }], - "name": "liquidationLimit", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "onlyOwnerCanUpdateRegistry", - "outputs": [{ "name": "", "type": "bool" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "acceptTokenOwnership", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "name": "_token", "type": "address" }, - { "name": "_to", "type": "address" }, - { "name": "_amount", "type": "uint256" } - ], - "name": "withdrawFromAnchor", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "converterType", - "outputs": [{ "name": "", "type": "uint16" }], - "payable": false, - "stateMutability": "pure", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "name": "_reserve1MaxStakedBalance", "type": "uint256" }, - { "name": "_reserve2MaxStakedBalance", "type": "uint256" } - ], - "name": "setMaxStakedBalances", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "updateRegistry", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [{ "name": "_whitelist", "type": "address" }], - "name": "setConversionWhitelist", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "version", - "outputs": [{ "name": "", "type": "uint16" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "name": "_reserveToken", "type": "address" }, - { "name": "_amount", "type": "uint256" }, - { "name": "_minReturn", "type": "uint256" } - ], - "name": "addLiquidity", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": true, - "stateMutability": "payable", - "type": "function" - }, - { - "constant": true, - "inputs": [{ "name": "_reserveToken", "type": "address" }], - "name": "poolToken", - "outputs": [{ "name": "", "type": "address" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "conversionFee", - "outputs": [{ "name": "", "type": "uint32" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "name": "_token", "type": "address" }, - { "name": "_to", "type": "address" }, - { "name": "_amount", "type": "uint256" } - ], - "name": "withdrawTokens", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "prevRegistry", - "outputs": [{ "name": "", "type": "address" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [{ "name": "_newOwner", "type": "address" }], - "name": "transferAnchorOwnership", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "name": "_poolToken", "type": "address" }, - { "name": "_amount", "type": "uint256" } - ], - "name": "removeLiquidityReturnAndFee", - "outputs": [ - { "name": "", "type": "uint256" }, - { "name": "", "type": "uint256" } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [{ "name": "_to", "type": "address" }], - "name": "withdrawETH", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [{ "name": "_dynamicFeeFactor", "type": "uint256" }], - "name": "setDynamicFeeFactor", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "name": "_token", "type": "address" }, - { "name": "_weight", "type": "uint32" } - ], - "name": "addReserve", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "connectorTokenCount", - "outputs": [{ "name": "", "type": "uint16" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "acceptOwnership", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "registry", - "outputs": [{ "name": "", "type": "address" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "owner", - "outputs": [{ "name": "", "type": "address" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "maxConversionFee", - "outputs": [{ "name": "", "type": "uint32" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [{ "name": "", "type": "address" }], - "name": "maxStakedBalances", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "reserveTokenCount", - "outputs": [{ "name": "", "type": "uint16" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "referenceRate", - "outputs": [ - { "name": "n", "type": "uint256" }, - { "name": "d", "type": "uint256" } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "name": "_sourceToken", "type": "address" }, - { "name": "_targetToken", "type": "address" }, - { "name": "_amount", "type": "uint256" } - ], - "name": "targetAmountAndFee", - "outputs": [ - { "name": "", "type": "uint256" }, - { "name": "", "type": "uint256" } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "restoreRegistry", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "conversionsEnabled", - "outputs": [{ "name": "", "type": "bool" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "name": "_reserveToken", "type": "address" }, - { "name": "_balance", "type": "uint256" } - ], - "name": "setReserveStakedBalance", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "referenceRateUpdateTime", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "conversionWhitelist", - "outputs": [{ "name": "", "type": "address" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "acceptAnchorOwnership", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [{ "name": "", "type": "uint256" }], - "name": "reserveTokens", - "outputs": [{ "name": "", "type": "address" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "isV28OrHigher", - "outputs": [{ "name": "", "type": "bool" }], - "payable": false, - "stateMutability": "pure", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "anchor", - "outputs": [{ "name": "", "type": "address" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "newOwner", - "outputs": [{ "name": "", "type": "address" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "upgrade", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "amplificationFactor", - "outputs": [{ "name": "", "type": "uint8" }], - "payable": false, - "stateMutability": "pure", - "type": "function" - }, - { - "constant": true, - "inputs": [{ "name": "", "type": "address" }], - "name": "reserves", - "outputs": [ - { "name": "balance", "type": "uint256" }, - { "name": "weight", "type": "uint32" }, - { "name": "deprecated1", "type": "bool" }, - { "name": "deprecated2", "type": "bool" }, - { "name": "isSet", "type": "bool" } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [{ "name": "_connectorToken", "type": "address" }], - "name": "getConnectorBalance", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "effectiveTokensRate", - "outputs": [ - { "name": "", "type": "uint256" }, - { "name": "", "type": "uint256" } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "secondaryReserveToken", - "outputs": [{ "name": "", "type": "address" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [{ "name": "_reserveToken", "type": "address" }], - "name": "reserveBalance", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "name": "_poolToken", "type": "address" }, - { "name": "_amount", "type": "uint256" }, - { "name": "_minReturn", "type": "uint256" } - ], - "name": "removeLiquidity", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "dynamicFeeFactor", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "name": "_sourceToken", "type": "address" }, - { "name": "_targetToken", "type": "address" }, - { "name": "_amount", "type": "uint256" }, - { "name": "_trader", "type": "address" }, - { "name": "_beneficiary", "type": "address" } - ], - "name": "convert", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": true, - "stateMutability": "payable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "effectiveReserveWeights", - "outputs": [ - { "name": "", "type": "uint256" }, - { "name": "", "type": "uint256" } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [{ "name": "_conversionFee", "type": "uint32" }], - "name": "setConversionFee", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [{ "name": "_newOwner", "type": "address" }], - "name": "transferOwnership", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "lastConversionRate", - "outputs": [ - { "name": "n", "type": "uint256" }, - { "name": "d", "type": "uint256" } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "token", - "outputs": [{ "name": "", "type": "address" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "name": "_poolTokensContainer", "type": "address" }, - { "name": "_registry", "type": "address" }, - { "name": "_maxConversionFee", "type": "uint32" } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "constructor" - }, - { "payable": true, "stateMutability": "payable", "type": "fallback" }, - { - "anonymous": false, - "inputs": [ - { "indexed": false, "name": "_prevFactor", "type": "uint256" }, - { "indexed": false, "name": "_newFactor", "type": "uint256" } - ], - "name": "DynamicFeeFactorUpdate", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": true, "name": "_provider", "type": "address" }, - { "indexed": true, "name": "_reserveToken", "type": "address" }, - { "indexed": false, "name": "_amount", "type": "uint256" }, - { "indexed": false, "name": "_newBalance", "type": "uint256" }, - { "indexed": false, "name": "_newSupply", "type": "uint256" } - ], - "name": "LiquidityAdded", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": true, "name": "_provider", "type": "address" }, - { "indexed": true, "name": "_reserveToken", "type": "address" }, - { "indexed": false, "name": "_amount", "type": "uint256" }, - { "indexed": false, "name": "_newBalance", "type": "uint256" }, - { "indexed": false, "name": "_newSupply", "type": "uint256" } - ], - "name": "LiquidityRemoved", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": true, "name": "_type", "type": "uint16" }, - { "indexed": true, "name": "_anchor", "type": "address" }, - { "indexed": true, "name": "_activated", "type": "bool" } - ], - "name": "Activation", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": true, "name": "_fromToken", "type": "address" }, - { "indexed": true, "name": "_toToken", "type": "address" }, - { "indexed": true, "name": "_trader", "type": "address" }, - { "indexed": false, "name": "_amount", "type": "uint256" }, - { "indexed": false, "name": "_return", "type": "uint256" }, - { "indexed": false, "name": "_conversionFee", "type": "int256" } - ], - "name": "Conversion", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": true, "name": "_token1", "type": "address" }, - { "indexed": true, "name": "_token2", "type": "address" }, - { "indexed": false, "name": "_rateN", "type": "uint256" }, - { "indexed": false, "name": "_rateD", "type": "uint256" } - ], - "name": "TokenRateUpdate", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": false, "name": "_prevFee", "type": "uint32" }, - { "indexed": false, "name": "_newFee", "type": "uint32" } - ], - "name": "ConversionFeeUpdate", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": true, "name": "_prevOwner", "type": "address" }, - { "indexed": true, "name": "_newOwner", "type": "address" } - ], - "name": "OwnerUpdate", - "type": "event" - } + { + "constant": true, + "inputs": [{ "name": "_reserveToken", "type": "address" }], + "name": "reserveStakedBalance", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "name": "_onlyOwnerCanUpdateRegistry", "type": "bool" }], + "name": "restrictRegistryUpdate", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "primaryReserveToken", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "maxStakedBalanceEnabled", + "outputs": [{ "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "reserveRatio", + "outputs": [{ "name": "", "type": "uint32" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "name": "_address", "type": "address" }], + "name": "connectors", + "outputs": [ + { "name": "", "type": "uint256" }, + { "name": "", "type": "uint32" }, + { "name": "", "type": "bool" }, + { "name": "", "type": "bool" }, + { "name": "", "type": "bool" } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "_primaryReserveToken", "type": "address" }, + { "name": "_primaryReserveOracle", "type": "address" }, + { "name": "_secondaryReserveOracle", "type": "address" } + ], + "name": "activate", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "hasETHReserve", + "outputs": [{ "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "disableMaxStakedBalances", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "name": "_index", "type": "uint256" }], + "name": "connectorTokens", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "name": "_reserveToken", "type": "address" }], + "name": "reserveWeight", + "outputs": [{ "name": "", "type": "uint32" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "name": "_sourceToken", "type": "address" }, + { "name": "_targetToken", "type": "address" }, + { "name": "_amount", "type": "uint256" } + ], + "name": "getReturn", + "outputs": [ + { "name": "", "type": "uint256" }, + { "name": "", "type": "uint256" } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "name": "_newOwner", "type": "address" }], + "name": "transferTokenOwnership", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "isActive", + "outputs": [{ "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "priceOracle", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "name": "_reserveToken", "type": "address" }], + "name": "reserveAmplifiedBalance", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "name": "_poolToken", "type": "address" }], + "name": "liquidationLimit", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "onlyOwnerCanUpdateRegistry", + "outputs": [{ "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "acceptTokenOwnership", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "_token", "type": "address" }, + { "name": "_to", "type": "address" }, + { "name": "_amount", "type": "uint256" } + ], + "name": "withdrawFromAnchor", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "converterType", + "outputs": [{ "name": "", "type": "uint16" }], + "payable": false, + "stateMutability": "pure", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "_reserve1MaxStakedBalance", "type": "uint256" }, + { "name": "_reserve2MaxStakedBalance", "type": "uint256" } + ], + "name": "setMaxStakedBalances", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "updateRegistry", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "name": "_whitelist", "type": "address" }], + "name": "setConversionWhitelist", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "version", + "outputs": [{ "name": "", "type": "uint16" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "_reserveToken", "type": "address" }, + { "name": "_amount", "type": "uint256" }, + { "name": "_minReturn", "type": "uint256" } + ], + "name": "addLiquidity", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": true, + "stateMutability": "payable", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "name": "_reserveToken", "type": "address" }], + "name": "poolToken", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "conversionFee", + "outputs": [{ "name": "", "type": "uint32" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "_token", "type": "address" }, + { "name": "_to", "type": "address" }, + { "name": "_amount", "type": "uint256" } + ], + "name": "withdrawTokens", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "prevRegistry", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "name": "_newOwner", "type": "address" }], + "name": "transferAnchorOwnership", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "name": "_poolToken", "type": "address" }, + { "name": "_amount", "type": "uint256" } + ], + "name": "removeLiquidityReturnAndFee", + "outputs": [ + { "name": "", "type": "uint256" }, + { "name": "", "type": "uint256" } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "name": "_to", "type": "address" }], + "name": "withdrawETH", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "name": "_dynamicFeeFactor", "type": "uint256" }], + "name": "setDynamicFeeFactor", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "_token", "type": "address" }, + { "name": "_weight", "type": "uint32" } + ], + "name": "addReserve", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "connectorTokenCount", + "outputs": [{ "name": "", "type": "uint16" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "registry", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "owner", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "maxConversionFee", + "outputs": [{ "name": "", "type": "uint32" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "name": "", "type": "address" }], + "name": "maxStakedBalances", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "reserveTokenCount", + "outputs": [{ "name": "", "type": "uint16" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "referenceRate", + "outputs": [ + { "name": "n", "type": "uint256" }, + { "name": "d", "type": "uint256" } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "name": "_sourceToken", "type": "address" }, + { "name": "_targetToken", "type": "address" }, + { "name": "_amount", "type": "uint256" } + ], + "name": "targetAmountAndFee", + "outputs": [ + { "name": "", "type": "uint256" }, + { "name": "", "type": "uint256" } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "restoreRegistry", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "conversionsEnabled", + "outputs": [{ "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "_reserveToken", "type": "address" }, + { "name": "_balance", "type": "uint256" } + ], + "name": "setReserveStakedBalance", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "referenceRateUpdateTime", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "conversionWhitelist", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "acceptAnchorOwnership", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "name": "", "type": "uint256" }], + "name": "reserveTokens", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "isV28OrHigher", + "outputs": [{ "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "pure", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "anchor", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "newOwner", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "upgrade", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "amplificationFactor", + "outputs": [{ "name": "", "type": "uint8" }], + "payable": false, + "stateMutability": "pure", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "name": "", "type": "address" }], + "name": "reserves", + "outputs": [ + { "name": "balance", "type": "uint256" }, + { "name": "weight", "type": "uint32" }, + { "name": "deprecated1", "type": "bool" }, + { "name": "deprecated2", "type": "bool" }, + { "name": "isSet", "type": "bool" } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "name": "_connectorToken", "type": "address" }], + "name": "getConnectorBalance", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "effectiveTokensRate", + "outputs": [ + { "name": "", "type": "uint256" }, + { "name": "", "type": "uint256" } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "secondaryReserveToken", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "name": "_reserveToken", "type": "address" }], + "name": "reserveBalance", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "_poolToken", "type": "address" }, + { "name": "_amount", "type": "uint256" }, + { "name": "_minReturn", "type": "uint256" } + ], + "name": "removeLiquidity", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "dynamicFeeFactor", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "_sourceToken", "type": "address" }, + { "name": "_targetToken", "type": "address" }, + { "name": "_amount", "type": "uint256" }, + { "name": "_trader", "type": "address" }, + { "name": "_beneficiary", "type": "address" } + ], + "name": "convert", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": true, + "stateMutability": "payable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "effectiveReserveWeights", + "outputs": [ + { "name": "", "type": "uint256" }, + { "name": "", "type": "uint256" } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "name": "_conversionFee", "type": "uint32" }], + "name": "setConversionFee", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "name": "_newOwner", "type": "address" }], + "name": "transferOwnership", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "lastConversionRate", + "outputs": [ + { "name": "n", "type": "uint256" }, + { "name": "d", "type": "uint256" } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "token", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "name": "_poolTokensContainer", "type": "address" }, + { "name": "_registry", "type": "address" }, + { "name": "_maxConversionFee", "type": "uint32" } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { "payable": true, "stateMutability": "payable", "type": "fallback" }, + { + "anonymous": false, + "inputs": [ + { "indexed": false, "name": "_prevFactor", "type": "uint256" }, + { "indexed": false, "name": "_newFactor", "type": "uint256" } + ], + "name": "DynamicFeeFactorUpdate", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "name": "_provider", "type": "address" }, + { "indexed": true, "name": "_reserveToken", "type": "address" }, + { "indexed": false, "name": "_amount", "type": "uint256" }, + { "indexed": false, "name": "_newBalance", "type": "uint256" }, + { "indexed": false, "name": "_newSupply", "type": "uint256" } + ], + "name": "LiquidityAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "name": "_provider", "type": "address" }, + { "indexed": true, "name": "_reserveToken", "type": "address" }, + { "indexed": false, "name": "_amount", "type": "uint256" }, + { "indexed": false, "name": "_newBalance", "type": "uint256" }, + { "indexed": false, "name": "_newSupply", "type": "uint256" } + ], + "name": "LiquidityRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "name": "_type", "type": "uint16" }, + { "indexed": true, "name": "_anchor", "type": "address" }, + { "indexed": true, "name": "_activated", "type": "bool" } + ], + "name": "Activation", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "name": "_fromToken", "type": "address" }, + { "indexed": true, "name": "_toToken", "type": "address" }, + { "indexed": true, "name": "_trader", "type": "address" }, + { "indexed": false, "name": "_amount", "type": "uint256" }, + { "indexed": false, "name": "_return", "type": "uint256" }, + { "indexed": false, "name": "_conversionFee", "type": "int256" } + ], + "name": "Conversion", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "name": "_token1", "type": "address" }, + { "indexed": true, "name": "_token2", "type": "address" }, + { "indexed": false, "name": "_rateN", "type": "uint256" }, + { "indexed": false, "name": "_rateD", "type": "uint256" } + ], + "name": "TokenRateUpdate", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": false, "name": "_prevFee", "type": "uint32" }, + { "indexed": false, "name": "_newFee", "type": "uint32" } + ], + "name": "ConversionFeeUpdate", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "name": "_prevOwner", "type": "address" }, + { "indexed": true, "name": "_newOwner", "type": "address" } + ], + "name": "OwnerUpdate", + "type": "event" + } ] diff --git a/scripts/contractInteraction/ABIs/Owned.json b/scripts/contractInteraction/ABIs/Owned.json index caad42fdd..030d4c066 100644 --- a/scripts/contractInteraction/ABIs/Owned.json +++ b/scripts/contractInteraction/ABIs/Owned.json @@ -1,53 +1,53 @@ [ - { - "constant": false, - "inputs": [], - "name": "acceptOwnership", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "owner", - "outputs": [{ "name": "", "type": "address" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "newOwner", - "outputs": [{ "name": "", "type": "address" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [{ "name": "_newOwner", "type": "address" }], - "name": "transferOwnership", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": true, "name": "_prevOwner", "type": "address" }, - { "indexed": true, "name": "_newOwner", "type": "address" } - ], - "name": "OwnerUpdate", - "type": "event" - } + { + "constant": false, + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "owner", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "newOwner", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "name": "_newOwner", "type": "address" }], + "name": "transferOwnership", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "name": "_prevOwner", "type": "address" }, + { "indexed": true, "name": "_newOwner", "type": "address" } + ], + "name": "OwnerUpdate", + "type": "event" + } ] diff --git a/scripts/contractInteraction/ABIs/RBTCWrapperProxy.json b/scripts/contractInteraction/ABIs/RBTCWrapperProxy.json index bb1282412..a7988ce68 100644 --- a/scripts/contractInteraction/ABIs/RBTCWrapperProxy.json +++ b/scripts/contractInteraction/ABIs/RBTCWrapperProxy.json @@ -1,542 +1,542 @@ [ - { - "inputs": [ - { - "internalType": "address", - "name": "_wrbtcTokenAddress", - "type": "address" - }, - { - "internalType": "address", - "name": "_sovrynSwapNetworkAddress", - "type": "address" - }, - { - "internalType": "contract IContractRegistry", - "name": "_registry", - "type": "address" - }, - { - "internalType": "address", - "name": "liquidityMiningAddress", - "type": "address" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "_provider", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "_reserveAmount", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "_poolTokenAmount", - "type": "uint256" - } - ], - "name": "LiquidityAdded", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "_provider", - "type": "address" - }, - { - "indexed": false, - "internalType": "contract IERC20Token[]", - "name": "_reserveTokens", - "type": "address[]" - }, - { - "indexed": false, - "internalType": "uint256[]", - "name": "_reserveAmounts", - "type": "uint256[]" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "_poolTokenAmount", - "type": "uint256" - } - ], - "name": "LiquidityAddedToV1", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "_provider", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "_reserveAmount", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "_poolTokenAmount", - "type": "uint256" - } - ], - "name": "LiquidityRemoved", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "_provider", - "type": "address" - }, - { - "indexed": false, - "internalType": "contract IERC20Token[]", - "name": "_reserveTokens", - "type": "address[]" - }, - { - "indexed": false, - "internalType": "uint256[]", - "name": "_reserveAmounts", - "type": "uint256[]" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "_poolTokenAmount", - "type": "uint256" - } - ], - "name": "LiquidityRemovedFromV1", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "_prevOwner", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "_newOwner", - "type": "address" - } - ], - "name": "OwnerUpdate", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "_beneficiary", - "type": "address" - }, - { - "indexed": true, - "internalType": "uint256", - "name": "_sourceTokenAmount", - "type": "uint256" - }, - { - "indexed": true, - "internalType": "uint256", - "name": "_targetTokenAmount", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "contract IERC20Token[]", - "name": "_path", - "type": "address[]" - } - ], - "name": "TokenConverted", - "type": "event" - }, - { - "payable": true, - "stateMutability": "payable", - "type": "fallback" - }, - { - "constant": false, - "inputs": [], - "name": "acceptOwnership", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "liquidityMiningContract", - "outputs": [ - { - "internalType": "contract LiquidityMining", - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "newOwner", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "onlyOwnerCanUpdateRegistry", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "owner", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "prevRegistry", - "outputs": [ - { - "internalType": "contract IContractRegistry", - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "registry", - "outputs": [ - { - "internalType": "contract IContractRegistry", - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "restoreRegistry", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "bool", - "name": "_onlyOwnerCanUpdateRegistry", - "type": "bool" - } - ], - "name": "restrictRegistryUpdate", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "sovrynSwapNetworkAddress", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address", - "name": "_newOwner", - "type": "address" - } - ], - "name": "transferOwnership", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "updateRegistry", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "wrbtcTokenAddress", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address", - "name": "_liquidityPoolConverterAddress", - "type": "address" - }, - { - "internalType": "address", - "name": "_reserveAddress", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_amount", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "_minReturn", - "type": "uint256" - } - ], - "name": "addLiquidityToV2", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "payable": true, - "stateMutability": "payable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address", - "name": "_liquidityPoolConverterAddress", - "type": "address" - }, - { - "internalType": "contract IERC20Token[]", - "name": "_reserveTokens", - "type": "address[]" - }, - { - "internalType": "uint256[]", - "name": "_reserveAmounts", - "type": "uint256[]" - }, - { - "internalType": "uint256", - "name": "_minReturn", - "type": "uint256" - } - ], - "name": "addLiquidityToV1", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "payable": true, - "stateMutability": "payable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address", - "name": "_liquidityPoolConverterAddress", - "type": "address" - }, - { - "internalType": "address", - "name": "_reserveAddress", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_amount", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "_minReturn", - "type": "uint256" - } - ], - "name": "removeLiquidityFromV2", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address", - "name": "_liquidityPoolConverterAddress", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_amount", - "type": "uint256" - }, - { - "internalType": "contract IERC20Token[]", - "name": "_reserveTokens", - "type": "address[]" - }, - { - "internalType": "uint256[]", - "name": "_reserveMinReturnAmounts", - "type": "uint256[]" - } - ], - "name": "removeLiquidityFromV1", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "contract IERC20Token[]", - "name": "_path", - "type": "address[]" - }, - { - "internalType": "uint256", - "name": "_amount", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "_minReturn", - "type": "uint256" - } - ], - "name": "convertByPath", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "payable": true, - "stateMutability": "payable", - "type": "function" - } + { + "inputs": [ + { + "internalType": "address", + "name": "_wrbtcTokenAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "_sovrynSwapNetworkAddress", + "type": "address" + }, + { + "internalType": "contract IContractRegistry", + "name": "_registry", + "type": "address" + }, + { + "internalType": "address", + "name": "liquidityMiningAddress", + "type": "address" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "_provider", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_reserveAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_poolTokenAmount", + "type": "uint256" + } + ], + "name": "LiquidityAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "_provider", + "type": "address" + }, + { + "indexed": false, + "internalType": "contract IERC20Token[]", + "name": "_reserveTokens", + "type": "address[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "_reserveAmounts", + "type": "uint256[]" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_poolTokenAmount", + "type": "uint256" + } + ], + "name": "LiquidityAddedToV1", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "_provider", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_reserveAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_poolTokenAmount", + "type": "uint256" + } + ], + "name": "LiquidityRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "_provider", + "type": "address" + }, + { + "indexed": false, + "internalType": "contract IERC20Token[]", + "name": "_reserveTokens", + "type": "address[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "_reserveAmounts", + "type": "uint256[]" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_poolTokenAmount", + "type": "uint256" + } + ], + "name": "LiquidityRemovedFromV1", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "_prevOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "_newOwner", + "type": "address" + } + ], + "name": "OwnerUpdate", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "_beneficiary", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "_sourceTokenAmount", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "_targetTokenAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "contract IERC20Token[]", + "name": "_path", + "type": "address[]" + } + ], + "name": "TokenConverted", + "type": "event" + }, + { + "payable": true, + "stateMutability": "payable", + "type": "fallback" + }, + { + "constant": false, + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "liquidityMiningContract", + "outputs": [ + { + "internalType": "contract LiquidityMining", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "newOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "onlyOwnerCanUpdateRegistry", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "prevRegistry", + "outputs": [ + { + "internalType": "contract IContractRegistry", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "registry", + "outputs": [ + { + "internalType": "contract IContractRegistry", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "restoreRegistry", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "bool", + "name": "_onlyOwnerCanUpdateRegistry", + "type": "bool" + } + ], + "name": "restrictRegistryUpdate", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "sovrynSwapNetworkAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "_newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "updateRegistry", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "wrbtcTokenAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "_liquidityPoolConverterAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "_reserveAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_minReturn", + "type": "uint256" + } + ], + "name": "addLiquidityToV2", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": true, + "stateMutability": "payable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "_liquidityPoolConverterAddress", + "type": "address" + }, + { + "internalType": "contract IERC20Token[]", + "name": "_reserveTokens", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "_reserveAmounts", + "type": "uint256[]" + }, + { + "internalType": "uint256", + "name": "_minReturn", + "type": "uint256" + } + ], + "name": "addLiquidityToV1", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": true, + "stateMutability": "payable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "_liquidityPoolConverterAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "_reserveAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_minReturn", + "type": "uint256" + } + ], + "name": "removeLiquidityFromV2", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "_liquidityPoolConverterAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "contract IERC20Token[]", + "name": "_reserveTokens", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "_reserveMinReturnAmounts", + "type": "uint256[]" + } + ], + "name": "removeLiquidityFromV1", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "contract IERC20Token[]", + "name": "_path", + "type": "address[]" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_minReturn", + "type": "uint256" + } + ], + "name": "convertByPath", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": true, + "stateMutability": "payable", + "type": "function" + } ] diff --git a/scripts/contractInteraction/ABIs/SovrynNft.json b/scripts/contractInteraction/ABIs/SovrynNft.json index 78fb5a018..a800aa7d2 100644 --- a/scripts/contractInteraction/ABIs/SovrynNft.json +++ b/scripts/contractInteraction/ABIs/SovrynNft.json @@ -1,513 +1,513 @@ [ - { - "inputs": [ - { - "internalType": "string", - "name": "name", - "type": "string" - }, - { - "internalType": "string", - "name": "symbol", - "type": "string" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "owner", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "approved", - "type": "address" - }, - { - "indexed": true, - "internalType": "uint256", - "name": "tokenId", - "type": "uint256" - } - ], - "name": "Approval", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "owner", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "operator", - "type": "address" - }, - { - "indexed": false, - "internalType": "bool", - "name": "approved", - "type": "bool" - } - ], - "name": "ApprovalForAll", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "previousOwner", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "OwnershipTransferred", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "indexed": true, - "internalType": "uint256", - "name": "tokenId", - "type": "uint256" - } - ], - "name": "Transfer", - "type": "event" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "uint256", - "name": "tokenId", - "type": "uint256" - } - ], - "name": "approve", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "owner", - "type": "address" - } - ], - "name": "balanceOf", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function", - "constant": true - }, - { - "inputs": [], - "name": "baseURI", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "view", - "type": "function", - "constant": true - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "tokenId", - "type": "uint256" - } - ], - "name": "getApproved", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function", - "constant": true - }, - { - "inputs": [ - { - "internalType": "address", - "name": "owner", - "type": "address" - }, - { - "internalType": "address", - "name": "operator", - "type": "address" - } - ], - "name": "isApprovedForAll", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function", - "constant": true - }, - { - "inputs": [], - "name": "name", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "view", - "type": "function", - "constant": true - }, - { - "inputs": [], - "name": "owner", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function", - "constant": true - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "tokenId", - "type": "uint256" - } - ], - "name": "ownerOf", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function", - "constant": true - }, - { - "inputs": [], - "name": "renounceOwnership", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "uint256", - "name": "tokenId", - "type": "uint256" - } - ], - "name": "safeTransferFrom", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "uint256", - "name": "tokenId", - "type": "uint256" - }, - { - "internalType": "bytes", - "name": "_data", - "type": "bytes" - } - ], - "name": "safeTransferFrom", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "operator", - "type": "address" - }, - { - "internalType": "bool", - "name": "approved", - "type": "bool" - } - ], - "name": "setApprovalForAll", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes4", - "name": "interfaceId", - "type": "bytes4" - } - ], - "name": "supportsInterface", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function", - "constant": true - }, - { - "inputs": [], - "name": "symbol", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "view", - "type": "function", - "constant": true - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "index", - "type": "uint256" - } - ], - "name": "tokenByIndex", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function", - "constant": true - }, - { - "inputs": [ - { - "internalType": "address", - "name": "owner", - "type": "address" - }, - { - "internalType": "uint256", - "name": "index", - "type": "uint256" - } - ], - "name": "tokenOfOwnerByIndex", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function", - "constant": true - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "tokenId", - "type": "uint256" - } - ], - "name": "tokenURI", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "view", - "type": "function", - "constant": true - }, - { - "inputs": [], - "name": "totalSupply", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function", - "constant": true - }, - { - "inputs": [ - { - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "uint256", - "name": "tokenId", - "type": "uint256" - } - ], - "name": "transferFrom", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "transferOwnership", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "receiver", - "type": "address" - } - ], - "name": "mint", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "tokenId", - "type": "uint256" - }, - { - "internalType": "string", - "name": "tokenURI", - "type": "string" - } - ], - "name": "setTokenURI", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - } + { + "inputs": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol", + "type": "string" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "approved", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "ApprovalForAll", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [], + "name": "baseURI", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "getApproved", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isApprovedForAll", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "ownerOf", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "setApprovalForAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "tokenByIndex", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "tokenOfOwnerByIndex", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "tokenURI", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "string", + "name": "tokenURI", + "type": "string" + } + ], + "name": "setTokenURI", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } ] diff --git a/scripts/contractInteraction/ABIs/SovrynSwapFormula.json b/scripts/contractInteraction/ABIs/SovrynSwapFormula.json index 447be1bca..09cc42de3 100644 --- a/scripts/contractInteraction/ABIs/SovrynSwapFormula.json +++ b/scripts/contractInteraction/ABIs/SovrynSwapFormula.json @@ -1,266 +1,266 @@ [ - { - "constant": true, - "inputs": [ - { "name": "_supply", "type": "uint256" }, - { "name": "_reserveBalance", "type": "uint256" }, - { "name": "_reserveRatio", "type": "uint32" }, - { "name": "_amount", "type": "uint256" } - ], - "name": "calculateFundCost", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "name": "_supply", "type": "uint256" }, - { "name": "_reserveBalance", "type": "uint256" }, - { "name": "_reserveWeight", "type": "uint32" }, - { "name": "_amount", "type": "uint256" } - ], - "name": "calculatePurchaseReturn", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "name": "_supply", "type": "uint256" }, - { "name": "_reserveBalance", "type": "uint256" }, - { "name": "_reserveRatio", "type": "uint32" }, - { "name": "_amount", "type": "uint256" } - ], - "name": "fundSupplyAmount", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "name": "_supply", "type": "uint256" }, - { "name": "_reserveBalance", "type": "uint256" }, - { "name": "_reserveRatio", "type": "uint32" }, - { "name": "_amount", "type": "uint256" } - ], - "name": "liquidateRate", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "name": "_supply", "type": "uint256" }, - { "name": "_reserveBalance", "type": "uint256" }, - { "name": "_reserveWeight", "type": "uint32" }, - { "name": "_amount", "type": "uint256" } - ], - "name": "purchaseRate", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "name": "_supply", "type": "uint256" }, - { "name": "_reserveBalance", "type": "uint256" }, - { "name": "_reserveWeight", "type": "uint32" }, - { "name": "_amount", "type": "uint256" } - ], - "name": "calculateSaleReturn", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "version", - "outputs": [{ "name": "", "type": "uint16" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "name": "_sourceReserveBalance", "type": "uint256" }, - { "name": "_sourceReserveWeight", "type": "uint32" }, - { "name": "_targetReserveBalance", "type": "uint256" }, - { "name": "_targetReserveWeight", "type": "uint32" }, - { "name": "_amount", "type": "uint256" } - ], - "name": "calculateCrossConnectorReturn", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "name": "_supply", "type": "uint256" }, - { "name": "_reserveBalance", "type": "uint256" }, - { "name": "_reserveWeight", "type": "uint32" }, - { "name": "_amount", "type": "uint256" } - ], - "name": "saleTargetAmount", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "name": "_sourceReserveBalance", "type": "uint256" }, - { "name": "_sourceReserveWeight", "type": "uint32" }, - { "name": "_targetReserveBalance", "type": "uint256" }, - { "name": "_targetReserveWeight", "type": "uint32" }, - { "name": "_amount", "type": "uint256" } - ], - "name": "calculateCrossReserveReturn", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "name": "_supply", "type": "uint256" }, - { "name": "_reserveBalance", "type": "uint256" }, - { "name": "_reserveRatio", "type": "uint32" }, - { "name": "_amount", "type": "uint256" } - ], - "name": "liquidateReserveAmount", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "name": "_sourceReserveBalance", "type": "uint256" }, - { "name": "_sourceReserveWeight", "type": "uint32" }, - { "name": "_targetReserveBalance", "type": "uint256" }, - { "name": "_targetReserveWeight", "type": "uint32" }, - { "name": "_amount", "type": "uint256" } - ], - "name": "crossReserveTargetAmount", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "name": "_sourceReserveBalance", "type": "uint256" }, - { "name": "_sourceReserveWeight", "type": "uint32" }, - { "name": "_targetReserveBalance", "type": "uint256" }, - { "name": "_targetReserveWeight", "type": "uint32" }, - { "name": "_amount", "type": "uint256" } - ], - "name": "crossReserveRate", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "name": "_primaryReserveStakedBalance", "type": "uint256" }, - { "name": "_primaryReserveBalance", "type": "uint256" }, - { "name": "_secondaryReserveBalance", "type": "uint256" }, - { "name": "_reserveRateNumerator", "type": "uint256" }, - { "name": "_reserveRateDenominator", "type": "uint256" } - ], - "name": "balancedWeights", - "outputs": [ - { "name": "", "type": "uint32" }, - { "name": "", "type": "uint32" } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "name": "_supply", "type": "uint256" }, - { "name": "_reserveBalance", "type": "uint256" }, - { "name": "_reserveRatio", "type": "uint32" }, - { "name": "_amount", "type": "uint256" } - ], - "name": "calculateLiquidateReturn", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "init", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "name": "_supply", "type": "uint256" }, - { "name": "_reserveBalance", "type": "uint256" }, - { "name": "_reserveRatio", "type": "uint32" }, - { "name": "_amount", "type": "uint256" } - ], - "name": "fundCost", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "name": "_supply", "type": "uint256" }, - { "name": "_reserveBalance", "type": "uint256" }, - { "name": "_reserveWeight", "type": "uint32" }, - { "name": "_amount", "type": "uint256" } - ], - "name": "purchaseTargetAmount", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "name": "_supply", "type": "uint256" }, - { "name": "_reserveBalance", "type": "uint256" }, - { "name": "_reserveWeight", "type": "uint32" }, - { "name": "_amount", "type": "uint256" } - ], - "name": "saleRate", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - } + { + "constant": true, + "inputs": [ + { "name": "_supply", "type": "uint256" }, + { "name": "_reserveBalance", "type": "uint256" }, + { "name": "_reserveRatio", "type": "uint32" }, + { "name": "_amount", "type": "uint256" } + ], + "name": "calculateFundCost", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "name": "_supply", "type": "uint256" }, + { "name": "_reserveBalance", "type": "uint256" }, + { "name": "_reserveWeight", "type": "uint32" }, + { "name": "_amount", "type": "uint256" } + ], + "name": "calculatePurchaseReturn", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "name": "_supply", "type": "uint256" }, + { "name": "_reserveBalance", "type": "uint256" }, + { "name": "_reserveRatio", "type": "uint32" }, + { "name": "_amount", "type": "uint256" } + ], + "name": "fundSupplyAmount", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "name": "_supply", "type": "uint256" }, + { "name": "_reserveBalance", "type": "uint256" }, + { "name": "_reserveRatio", "type": "uint32" }, + { "name": "_amount", "type": "uint256" } + ], + "name": "liquidateRate", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "name": "_supply", "type": "uint256" }, + { "name": "_reserveBalance", "type": "uint256" }, + { "name": "_reserveWeight", "type": "uint32" }, + { "name": "_amount", "type": "uint256" } + ], + "name": "purchaseRate", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "name": "_supply", "type": "uint256" }, + { "name": "_reserveBalance", "type": "uint256" }, + { "name": "_reserveWeight", "type": "uint32" }, + { "name": "_amount", "type": "uint256" } + ], + "name": "calculateSaleReturn", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "version", + "outputs": [{ "name": "", "type": "uint16" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "name": "_sourceReserveBalance", "type": "uint256" }, + { "name": "_sourceReserveWeight", "type": "uint32" }, + { "name": "_targetReserveBalance", "type": "uint256" }, + { "name": "_targetReserveWeight", "type": "uint32" }, + { "name": "_amount", "type": "uint256" } + ], + "name": "calculateCrossConnectorReturn", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "name": "_supply", "type": "uint256" }, + { "name": "_reserveBalance", "type": "uint256" }, + { "name": "_reserveWeight", "type": "uint32" }, + { "name": "_amount", "type": "uint256" } + ], + "name": "saleTargetAmount", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "name": "_sourceReserveBalance", "type": "uint256" }, + { "name": "_sourceReserveWeight", "type": "uint32" }, + { "name": "_targetReserveBalance", "type": "uint256" }, + { "name": "_targetReserveWeight", "type": "uint32" }, + { "name": "_amount", "type": "uint256" } + ], + "name": "calculateCrossReserveReturn", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "name": "_supply", "type": "uint256" }, + { "name": "_reserveBalance", "type": "uint256" }, + { "name": "_reserveRatio", "type": "uint32" }, + { "name": "_amount", "type": "uint256" } + ], + "name": "liquidateReserveAmount", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "name": "_sourceReserveBalance", "type": "uint256" }, + { "name": "_sourceReserveWeight", "type": "uint32" }, + { "name": "_targetReserveBalance", "type": "uint256" }, + { "name": "_targetReserveWeight", "type": "uint32" }, + { "name": "_amount", "type": "uint256" } + ], + "name": "crossReserveTargetAmount", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "name": "_sourceReserveBalance", "type": "uint256" }, + { "name": "_sourceReserveWeight", "type": "uint32" }, + { "name": "_targetReserveBalance", "type": "uint256" }, + { "name": "_targetReserveWeight", "type": "uint32" }, + { "name": "_amount", "type": "uint256" } + ], + "name": "crossReserveRate", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "name": "_primaryReserveStakedBalance", "type": "uint256" }, + { "name": "_primaryReserveBalance", "type": "uint256" }, + { "name": "_secondaryReserveBalance", "type": "uint256" }, + { "name": "_reserveRateNumerator", "type": "uint256" }, + { "name": "_reserveRateDenominator", "type": "uint256" } + ], + "name": "balancedWeights", + "outputs": [ + { "name": "", "type": "uint32" }, + { "name": "", "type": "uint32" } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "name": "_supply", "type": "uint256" }, + { "name": "_reserveBalance", "type": "uint256" }, + { "name": "_reserveRatio", "type": "uint32" }, + { "name": "_amount", "type": "uint256" } + ], + "name": "calculateLiquidateReturn", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "init", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "name": "_supply", "type": "uint256" }, + { "name": "_reserveBalance", "type": "uint256" }, + { "name": "_reserveRatio", "type": "uint32" }, + { "name": "_amount", "type": "uint256" } + ], + "name": "fundCost", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "name": "_supply", "type": "uint256" }, + { "name": "_reserveBalance", "type": "uint256" }, + { "name": "_reserveWeight", "type": "uint32" }, + { "name": "_amount", "type": "uint256" } + ], + "name": "purchaseTargetAmount", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "name": "_supply", "type": "uint256" }, + { "name": "_reserveBalance", "type": "uint256" }, + { "name": "_reserveWeight", "type": "uint32" }, + { "name": "_amount", "type": "uint256" } + ], + "name": "saleRate", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + } ] diff --git a/scripts/contractInteraction/ABIs/SovrynSwapNetwork.json b/scripts/contractInteraction/ABIs/SovrynSwapNetwork.json index e65583aa3..47ea6e38c 100644 --- a/scripts/contractInteraction/ABIs/SovrynSwapNetwork.json +++ b/scripts/contractInteraction/ABIs/SovrynSwapNetwork.json @@ -1,392 +1,392 @@ [ - { - "constant": false, - "inputs": [{ "name": "_onlyOwnerCanUpdateRegistry", "type": "bool" }], - "name": "restrictRegistryUpdate", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "name": "_token", "type": "address" }, - { "name": "_register", "type": "bool" } - ], - "name": "registerEtherToken", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "name": "_path", "type": "address[]" }, - { "name": "_amount", "type": "uint256" } - ], - "name": "getReturnByPath", - "outputs": [ - { "name": "", "type": "uint256" }, - { "name": "", "type": "uint256" } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "name": "_path", "type": "address[]" }, - { "name": "_amount", "type": "uint256" }, - { "name": "_minReturn", "type": "uint256" }, - { "name": "_beneficiary", "type": "address" }, - { "name": "_affiliateAccount", "type": "address" }, - { "name": "_affiliateFee", "type": "uint256" } - ], - "name": "claimAndConvertFor2", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "onlyOwnerCanUpdateRegistry", - "outputs": [{ "name": "", "type": "bool" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "updateRegistry", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "name": "_path", "type": "address[]" }, - { "name": "_amount", "type": "uint256" }, - { "name": "_minReturn", "type": "uint256" }, - { "name": "_affiliateAccount", "type": "address" }, - { "name": "_affiliateFee", "type": "uint256" } - ], - "name": "convert2", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": true, - "stateMutability": "payable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "maxAffiliateFee", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "name": "_token", "type": "address" }, - { "name": "_to", "type": "address" }, - { "name": "_amount", "type": "uint256" } - ], - "name": "withdrawTokens", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "prevRegistry", - "outputs": [{ "name": "", "type": "address" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "acceptOwnership", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "registry", - "outputs": [{ "name": "", "type": "address" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "name": "_path", "type": "address[]" }, - { "name": "_amount", "type": "uint256" } - ], - "name": "rateByPath", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [{ "name": "", "type": "address" }], - "name": "etherTokens", - "outputs": [{ "name": "", "type": "bool" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "name": "_path", "type": "address[]" }, - { "name": "_sovrynSwapX", "type": "address" }, - { "name": "_conversionId", "type": "uint256" }, - { "name": "_minReturn", "type": "uint256" }, - { "name": "_beneficiary", "type": "address" } - ], - "name": "completeXConversion", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "owner", - "outputs": [{ "name": "", "type": "address" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "name": "_path", "type": "address[]" }, - { "name": "_amount", "type": "uint256" }, - { "name": "_minReturn", "type": "uint256" }, - { "name": "_beneficiary", "type": "address" }, - { "name": "_affiliateAccount", "type": "address" }, - { "name": "_affiliateFee", "type": "uint256" } - ], - "name": "convertFor2", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": true, - "stateMutability": "payable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "name": "_path", "type": "address[]" }, - { "name": "_amount", "type": "uint256" }, - { "name": "_minReturn", "type": "uint256" }, - { "name": "_beneficiary", "type": "address" } - ], - "name": "claimAndConvertFor", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "restoreRegistry", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "name": "_path", "type": "address[]" }, - { "name": "_amount", "type": "uint256" }, - { "name": "_minReturn", "type": "uint256" }, - { "name": "_beneficiary", "type": "address" }, - { "name": "_affiliateAccount", "type": "address" }, - { "name": "_affiliateFee", "type": "uint256" } - ], - "name": "convertByPath", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": true, - "stateMutability": "payable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "name": "_path", "type": "address[]" }, - { "name": "_amount", "type": "uint256" }, - { "name": "_minReturn", "type": "uint256" }, - { "name": "_targetBlockchain", "type": "bytes32" }, - { "name": "_targetAccount", "type": "bytes32" }, - { "name": "_conversionId", "type": "uint256" } - ], - "name": "xConvert", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": true, - "stateMutability": "payable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "name": "_path", "type": "address[]" }, - { "name": "_amount", "type": "uint256" }, - { "name": "_minReturn", "type": "uint256" } - ], - "name": "claimAndConvert", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "name": "_path", "type": "address[]" }, - { "name": "_amount", "type": "uint256" }, - { "name": "_minReturn", "type": "uint256" }, - { "name": "_beneficiary", "type": "address" } - ], - "name": "convertFor", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": true, - "stateMutability": "payable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "name": "_path", "type": "address[]" }, - { "name": "_amount", "type": "uint256" }, - { "name": "_minReturn", "type": "uint256" }, - { "name": "_targetBlockchain", "type": "bytes32" }, - { "name": "_targetAccount", "type": "bytes32" }, - { "name": "_conversionId", "type": "uint256" }, - { "name": "_affiliateAccount", "type": "address" }, - { "name": "_affiliateFee", "type": "uint256" } - ], - "name": "xConvert2", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": true, - "stateMutability": "payable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "newOwner", - "outputs": [{ "name": "", "type": "address" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "name": "_sourceToken", "type": "address" }, - { "name": "_targetToken", "type": "address" } - ], - "name": "conversionPath", - "outputs": [{ "name": "", "type": "address[]" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "name": "_path", "type": "address[]" }, - { "name": "_amount", "type": "uint256" }, - { "name": "_minReturn", "type": "uint256" }, - { "name": "_affiliateAccount", "type": "address" }, - { "name": "_affiliateFee", "type": "uint256" } - ], - "name": "claimAndConvert2", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [{ "name": "_newOwner", "type": "address" }], - "name": "transferOwnership", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "name": "_path", "type": "address[]" }, - { "name": "_amount", "type": "uint256" }, - { "name": "_minReturn", "type": "uint256" } - ], - "name": "convert", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": true, - "stateMutability": "payable", - "type": "function" - }, - { - "constant": false, - "inputs": [{ "name": "_maxAffiliateFee", "type": "uint256" }], - "name": "setMaxAffiliateFee", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [{ "name": "_registry", "type": "address" }], - "payable": false, - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": true, "name": "_smartToken", "type": "address" }, - { "indexed": true, "name": "_fromToken", "type": "address" }, - { "indexed": true, "name": "_toToken", "type": "address" }, - { "indexed": false, "name": "_fromAmount", "type": "uint256" }, - { "indexed": false, "name": "_toAmount", "type": "uint256" }, - { "indexed": false, "name": "_trader", "type": "address" } - ], - "name": "Conversion", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": true, "name": "_prevOwner", "type": "address" }, - { "indexed": true, "name": "_newOwner", "type": "address" } - ], - "name": "OwnerUpdate", - "type": "event" - } + { + "constant": false, + "inputs": [{ "name": "_onlyOwnerCanUpdateRegistry", "type": "bool" }], + "name": "restrictRegistryUpdate", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "_token", "type": "address" }, + { "name": "_register", "type": "bool" } + ], + "name": "registerEtherToken", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "name": "_path", "type": "address[]" }, + { "name": "_amount", "type": "uint256" } + ], + "name": "getReturnByPath", + "outputs": [ + { "name": "", "type": "uint256" }, + { "name": "", "type": "uint256" } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "_path", "type": "address[]" }, + { "name": "_amount", "type": "uint256" }, + { "name": "_minReturn", "type": "uint256" }, + { "name": "_beneficiary", "type": "address" }, + { "name": "_affiliateAccount", "type": "address" }, + { "name": "_affiliateFee", "type": "uint256" } + ], + "name": "claimAndConvertFor2", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "onlyOwnerCanUpdateRegistry", + "outputs": [{ "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "updateRegistry", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "_path", "type": "address[]" }, + { "name": "_amount", "type": "uint256" }, + { "name": "_minReturn", "type": "uint256" }, + { "name": "_affiliateAccount", "type": "address" }, + { "name": "_affiliateFee", "type": "uint256" } + ], + "name": "convert2", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": true, + "stateMutability": "payable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "maxAffiliateFee", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "_token", "type": "address" }, + { "name": "_to", "type": "address" }, + { "name": "_amount", "type": "uint256" } + ], + "name": "withdrawTokens", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "prevRegistry", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "registry", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "name": "_path", "type": "address[]" }, + { "name": "_amount", "type": "uint256" } + ], + "name": "rateByPath", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "name": "", "type": "address" }], + "name": "etherTokens", + "outputs": [{ "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "_path", "type": "address[]" }, + { "name": "_sovrynSwapX", "type": "address" }, + { "name": "_conversionId", "type": "uint256" }, + { "name": "_minReturn", "type": "uint256" }, + { "name": "_beneficiary", "type": "address" } + ], + "name": "completeXConversion", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "owner", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "_path", "type": "address[]" }, + { "name": "_amount", "type": "uint256" }, + { "name": "_minReturn", "type": "uint256" }, + { "name": "_beneficiary", "type": "address" }, + { "name": "_affiliateAccount", "type": "address" }, + { "name": "_affiliateFee", "type": "uint256" } + ], + "name": "convertFor2", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": true, + "stateMutability": "payable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "_path", "type": "address[]" }, + { "name": "_amount", "type": "uint256" }, + { "name": "_minReturn", "type": "uint256" }, + { "name": "_beneficiary", "type": "address" } + ], + "name": "claimAndConvertFor", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "restoreRegistry", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "_path", "type": "address[]" }, + { "name": "_amount", "type": "uint256" }, + { "name": "_minReturn", "type": "uint256" }, + { "name": "_beneficiary", "type": "address" }, + { "name": "_affiliateAccount", "type": "address" }, + { "name": "_affiliateFee", "type": "uint256" } + ], + "name": "convertByPath", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": true, + "stateMutability": "payable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "_path", "type": "address[]" }, + { "name": "_amount", "type": "uint256" }, + { "name": "_minReturn", "type": "uint256" }, + { "name": "_targetBlockchain", "type": "bytes32" }, + { "name": "_targetAccount", "type": "bytes32" }, + { "name": "_conversionId", "type": "uint256" } + ], + "name": "xConvert", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": true, + "stateMutability": "payable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "_path", "type": "address[]" }, + { "name": "_amount", "type": "uint256" }, + { "name": "_minReturn", "type": "uint256" } + ], + "name": "claimAndConvert", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "_path", "type": "address[]" }, + { "name": "_amount", "type": "uint256" }, + { "name": "_minReturn", "type": "uint256" }, + { "name": "_beneficiary", "type": "address" } + ], + "name": "convertFor", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": true, + "stateMutability": "payable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "_path", "type": "address[]" }, + { "name": "_amount", "type": "uint256" }, + { "name": "_minReturn", "type": "uint256" }, + { "name": "_targetBlockchain", "type": "bytes32" }, + { "name": "_targetAccount", "type": "bytes32" }, + { "name": "_conversionId", "type": "uint256" }, + { "name": "_affiliateAccount", "type": "address" }, + { "name": "_affiliateFee", "type": "uint256" } + ], + "name": "xConvert2", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": true, + "stateMutability": "payable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "newOwner", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "name": "_sourceToken", "type": "address" }, + { "name": "_targetToken", "type": "address" } + ], + "name": "conversionPath", + "outputs": [{ "name": "", "type": "address[]" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "_path", "type": "address[]" }, + { "name": "_amount", "type": "uint256" }, + { "name": "_minReturn", "type": "uint256" }, + { "name": "_affiliateAccount", "type": "address" }, + { "name": "_affiliateFee", "type": "uint256" } + ], + "name": "claimAndConvert2", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "name": "_newOwner", "type": "address" }], + "name": "transferOwnership", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "_path", "type": "address[]" }, + { "name": "_amount", "type": "uint256" }, + { "name": "_minReturn", "type": "uint256" } + ], + "name": "convert", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": true, + "stateMutability": "payable", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "name": "_maxAffiliateFee", "type": "uint256" }], + "name": "setMaxAffiliateFee", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "name": "_registry", "type": "address" }], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "name": "_smartToken", "type": "address" }, + { "indexed": true, "name": "_fromToken", "type": "address" }, + { "indexed": true, "name": "_toToken", "type": "address" }, + { "indexed": false, "name": "_fromAmount", "type": "uint256" }, + { "indexed": false, "name": "_toAmount", "type": "uint256" }, + { "indexed": false, "name": "_trader", "type": "address" } + ], + "name": "Conversion", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "name": "_prevOwner", "type": "address" }, + { "indexed": true, "name": "_newOwner", "type": "address" } + ], + "name": "OwnerUpdate", + "type": "event" + } ] diff --git a/scripts/contractInteraction/ABIs/Watcher.json b/scripts/contractInteraction/ABIs/Watcher.json index 183bb45c2..9f60e0f09 100644 --- a/scripts/contractInteraction/ABIs/Watcher.json +++ b/scripts/contractInteraction/ABIs/Watcher.json @@ -1,294 +1,403 @@ [ - { - "inputs": [ - { "internalType": "contract ISovrynProtocol", "name": "_sovrynProtocol", "type": "address" }, - { "internalType": "contract ISovrynSwapNetwork", "name": "_sovrynSwapNetwork", "type": "address" }, - { "internalType": "contract IPriceFeeds", "name": "_priceFeeds", "type": "address" }, - { "internalType": "contract IWRBTCToken", "name": "_wrbtcToken", "type": "address" } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": true, "internalType": "address", "name": "_sourceToken", "type": "address" }, - { "indexed": true, "internalType": "address", "name": "_targetToken", "type": "address" }, - { "indexed": false, "internalType": "uint256", "name": "_sourceTokenAmount", "type": "uint256" }, - { "indexed": false, "internalType": "uint256", "name": "_targetTokenAmount", "type": "uint256" }, - { "indexed": false, "internalType": "uint256", "name": "_priceFeedAmount", "type": "uint256" }, - { "indexed": false, "internalType": "uint256", "name": "_profit", "type": "uint256" }, - { "indexed": false, "internalType": "address", "name": "_sender", "type": "address" } - ], - "name": "Arbitrage", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": false, "internalType": "bytes32", "name": "_loanId", "type": "bytes32" }, - { "indexed": true, "internalType": "address", "name": "_loanToken", "type": "address" }, - { "indexed": true, "internalType": "address", "name": "_seizedToken", "type": "address" }, - { "indexed": false, "internalType": "uint256", "name": "_closeAmount", "type": "uint256" }, - { "indexed": false, "internalType": "uint256", "name": "_seizedAmount", "type": "uint256" }, - { "indexed": false, "internalType": "address", "name": "_sender", "type": "address" } - ], - "name": "Liquidation", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": true, "internalType": "bytes32", "name": "role", "type": "bytes32" }, - { "indexed": true, "internalType": "bytes32", "name": "previousAdminRole", "type": "bytes32" }, - { "indexed": true, "internalType": "bytes32", "name": "newAdminRole", "type": "bytes32" } - ], - "name": "RoleAdminChanged", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": true, "internalType": "bytes32", "name": "role", "type": "bytes32" }, - { "indexed": true, "internalType": "address", "name": "account", "type": "address" }, - { "indexed": true, "internalType": "address", "name": "sender", "type": "address" } - ], - "name": "RoleGranted", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": true, "internalType": "bytes32", "name": "role", "type": "bytes32" }, - { "indexed": true, "internalType": "address", "name": "account", "type": "address" }, - { "indexed": true, "internalType": "address", "name": "sender", "type": "address" } - ], - "name": "RoleRevoked", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": false, "internalType": "bytes32", "name": "_loanId", "type": "bytes32" }, - { "indexed": true, "internalType": "address", "name": "_sourceToken", "type": "address" }, - { "indexed": true, "internalType": "address", "name": "_targetToken", "type": "address" }, - { "indexed": false, "internalType": "uint256", "name": "_sourceTokenAmount", "type": "uint256" }, - { "indexed": false, "internalType": "uint256", "name": "_targetTokenAmount", "type": "uint256" }, - { "indexed": false, "internalType": "address", "name": "_sender", "type": "address" } - ], - "name": "Swapback", - "type": "event" - }, - { - "inputs": [], - "name": "DEFAULT_ADMIN_ROLE", - "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "RBTC_ADDRESS", - "outputs": [{ "internalType": "address", "name": "", "type": "address" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "ROLE_EXECUTOR", - "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "ROLE_OWNER", - "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "contract IERC20[]", "name": "_conversionPath", "type": "address[]" }, - { "internalType": "uint256", "name": "_amount", "type": "uint256" }, - { "internalType": "uint256", "name": "_minProfit", "type": "uint256" } - ], - "name": "arbitrage", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "contract IERC20", "name": "_token", "type": "address" }, - { "internalType": "uint256", "name": "_amount", "type": "uint256" } - ], - "name": "depositTokens", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [{ "internalType": "bytes32", "name": "role", "type": "bytes32" }], - "name": "getRoleAdmin", - "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "bytes32", "name": "role", "type": "bytes32" }, - { "internalType": "address", "name": "account", "type": "address" } - ], - "name": "grantRole", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "bytes32", "name": "role", "type": "bytes32" }, - { "internalType": "address", "name": "account", "type": "address" } - ], - "name": "hasRole", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "bytes32", "name": "_loanId", "type": "bytes32" }, - { "internalType": "uint256", "name": "_closeAmount", "type": "uint256" } - ], - "name": "liquidate", - "outputs": [ - { "internalType": "uint256", "name": "loanCloseAmount", "type": "uint256" }, - { "internalType": "uint256", "name": "seizedAmount", "type": "uint256" }, - { "internalType": "address", "name": "seizedToken", "type": "address" } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "bytes32", "name": "_loanId", "type": "bytes32" }, - { "internalType": "uint256", "name": "_closeAmount", "type": "uint256" }, - { "internalType": "contract IERC20[]", "name": "_swapbackConversionPath", "type": "address[]" }, - { "internalType": "uint256", "name": "_swapbackMinProfit", "type": "uint256" }, - { "internalType": "bool", "name": "_requireSwapback", "type": "bool" } - ], - "name": "liquidateWithSwapback", - "outputs": [ - { "internalType": "uint256", "name": "loanCloseAmount", "type": "uint256" }, - { "internalType": "uint256", "name": "seizedAmount", "type": "uint256" }, - { "internalType": "address", "name": "seizedToken", "type": "address" } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "priceFeeds", - "outputs": [{ "internalType": "contract IPriceFeeds", "name": "", "type": "address" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "bytes32", "name": "role", "type": "bytes32" }, - { "internalType": "address", "name": "account", "type": "address" } - ], - "name": "renounceRole", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "bytes32", "name": "role", "type": "bytes32" }, - { "internalType": "address", "name": "account", "type": "address" } - ], - "name": "revokeRole", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [{ "internalType": "contract IPriceFeeds", "name": "_priceFeeds", "type": "address" }], - "name": "setPriceFeeds", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [{ "internalType": "contract ISovrynProtocol", "name": "_sovrynProtocol", "type": "address" }], - "name": "setSovrynProtocol", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [{ "internalType": "contract ISovrynSwapNetwork", "name": "_sovrynSwapNetwork", "type": "address" }], - "name": "setSovrynSwapNetwork", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [{ "internalType": "contract IWRBTCToken", "name": "_wrbtcToken", "type": "address" }], - "name": "setWRBTCToken", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "sovrynProtocol", - "outputs": [{ "internalType": "contract ISovrynProtocol", "name": "", "type": "address" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "sovrynSwapNetwork", - "outputs": [{ "internalType": "contract ISovrynSwapNetwork", "name": "", "type": "address" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [{ "internalType": "bytes4", "name": "interfaceId", "type": "bytes4" }], - "name": "supportsInterface", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "uint256", "name": "_amount", "type": "uint256" }, - { "internalType": "address payable", "name": "_receiver", "type": "address" } - ], - "name": "withdrawRbtc", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "contract IERC20", "name": "_token", "type": "address" }, - { "internalType": "uint256", "name": "_amount", "type": "uint256" }, - { "internalType": "address payable", "name": "_receiver", "type": "address" } - ], - "name": "withdrawTokens", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "wrbtcToken", - "outputs": [{ "internalType": "contract IWRBTCToken", "name": "", "type": "address" }], - "stateMutability": "view", - "type": "function" - }, - { "stateMutability": "payable", "type": "receive" } + { + "inputs": [ + { + "internalType": "contract ISovrynProtocol", + "name": "_sovrynProtocol", + "type": "address" + }, + { + "internalType": "contract ISovrynSwapNetwork", + "name": "_sovrynSwapNetwork", + "type": "address" + }, + { "internalType": "contract IPriceFeeds", "name": "_priceFeeds", "type": "address" }, + { "internalType": "contract IWRBTCToken", "name": "_wrbtcToken", "type": "address" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "_sourceToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "_targetToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_sourceTokenAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_targetTokenAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_priceFeedAmount", + "type": "uint256" + }, + { "indexed": false, "internalType": "uint256", "name": "_profit", "type": "uint256" }, + { "indexed": false, "internalType": "address", "name": "_sender", "type": "address" } + ], + "name": "Arbitrage", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": false, "internalType": "bytes32", "name": "_loanId", "type": "bytes32" }, + { + "indexed": true, + "internalType": "address", + "name": "_loanToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "_seizedToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_closeAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_seizedAmount", + "type": "uint256" + }, + { "indexed": false, "internalType": "address", "name": "_sender", "type": "address" } + ], + "name": "Liquidation", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { + "indexed": true, + "internalType": "bytes32", + "name": "previousAdminRole", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "newAdminRole", + "type": "bytes32" + } + ], + "name": "RoleAdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "indexed": true, "internalType": "address", "name": "account", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "sender", "type": "address" } + ], + "name": "RoleGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "indexed": true, "internalType": "address", "name": "account", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "sender", "type": "address" } + ], + "name": "RoleRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": false, "internalType": "bytes32", "name": "_loanId", "type": "bytes32" }, + { + "indexed": true, + "internalType": "address", + "name": "_sourceToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "_targetToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_sourceTokenAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_targetTokenAmount", + "type": "uint256" + }, + { "indexed": false, "internalType": "address", "name": "_sender", "type": "address" } + ], + "name": "Swapback", + "type": "event" + }, + { + "inputs": [], + "name": "DEFAULT_ADMIN_ROLE", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "RBTC_ADDRESS", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "ROLE_EXECUTOR", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "ROLE_OWNER", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20[]", + "name": "_conversionPath", + "type": "address[]" + }, + { "internalType": "uint256", "name": "_amount", "type": "uint256" }, + { "internalType": "uint256", "name": "_minProfit", "type": "uint256" } + ], + "name": "arbitrage", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "contract IERC20", "name": "_token", "type": "address" }, + { "internalType": "uint256", "name": "_amount", "type": "uint256" } + ], + "name": "depositTokens", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [{ "internalType": "bytes32", "name": "role", "type": "bytes32" }], + "name": "getRoleAdmin", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "internalType": "address", "name": "account", "type": "address" } + ], + "name": "grantRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "internalType": "address", "name": "account", "type": "address" } + ], + "name": "hasRole", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "_loanId", "type": "bytes32" }, + { "internalType": "uint256", "name": "_closeAmount", "type": "uint256" } + ], + "name": "liquidate", + "outputs": [ + { "internalType": "uint256", "name": "loanCloseAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "seizedAmount", "type": "uint256" }, + { "internalType": "address", "name": "seizedToken", "type": "address" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "_loanId", "type": "bytes32" }, + { "internalType": "uint256", "name": "_closeAmount", "type": "uint256" }, + { + "internalType": "contract IERC20[]", + "name": "_swapbackConversionPath", + "type": "address[]" + }, + { "internalType": "uint256", "name": "_swapbackMinProfit", "type": "uint256" }, + { "internalType": "bool", "name": "_requireSwapback", "type": "bool" } + ], + "name": "liquidateWithSwapback", + "outputs": [ + { "internalType": "uint256", "name": "loanCloseAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "seizedAmount", "type": "uint256" }, + { "internalType": "address", "name": "seizedToken", "type": "address" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "priceFeeds", + "outputs": [{ "internalType": "contract IPriceFeeds", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "internalType": "address", "name": "account", "type": "address" } + ], + "name": "renounceRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "internalType": "address", "name": "account", "type": "address" } + ], + "name": "revokeRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "contract IPriceFeeds", "name": "_priceFeeds", "type": "address" } + ], + "name": "setPriceFeeds", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract ISovrynProtocol", + "name": "_sovrynProtocol", + "type": "address" + } + ], + "name": "setSovrynProtocol", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract ISovrynSwapNetwork", + "name": "_sovrynSwapNetwork", + "type": "address" + } + ], + "name": "setSovrynSwapNetwork", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "contract IWRBTCToken", "name": "_wrbtcToken", "type": "address" } + ], + "name": "setWRBTCToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "sovrynProtocol", + "outputs": [{ "internalType": "contract ISovrynProtocol", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "sovrynSwapNetwork", + "outputs": [ + { "internalType": "contract ISovrynSwapNetwork", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "bytes4", "name": "interfaceId", "type": "bytes4" }], + "name": "supportsInterface", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "_amount", "type": "uint256" }, + { "internalType": "address payable", "name": "_receiver", "type": "address" } + ], + "name": "withdrawRbtc", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "contract IERC20", "name": "_token", "type": "address" }, + { "internalType": "uint256", "name": "_amount", "type": "uint256" }, + { "internalType": "address payable", "name": "_receiver", "type": "address" } + ], + "name": "withdrawTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "wrbtcToken", + "outputs": [{ "internalType": "contract IWRBTCToken", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { "stateMutability": "payable", "type": "receive" } ] diff --git a/scripts/contractInteraction/ABIs/aggregator.json b/scripts/contractInteraction/ABIs/aggregator.json index fe6ee8966..409956346 100644 --- a/scripts/contractInteraction/ABIs/aggregator.json +++ b/scripts/contractInteraction/ABIs/aggregator.json @@ -1,1097 +1,1097 @@ [ - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "owner", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "spender", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "name": "Approval", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "cacheSize", - "type": "uint256" - } - ], - "name": "CacheSizeChanged", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "forgeValidator", - "type": "address" - } - ], - "name": "ForgeValidatorChanged", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "minter", - "type": "address" - }, - { - "indexed": false, - "internalType": "address", - "name": "recipient", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "mAssetQuantity", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "address", - "name": "bAsset", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "bAssetQuantity", - "type": "uint256" - } - ], - "name": "Minted", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "minter", - "type": "address" - }, - { - "indexed": false, - "internalType": "address", - "name": "recipient", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "mAssetQuantity", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "address[]", - "name": "bAssets", - "type": "address[]" - }, - { - "indexed": false, - "internalType": "uint256[]", - "name": "bAssetQuantities", - "type": "uint256[]" - } - ], - "name": "MintedMulti", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "payer", - "type": "address" - }, - { - "indexed": false, - "internalType": "address", - "name": "asset", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "feeQuantity", - "type": "uint256" - } - ], - "name": "PaidFee", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "redeemer", - "type": "address" - }, - { - "indexed": false, - "internalType": "address", - "name": "recipient", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "mAssetQuantity", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "address[]", - "name": "bAssets", - "type": "address[]" - }, - { - "indexed": false, - "internalType": "uint256[]", - "name": "bAssetQuantities", - "type": "uint256[]" - } - ], - "name": "Redeemed", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "redeemer", - "type": "address" - }, - { - "indexed": false, - "internalType": "address", - "name": "recipient", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "mAssetQuantity", - "type": "uint256" - } - ], - "name": "RedeemedMasset", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "fee", - "type": "uint256" - } - ], - "name": "RedemptionFeeChanged", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "fee", - "type": "uint256" - } - ], - "name": "SwapFeeChanged", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "swapper", - "type": "address" - }, - { - "indexed": false, - "internalType": "address", - "name": "input", - "type": "address" - }, - { - "indexed": false, - "internalType": "address", - "name": "output", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "outputAmount", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "address", - "name": "recipient", - "type": "address" - } - ], - "name": "Swapped", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "name": "Transfer", - "type": "event" - }, - { - "constant": true, - "inputs": [ - { - "internalType": "address", - "name": "owner", - "type": "address" - }, - { - "internalType": "address", - "name": "spender", - "type": "address" - } - ], - "name": "allowance", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address", - "name": "spender", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "approve", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "balanceOf", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "cacheSize", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "collectInterest", - "outputs": [ - { - "internalType": "uint256", - "name": "swapFeesGained", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "newSupply", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "collectPlatformInterest", - "outputs": [ - { - "internalType": "uint256", - "name": "interestGained", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "newSupply", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "decimals", - "outputs": [ - { - "internalType": "uint8", - "name": "", - "type": "uint8" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address", - "name": "spender", - "type": "address" - }, - { - "internalType": "uint256", - "name": "subtractedValue", - "type": "uint256" - } - ], - "name": "decreaseAllowance", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "forgeValidator", - "outputs": [ - { - "internalType": "contract IForgeValidator", - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "getBasketManager", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "internalType": "address", - "name": "_input", - "type": "address" - }, - { - "internalType": "address", - "name": "_output", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_quantity", - "type": "uint256" - } - ], - "name": "getSwapOutput", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - }, - { - "internalType": "string", - "name": "", - "type": "string" - }, - { - "internalType": "uint256", - "name": "output", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address", - "name": "spender", - "type": "address" - }, - { - "internalType": "uint256", - "name": "addedValue", - "type": "uint256" - } - ], - "name": "increaseAllowance", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "string", - "name": "_nameArg", - "type": "string" - }, - { - "internalType": "string", - "name": "_symbolArg", - "type": "string" - }, - { - "internalType": "address", - "name": "_nexus", - "type": "address" - }, - { - "internalType": "address", - "name": "_forgeValidator", - "type": "address" - }, - { - "internalType": "address", - "name": "_basketManager", - "type": "address" - } - ], - "name": "initialize", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address", - "name": "_basset", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_massetQuantity", - "type": "uint256" - }, - { - "internalType": "address", - "name": "_recipient", - "type": "address" - } - ], - "name": "redeemToBridge", - "outputs": [ - { - "internalType": "uint256", - "name": "massetRedeemed", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "lockForgeValidator", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address", - "name": "_bAsset", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_bAssetQuantity", - "type": "uint256" - } - ], - "name": "mint", - "outputs": [ - { - "internalType": "uint256", - "name": "massetMinted", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address[]", - "name": "_bAssets", - "type": "address[]" - }, - { - "internalType": "uint256[]", - "name": "_bAssetQuantity", - "type": "uint256[]" - }, - { - "internalType": "address", - "name": "_recipient", - "type": "address" - } - ], - "name": "mintMulti", - "outputs": [ - { - "internalType": "uint256", - "name": "massetMinted", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address", - "name": "_bAsset", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_bAssetQuantity", - "type": "uint256" - }, - { - "internalType": "address", - "name": "_recipient", - "type": "address" - } - ], - "name": "mintTo", - "outputs": [ - { - "internalType": "uint256", - "name": "massetMinted", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "name", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "nexus", - "outputs": [ - { - "internalType": "contract INexus", - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address", - "name": "_bAsset", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_bAssetQuantity", - "type": "uint256" - } - ], - "name": "redeem", - "outputs": [ - { - "internalType": "uint256", - "name": "massetRedeemed", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "uint256", - "name": "_mAssetQuantity", - "type": "uint256" - }, - { - "internalType": "address", - "name": "_recipient", - "type": "address" - } - ], - "name": "redeemMasset", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address[]", - "name": "_bAssets", - "type": "address[]" - }, - { - "internalType": "uint256[]", - "name": "_bAssetQuantities", - "type": "uint256[]" - }, - { - "internalType": "address", - "name": "_recipient", - "type": "address" - } - ], - "name": "redeemMulti", - "outputs": [ - { - "internalType": "uint256", - "name": "massetRedeemed", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address", - "name": "_bAsset", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_bAssetQuantity", - "type": "uint256" - }, - { - "internalType": "address", - "name": "_recipient", - "type": "address" - } - ], - "name": "redeemTo", - "outputs": [ - { - "internalType": "uint256", - "name": "massetRedeemed", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "redemptionFee", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "uint256", - "name": "_cacheSize", - "type": "uint256" - } - ], - "name": "setCacheSize", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "uint256", - "name": "_redemptionFee", - "type": "uint256" - } - ], - "name": "setRedemptionFee", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "uint256", - "name": "_swapFee", - "type": "uint256" - } - ], - "name": "setSwapFee", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "surplus", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address", - "name": "_input", - "type": "address" - }, - { - "internalType": "address", - "name": "_output", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_quantity", - "type": "uint256" - }, - { - "internalType": "address", - "name": "_recipient", - "type": "address" - } - ], - "name": "swap", - "outputs": [ - { - "internalType": "uint256", - "name": "output", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "swapFee", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "symbol", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "totalSupply", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address", - "name": "recipient", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "transfer", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address", - "name": "sender", - "type": "address" - }, - { - "internalType": "address", - "name": "recipient", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "transferFrom", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address", - "name": "_newForgeValidator", - "type": "address" - } - ], - "name": "upgradeForgeValidator", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - } + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "cacheSize", + "type": "uint256" + } + ], + "name": "CacheSizeChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "forgeValidator", + "type": "address" + } + ], + "name": "ForgeValidatorChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "minter", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "mAssetQuantity", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "bAsset", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "bAssetQuantity", + "type": "uint256" + } + ], + "name": "Minted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "minter", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "mAssetQuantity", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address[]", + "name": "bAssets", + "type": "address[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "bAssetQuantities", + "type": "uint256[]" + } + ], + "name": "MintedMulti", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "payer", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "feeQuantity", + "type": "uint256" + } + ], + "name": "PaidFee", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "redeemer", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "mAssetQuantity", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address[]", + "name": "bAssets", + "type": "address[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "bAssetQuantities", + "type": "uint256[]" + } + ], + "name": "Redeemed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "redeemer", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "mAssetQuantity", + "type": "uint256" + } + ], + "name": "RedeemedMasset", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "RedemptionFeeChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "SwapFeeChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "swapper", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "input", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "output", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "outputAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + } + ], + "name": "Swapped", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "cacheSize", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "collectInterest", + "outputs": [ + { + "internalType": "uint256", + "name": "swapFeesGained", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "newSupply", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "collectPlatformInterest", + "outputs": [ + { + "internalType": "uint256", + "name": "interestGained", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "newSupply", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "subtractedValue", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "forgeValidator", + "outputs": [ + { + "internalType": "contract IForgeValidator", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getBasketManager", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "_input", + "type": "address" + }, + { + "internalType": "address", + "name": "_output", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_quantity", + "type": "uint256" + } + ], + "name": "getSwapOutput", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + }, + { + "internalType": "string", + "name": "", + "type": "string" + }, + { + "internalType": "uint256", + "name": "output", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "addedValue", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "string", + "name": "_nameArg", + "type": "string" + }, + { + "internalType": "string", + "name": "_symbolArg", + "type": "string" + }, + { + "internalType": "address", + "name": "_nexus", + "type": "address" + }, + { + "internalType": "address", + "name": "_forgeValidator", + "type": "address" + }, + { + "internalType": "address", + "name": "_basketManager", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "_basset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_massetQuantity", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_recipient", + "type": "address" + } + ], + "name": "redeemToBridge", + "outputs": [ + { + "internalType": "uint256", + "name": "massetRedeemed", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "lockForgeValidator", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "_bAsset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_bAssetQuantity", + "type": "uint256" + } + ], + "name": "mint", + "outputs": [ + { + "internalType": "uint256", + "name": "massetMinted", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address[]", + "name": "_bAssets", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "_bAssetQuantity", + "type": "uint256[]" + }, + { + "internalType": "address", + "name": "_recipient", + "type": "address" + } + ], + "name": "mintMulti", + "outputs": [ + { + "internalType": "uint256", + "name": "massetMinted", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "_bAsset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_bAssetQuantity", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_recipient", + "type": "address" + } + ], + "name": "mintTo", + "outputs": [ + { + "internalType": "uint256", + "name": "massetMinted", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "nexus", + "outputs": [ + { + "internalType": "contract INexus", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "_bAsset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_bAssetQuantity", + "type": "uint256" + } + ], + "name": "redeem", + "outputs": [ + { + "internalType": "uint256", + "name": "massetRedeemed", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "uint256", + "name": "_mAssetQuantity", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_recipient", + "type": "address" + } + ], + "name": "redeemMasset", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address[]", + "name": "_bAssets", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "_bAssetQuantities", + "type": "uint256[]" + }, + { + "internalType": "address", + "name": "_recipient", + "type": "address" + } + ], + "name": "redeemMulti", + "outputs": [ + { + "internalType": "uint256", + "name": "massetRedeemed", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "_bAsset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_bAssetQuantity", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_recipient", + "type": "address" + } + ], + "name": "redeemTo", + "outputs": [ + { + "internalType": "uint256", + "name": "massetRedeemed", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "redemptionFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "uint256", + "name": "_cacheSize", + "type": "uint256" + } + ], + "name": "setCacheSize", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "uint256", + "name": "_redemptionFee", + "type": "uint256" + } + ], + "name": "setRedemptionFee", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "uint256", + "name": "_swapFee", + "type": "uint256" + } + ], + "name": "setSwapFee", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "surplus", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "_input", + "type": "address" + }, + { + "internalType": "address", + "name": "_output", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_quantity", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_recipient", + "type": "address" + } + ], + "name": "swap", + "outputs": [ + { + "internalType": "uint256", + "name": "output", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "swapFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "_newForgeValidator", + "type": "address" + } + ], + "name": "upgradeForgeValidator", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + } ] diff --git a/scripts/contractInteraction/ABIs/crowd.json b/scripts/contractInteraction/ABIs/crowd.json index 4209977c5..620442d50 100644 --- a/scripts/contractInteraction/ABIs/crowd.json +++ b/scripts/contractInteraction/ABIs/crowd.json @@ -1,469 +1,469 @@ [ - { - "inputs": [], - "name": "buy", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [], - "name": "buyNOimburse", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [], - "name": "fakeBuy", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "CSOVAddress", - "type": "address" - }, - { - "internalType": "address[]", - "name": "_NFTAddresses", - "type": "address[]" - }, - { - "internalType": "uint256[]", - "name": "maxDepositList", - "type": "uint256[]" - }, - { - "internalType": "address payable", - "name": "_sovrynAddress", - "type": "address" - } - ], - "stateMutability": "payable", - "type": "constructor" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "total", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "sale", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "minp", - "type": "uint256" - } - ], - "name": "CrowdSaleStarted", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address payable", - "name": "imbursePurchaser", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "Imburse", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "previousOwner", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "OwnershipTransferred", - "type": "event" - }, - { - "inputs": [ - { - "internalType": "bool", - "name": "isSaleEnded", - "type": "bool" - } - ], - "name": "saleClosure", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "duration", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "_rate", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "_minPurchase", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "_crowdSaleSupply", - "type": "uint256" - } - ], - "name": "start", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address payable", - "name": "purchaser", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "value", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "TokenPurchase", - "type": "event" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "transferOwnership", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "withdrawFunds", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address payable", - "name": "Sovryn", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "value", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amountDeposited", - "type": "uint256" - } - ], - "name": "WithdrawFunds", - "type": "event" - }, - { - "inputs": [], - "name": "withdrawTokens", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "availableTokens", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "owner", - "type": "address" - } - ], - "name": "balanceOf", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "crowdSaleSupply", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "depositAllowed", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "end", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address payable", - "name": "investor", - "type": "address" - } - ], - "name": "getMaxPurchase", - "outputs": [ - { - "internalType": "uint256", - "name": "maxpurchase", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "name": "InvestorTotalDeposits", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "minPurchase", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "owner", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "rate", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "RBTCDepositRequest", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "reimburseRBTC", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "satRaised", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "sovrynAddress", - "outputs": [ - { - "internalType": "address payable", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "token", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "tokenTotalSupply", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "TotalSaleBalanceSat", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - } + { + "inputs": [], + "name": "buy", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "buyNOimburse", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "fakeBuy", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "CSOVAddress", + "type": "address" + }, + { + "internalType": "address[]", + "name": "_NFTAddresses", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "maxDepositList", + "type": "uint256[]" + }, + { + "internalType": "address payable", + "name": "_sovrynAddress", + "type": "address" + } + ], + "stateMutability": "payable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "total", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "sale", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "minp", + "type": "uint256" + } + ], + "name": "CrowdSaleStarted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address payable", + "name": "imbursePurchaser", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Imburse", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "isSaleEnded", + "type": "bool" + } + ], + "name": "saleClosure", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "duration", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_rate", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_minPurchase", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_crowdSaleSupply", + "type": "uint256" + } + ], + "name": "start", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address payable", + "name": "purchaser", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "TokenPurchase", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "withdrawFunds", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address payable", + "name": "Sovryn", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountDeposited", + "type": "uint256" + } + ], + "name": "WithdrawFunds", + "type": "event" + }, + { + "inputs": [], + "name": "withdrawTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "availableTokens", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "crowdSaleSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "depositAllowed", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "end", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address payable", + "name": "investor", + "type": "address" + } + ], + "name": "getMaxPurchase", + "outputs": [ + { + "internalType": "uint256", + "name": "maxpurchase", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "InvestorTotalDeposits", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "minPurchase", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rate", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "RBTCDepositRequest", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "reimburseRBTC", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "satRaised", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "sovrynAddress", + "outputs": [ + { + "internalType": "address payable", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "token", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "tokenTotalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "TotalSaleBalanceSat", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } ] diff --git a/scripts/contractInteraction/bridge-multisig/Bridge.json b/scripts/contractInteraction/bridge-multisig/Bridge.json index 8eb7128a3..229794060 100644 --- a/scripts/contractInteraction/bridge-multisig/Bridge.json +++ b/scripts/contractInteraction/bridge-multisig/Bridge.json @@ -1,758 +1,758 @@ [ - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "_tokenAddress", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "_to", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "_amount", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint8", - "name": "_decimals", - "type": "uint8" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "_granularity", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "_formattedAmount", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint8", - "name": "_calculatedDecimals", - "type": "uint8" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "_calculatedGranularity", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "bytes", - "name": "_userData", - "type": "bytes" - } - ], - "name": "AcceptedCrossTransfer", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "_tokenAddress", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "_to", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "_amount", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "string", - "name": "_symbol", - "type": "string" - }, - { - "indexed": false, - "internalType": "bytes", - "name": "_userData", - "type": "bytes" - }, - { - "indexed": false, - "internalType": "uint8", - "name": "_decimals", - "type": "uint8" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "_granularity", - "type": "uint256" - } - ], - "name": "Cross", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "bytes", - "name": "_errorData", - "type": "bytes" - } - ], - "name": "ErrorTokenReceiver", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "_newFederation", - "type": "address" - } - ], - "name": "FederationChanged", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "_newSideTokenAddress", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "_originalTokenAddress", - "type": "address" - }, - { - "indexed": false, - "internalType": "string", - "name": "_newSymbol", - "type": "string" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "_granularity", - "type": "uint256" - } - ], - "name": "NewSideToken", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "previousOwner", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "OwnershipTransferred", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "Paused", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "PauserAdded", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "PauserRemoved", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "_newSideTokenFactory", - "type": "address" - } - ], - "name": "SideTokenFactoryChanged", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "Unpaused", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "bool", - "name": "isUpgrading", - "type": "bool" - } - ], - "name": "Upgrading", - "type": "event" - }, - { - "constant": false, - "inputs": [{ "internalType": "address", "name": "account", "type": "address" }], - "name": "addPauser", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "allowTokens", - "outputs": [{ "internalType": "contract IAllowTokens", "name": "", "type": "address" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "ethFeeCollected", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "feePercentageDivider", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "initialPrefixSetup", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "initialize", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [{ "internalType": "address", "name": "sender", "type": "address" }], - "name": "initialize", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "isOwner", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [{ "internalType": "address", "name": "account", "type": "address" }], - "name": "isPauser", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "isSuffix", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "isUpgrading", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [{ "internalType": "address", "name": "", "type": "address" }], - "name": "knownTokens", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "lastDay", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [{ "internalType": "address", "name": "", "type": "address" }], - "name": "mappedTokens", - "outputs": [{ "internalType": "contract ISideToken", "name": "", "type": "address" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [{ "internalType": "address", "name": "", "type": "address" }], - "name": "originalTokens", - "outputs": [{ "internalType": "address", "name": "", "type": "address" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "owner", - "outputs": [{ "internalType": "address", "name": "", "type": "address" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "pause", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "paused", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], - "name": "processed", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "renounceOwnership", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "renouncePauser", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "sideTokenFactory", - "outputs": [ - { - "internalType": "contract ISideTokenFactory", - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "spentToday", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "symbolPrefix", - "outputs": [{ "internalType": "string", "name": "", "type": "string" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [{ "internalType": "address", "name": "newOwner", "type": "address" }], - "name": "transferOwnership", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "unpause", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "version", - "outputs": [{ "internalType": "string", "name": "", "type": "string" }], - "payable": false, - "stateMutability": "pure", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "internalType": "address", "name": "tokenAddress", "type": "address" }, - { "internalType": "address", "name": "receiver", "type": "address" }, - { "internalType": "uint256", "name": "amount", "type": "uint256" }, - { "internalType": "string", "name": "symbol", "type": "string" }, - { "internalType": "bytes32", "name": "blockHash", "type": "bytes32" }, - { - "internalType": "bytes32", - "name": "transactionHash", - "type": "bytes32" - }, - { "internalType": "uint32", "name": "logIndex", "type": "uint32" }, - { "internalType": "uint8", "name": "decimals", "type": "uint8" }, - { "internalType": "uint256", "name": "granularity", "type": "uint256" } - ], - "name": "acceptTransfer", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "internalType": "address", "name": "tokenAddress", "type": "address" }, - { "internalType": "address", "name": "receiver", "type": "address" }, - { "internalType": "uint256", "name": "amount", "type": "uint256" }, - { "internalType": "string", "name": "symbol", "type": "string" }, - { "internalType": "bytes32", "name": "blockHash", "type": "bytes32" }, - { - "internalType": "bytes32", - "name": "transactionHash", - "type": "bytes32" - }, - { "internalType": "uint32", "name": "logIndex", "type": "uint32" }, - { "internalType": "uint8", "name": "decimals", "type": "uint8" }, - { "internalType": "uint256", "name": "granularity", "type": "uint256" }, - { "internalType": "bytes", "name": "userData", "type": "bytes" } - ], - "name": "acceptTransferAt", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "internalType": "address", "name": "tokenToUse", "type": "address" }, - { "internalType": "uint256", "name": "amount", "type": "uint256" }, - { "internalType": "address", "name": "receiver", "type": "address" }, - { "internalType": "bytes", "name": "extraData", "type": "bytes" } - ], - "name": "receiveTokensAt", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "internalType": "address", "name": "tokenToUse", "type": "address" }, - { "internalType": "uint256", "name": "amount", "type": "uint256" } - ], - "name": "receiveTokens", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "internalType": "address", "name": "operator", "type": "address" }, - { "internalType": "address", "name": "from", "type": "address" }, - { "internalType": "address", "name": "to", "type": "address" }, - { "internalType": "uint256", "name": "amount", "type": "uint256" }, - { "internalType": "bytes", "name": "userData", "type": "bytes" }, - { "internalType": "bytes", "name": "", "type": "bytes" } - ], - "name": "tokensReceived", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "internalType": "bytes32", "name": "_blockHash", "type": "bytes32" }, - { - "internalType": "bytes32", - "name": "_transactionHash", - "type": "bytes32" - }, - { "internalType": "address", "name": "_receiver", "type": "address" }, - { "internalType": "uint256", "name": "_amount", "type": "uint256" }, - { "internalType": "uint32", "name": "_logIndex", "type": "uint32" } - ], - "name": "getTransactionId", - "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], - "payable": false, - "stateMutability": "pure", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "calcMaxWithdraw", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [{ "internalType": "address", "name": "newFederation", "type": "address" }], - "name": "changeFederation", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "getFederation", - "outputs": [{ "internalType": "address", "name": "", "type": "address" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address", - "name": "newSideTokenFactory", - "type": "address" - } - ], - "name": "changeSideTokenFactory", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "startUpgrade", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "endUpgrade", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "internalType": "address", "name": "_receiver", "type": "address" }, - { "internalType": "bytes", "name": "_extraData", "type": "bytes" } - ], - "name": "recieveEthAt", - "outputs": [], - "payable": true, - "stateMutability": "payable", - "type": "function" - }, - { - "constant": false, - "inputs": [{ "internalType": "address", "name": "_WETHAddr", "type": "address" }], - "name": "setWETHAddress", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [{ "internalType": "address", "name": "newAllowTokens", "type": "address" }], - "name": "changeAllowTokens", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "internalType": "bool", "name": "_isSuffix", "type": "bool" }, - { "internalType": "string", "name": "_prefix", "type": "string" } - ], - "name": "initialSymbolPrefixSetup", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [{ "internalType": "address payable", "name": "_to", "type": "address" }], - "name": "withdrawAllEthFees", - "outputs": [], - "payable": true, - "stateMutability": "payable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "string", - "name": "_nativeTokenSymbol", - "type": "string" - } - ], - "name": "setNativeTokenSymbol", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "getNativeTokenSymbol", - "outputs": [{ "internalType": "string", "name": "", "type": "string" }], - "payable": false, - "stateMutability": "view", - "type": "function" - } + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "_tokenAddress", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "_decimals", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_granularity", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_formattedAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "_calculatedDecimals", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_calculatedGranularity", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "_userData", + "type": "bytes" + } + ], + "name": "AcceptedCrossTransfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "_tokenAddress", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "string", + "name": "_symbol", + "type": "string" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "_userData", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "_decimals", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_granularity", + "type": "uint256" + } + ], + "name": "Cross", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes", + "name": "_errorData", + "type": "bytes" + } + ], + "name": "ErrorTokenReceiver", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "_newFederation", + "type": "address" + } + ], + "name": "FederationChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "_newSideTokenAddress", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "_originalTokenAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "string", + "name": "_newSymbol", + "type": "string" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_granularity", + "type": "uint256" + } + ], + "name": "NewSideToken", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "PauserAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "PauserRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "_newSideTokenFactory", + "type": "address" + } + ], + "name": "SideTokenFactoryChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bool", + "name": "isUpgrading", + "type": "bool" + } + ], + "name": "Upgrading", + "type": "event" + }, + { + "constant": false, + "inputs": [{ "internalType": "address", "name": "account", "type": "address" }], + "name": "addPauser", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "allowTokens", + "outputs": [{ "internalType": "contract IAllowTokens", "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "ethFeeCollected", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "feePercentageDivider", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "initialPrefixSetup", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "initialize", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "internalType": "address", "name": "sender", "type": "address" }], + "name": "initialize", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "isOwner", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "internalType": "address", "name": "account", "type": "address" }], + "name": "isPauser", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "isSuffix", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "isUpgrading", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "knownTokens", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "lastDay", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "mappedTokens", + "outputs": [{ "internalType": "contract ISideToken", "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "originalTokens", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "owner", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "pause", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "paused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "name": "processed", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "renouncePauser", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "sideTokenFactory", + "outputs": [ + { + "internalType": "contract ISideTokenFactory", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "spentToday", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbolPrefix", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "internalType": "address", "name": "newOwner", "type": "address" }], + "name": "transferOwnership", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "unpause", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "version", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "payable": false, + "stateMutability": "pure", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "tokenAddress", "type": "address" }, + { "internalType": "address", "name": "receiver", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "internalType": "string", "name": "symbol", "type": "string" }, + { "internalType": "bytes32", "name": "blockHash", "type": "bytes32" }, + { + "internalType": "bytes32", + "name": "transactionHash", + "type": "bytes32" + }, + { "internalType": "uint32", "name": "logIndex", "type": "uint32" }, + { "internalType": "uint8", "name": "decimals", "type": "uint8" }, + { "internalType": "uint256", "name": "granularity", "type": "uint256" } + ], + "name": "acceptTransfer", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "tokenAddress", "type": "address" }, + { "internalType": "address", "name": "receiver", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "internalType": "string", "name": "symbol", "type": "string" }, + { "internalType": "bytes32", "name": "blockHash", "type": "bytes32" }, + { + "internalType": "bytes32", + "name": "transactionHash", + "type": "bytes32" + }, + { "internalType": "uint32", "name": "logIndex", "type": "uint32" }, + { "internalType": "uint8", "name": "decimals", "type": "uint8" }, + { "internalType": "uint256", "name": "granularity", "type": "uint256" }, + { "internalType": "bytes", "name": "userData", "type": "bytes" } + ], + "name": "acceptTransferAt", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "tokenToUse", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "internalType": "address", "name": "receiver", "type": "address" }, + { "internalType": "bytes", "name": "extraData", "type": "bytes" } + ], + "name": "receiveTokensAt", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "tokenToUse", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "receiveTokens", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "operator", "type": "address" }, + { "internalType": "address", "name": "from", "type": "address" }, + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "internalType": "bytes", "name": "userData", "type": "bytes" }, + { "internalType": "bytes", "name": "", "type": "bytes" } + ], + "name": "tokensReceived", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "internalType": "bytes32", "name": "_blockHash", "type": "bytes32" }, + { + "internalType": "bytes32", + "name": "_transactionHash", + "type": "bytes32" + }, + { "internalType": "address", "name": "_receiver", "type": "address" }, + { "internalType": "uint256", "name": "_amount", "type": "uint256" }, + { "internalType": "uint32", "name": "_logIndex", "type": "uint32" } + ], + "name": "getTransactionId", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "payable": false, + "stateMutability": "pure", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "calcMaxWithdraw", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "internalType": "address", "name": "newFederation", "type": "address" }], + "name": "changeFederation", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getFederation", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "newSideTokenFactory", + "type": "address" + } + ], + "name": "changeSideTokenFactory", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "startUpgrade", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "endUpgrade", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "_receiver", "type": "address" }, + { "internalType": "bytes", "name": "_extraData", "type": "bytes" } + ], + "name": "recieveEthAt", + "outputs": [], + "payable": true, + "stateMutability": "payable", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "internalType": "address", "name": "_WETHAddr", "type": "address" }], + "name": "setWETHAddress", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "internalType": "address", "name": "newAllowTokens", "type": "address" }], + "name": "changeAllowTokens", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "bool", "name": "_isSuffix", "type": "bool" }, + { "internalType": "string", "name": "_prefix", "type": "string" } + ], + "name": "initialSymbolPrefixSetup", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "internalType": "address payable", "name": "_to", "type": "address" }], + "name": "withdrawAllEthFees", + "outputs": [], + "payable": true, + "stateMutability": "payable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "string", + "name": "_nativeTokenSymbol", + "type": "string" + } + ], + "name": "setNativeTokenSymbol", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getNativeTokenSymbol", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "payable": false, + "stateMutability": "view", + "type": "function" + } ] diff --git a/scripts/contractInteraction/bridge-multisig/Masset.json b/scripts/contractInteraction/bridge-multisig/Masset.json index fe6ee8966..409956346 100644 --- a/scripts/contractInteraction/bridge-multisig/Masset.json +++ b/scripts/contractInteraction/bridge-multisig/Masset.json @@ -1,1097 +1,1097 @@ [ - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "owner", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "spender", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "name": "Approval", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "cacheSize", - "type": "uint256" - } - ], - "name": "CacheSizeChanged", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "forgeValidator", - "type": "address" - } - ], - "name": "ForgeValidatorChanged", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "minter", - "type": "address" - }, - { - "indexed": false, - "internalType": "address", - "name": "recipient", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "mAssetQuantity", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "address", - "name": "bAsset", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "bAssetQuantity", - "type": "uint256" - } - ], - "name": "Minted", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "minter", - "type": "address" - }, - { - "indexed": false, - "internalType": "address", - "name": "recipient", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "mAssetQuantity", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "address[]", - "name": "bAssets", - "type": "address[]" - }, - { - "indexed": false, - "internalType": "uint256[]", - "name": "bAssetQuantities", - "type": "uint256[]" - } - ], - "name": "MintedMulti", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "payer", - "type": "address" - }, - { - "indexed": false, - "internalType": "address", - "name": "asset", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "feeQuantity", - "type": "uint256" - } - ], - "name": "PaidFee", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "redeemer", - "type": "address" - }, - { - "indexed": false, - "internalType": "address", - "name": "recipient", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "mAssetQuantity", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "address[]", - "name": "bAssets", - "type": "address[]" - }, - { - "indexed": false, - "internalType": "uint256[]", - "name": "bAssetQuantities", - "type": "uint256[]" - } - ], - "name": "Redeemed", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "redeemer", - "type": "address" - }, - { - "indexed": false, - "internalType": "address", - "name": "recipient", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "mAssetQuantity", - "type": "uint256" - } - ], - "name": "RedeemedMasset", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "fee", - "type": "uint256" - } - ], - "name": "RedemptionFeeChanged", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "fee", - "type": "uint256" - } - ], - "name": "SwapFeeChanged", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "swapper", - "type": "address" - }, - { - "indexed": false, - "internalType": "address", - "name": "input", - "type": "address" - }, - { - "indexed": false, - "internalType": "address", - "name": "output", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "outputAmount", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "address", - "name": "recipient", - "type": "address" - } - ], - "name": "Swapped", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "name": "Transfer", - "type": "event" - }, - { - "constant": true, - "inputs": [ - { - "internalType": "address", - "name": "owner", - "type": "address" - }, - { - "internalType": "address", - "name": "spender", - "type": "address" - } - ], - "name": "allowance", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address", - "name": "spender", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "approve", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "balanceOf", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "cacheSize", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "collectInterest", - "outputs": [ - { - "internalType": "uint256", - "name": "swapFeesGained", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "newSupply", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "collectPlatformInterest", - "outputs": [ - { - "internalType": "uint256", - "name": "interestGained", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "newSupply", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "decimals", - "outputs": [ - { - "internalType": "uint8", - "name": "", - "type": "uint8" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address", - "name": "spender", - "type": "address" - }, - { - "internalType": "uint256", - "name": "subtractedValue", - "type": "uint256" - } - ], - "name": "decreaseAllowance", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "forgeValidator", - "outputs": [ - { - "internalType": "contract IForgeValidator", - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "getBasketManager", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "internalType": "address", - "name": "_input", - "type": "address" - }, - { - "internalType": "address", - "name": "_output", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_quantity", - "type": "uint256" - } - ], - "name": "getSwapOutput", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - }, - { - "internalType": "string", - "name": "", - "type": "string" - }, - { - "internalType": "uint256", - "name": "output", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address", - "name": "spender", - "type": "address" - }, - { - "internalType": "uint256", - "name": "addedValue", - "type": "uint256" - } - ], - "name": "increaseAllowance", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "string", - "name": "_nameArg", - "type": "string" - }, - { - "internalType": "string", - "name": "_symbolArg", - "type": "string" - }, - { - "internalType": "address", - "name": "_nexus", - "type": "address" - }, - { - "internalType": "address", - "name": "_forgeValidator", - "type": "address" - }, - { - "internalType": "address", - "name": "_basketManager", - "type": "address" - } - ], - "name": "initialize", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address", - "name": "_basset", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_massetQuantity", - "type": "uint256" - }, - { - "internalType": "address", - "name": "_recipient", - "type": "address" - } - ], - "name": "redeemToBridge", - "outputs": [ - { - "internalType": "uint256", - "name": "massetRedeemed", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "lockForgeValidator", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address", - "name": "_bAsset", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_bAssetQuantity", - "type": "uint256" - } - ], - "name": "mint", - "outputs": [ - { - "internalType": "uint256", - "name": "massetMinted", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address[]", - "name": "_bAssets", - "type": "address[]" - }, - { - "internalType": "uint256[]", - "name": "_bAssetQuantity", - "type": "uint256[]" - }, - { - "internalType": "address", - "name": "_recipient", - "type": "address" - } - ], - "name": "mintMulti", - "outputs": [ - { - "internalType": "uint256", - "name": "massetMinted", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address", - "name": "_bAsset", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_bAssetQuantity", - "type": "uint256" - }, - { - "internalType": "address", - "name": "_recipient", - "type": "address" - } - ], - "name": "mintTo", - "outputs": [ - { - "internalType": "uint256", - "name": "massetMinted", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "name", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "nexus", - "outputs": [ - { - "internalType": "contract INexus", - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address", - "name": "_bAsset", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_bAssetQuantity", - "type": "uint256" - } - ], - "name": "redeem", - "outputs": [ - { - "internalType": "uint256", - "name": "massetRedeemed", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "uint256", - "name": "_mAssetQuantity", - "type": "uint256" - }, - { - "internalType": "address", - "name": "_recipient", - "type": "address" - } - ], - "name": "redeemMasset", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address[]", - "name": "_bAssets", - "type": "address[]" - }, - { - "internalType": "uint256[]", - "name": "_bAssetQuantities", - "type": "uint256[]" - }, - { - "internalType": "address", - "name": "_recipient", - "type": "address" - } - ], - "name": "redeemMulti", - "outputs": [ - { - "internalType": "uint256", - "name": "massetRedeemed", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address", - "name": "_bAsset", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_bAssetQuantity", - "type": "uint256" - }, - { - "internalType": "address", - "name": "_recipient", - "type": "address" - } - ], - "name": "redeemTo", - "outputs": [ - { - "internalType": "uint256", - "name": "massetRedeemed", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "redemptionFee", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "uint256", - "name": "_cacheSize", - "type": "uint256" - } - ], - "name": "setCacheSize", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "uint256", - "name": "_redemptionFee", - "type": "uint256" - } - ], - "name": "setRedemptionFee", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "uint256", - "name": "_swapFee", - "type": "uint256" - } - ], - "name": "setSwapFee", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "surplus", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address", - "name": "_input", - "type": "address" - }, - { - "internalType": "address", - "name": "_output", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_quantity", - "type": "uint256" - }, - { - "internalType": "address", - "name": "_recipient", - "type": "address" - } - ], - "name": "swap", - "outputs": [ - { - "internalType": "uint256", - "name": "output", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "swapFee", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "symbol", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "totalSupply", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address", - "name": "recipient", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "transfer", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address", - "name": "sender", - "type": "address" - }, - { - "internalType": "address", - "name": "recipient", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "transferFrom", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address", - "name": "_newForgeValidator", - "type": "address" - } - ], - "name": "upgradeForgeValidator", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - } + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "cacheSize", + "type": "uint256" + } + ], + "name": "CacheSizeChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "forgeValidator", + "type": "address" + } + ], + "name": "ForgeValidatorChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "minter", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "mAssetQuantity", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "bAsset", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "bAssetQuantity", + "type": "uint256" + } + ], + "name": "Minted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "minter", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "mAssetQuantity", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address[]", + "name": "bAssets", + "type": "address[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "bAssetQuantities", + "type": "uint256[]" + } + ], + "name": "MintedMulti", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "payer", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "feeQuantity", + "type": "uint256" + } + ], + "name": "PaidFee", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "redeemer", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "mAssetQuantity", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address[]", + "name": "bAssets", + "type": "address[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "bAssetQuantities", + "type": "uint256[]" + } + ], + "name": "Redeemed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "redeemer", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "mAssetQuantity", + "type": "uint256" + } + ], + "name": "RedeemedMasset", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "RedemptionFeeChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "SwapFeeChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "swapper", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "input", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "output", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "outputAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + } + ], + "name": "Swapped", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "cacheSize", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "collectInterest", + "outputs": [ + { + "internalType": "uint256", + "name": "swapFeesGained", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "newSupply", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "collectPlatformInterest", + "outputs": [ + { + "internalType": "uint256", + "name": "interestGained", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "newSupply", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "subtractedValue", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "forgeValidator", + "outputs": [ + { + "internalType": "contract IForgeValidator", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getBasketManager", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "_input", + "type": "address" + }, + { + "internalType": "address", + "name": "_output", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_quantity", + "type": "uint256" + } + ], + "name": "getSwapOutput", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + }, + { + "internalType": "string", + "name": "", + "type": "string" + }, + { + "internalType": "uint256", + "name": "output", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "addedValue", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "string", + "name": "_nameArg", + "type": "string" + }, + { + "internalType": "string", + "name": "_symbolArg", + "type": "string" + }, + { + "internalType": "address", + "name": "_nexus", + "type": "address" + }, + { + "internalType": "address", + "name": "_forgeValidator", + "type": "address" + }, + { + "internalType": "address", + "name": "_basketManager", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "_basset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_massetQuantity", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_recipient", + "type": "address" + } + ], + "name": "redeemToBridge", + "outputs": [ + { + "internalType": "uint256", + "name": "massetRedeemed", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "lockForgeValidator", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "_bAsset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_bAssetQuantity", + "type": "uint256" + } + ], + "name": "mint", + "outputs": [ + { + "internalType": "uint256", + "name": "massetMinted", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address[]", + "name": "_bAssets", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "_bAssetQuantity", + "type": "uint256[]" + }, + { + "internalType": "address", + "name": "_recipient", + "type": "address" + } + ], + "name": "mintMulti", + "outputs": [ + { + "internalType": "uint256", + "name": "massetMinted", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "_bAsset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_bAssetQuantity", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_recipient", + "type": "address" + } + ], + "name": "mintTo", + "outputs": [ + { + "internalType": "uint256", + "name": "massetMinted", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "nexus", + "outputs": [ + { + "internalType": "contract INexus", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "_bAsset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_bAssetQuantity", + "type": "uint256" + } + ], + "name": "redeem", + "outputs": [ + { + "internalType": "uint256", + "name": "massetRedeemed", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "uint256", + "name": "_mAssetQuantity", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_recipient", + "type": "address" + } + ], + "name": "redeemMasset", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address[]", + "name": "_bAssets", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "_bAssetQuantities", + "type": "uint256[]" + }, + { + "internalType": "address", + "name": "_recipient", + "type": "address" + } + ], + "name": "redeemMulti", + "outputs": [ + { + "internalType": "uint256", + "name": "massetRedeemed", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "_bAsset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_bAssetQuantity", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_recipient", + "type": "address" + } + ], + "name": "redeemTo", + "outputs": [ + { + "internalType": "uint256", + "name": "massetRedeemed", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "redemptionFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "uint256", + "name": "_cacheSize", + "type": "uint256" + } + ], + "name": "setCacheSize", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "uint256", + "name": "_redemptionFee", + "type": "uint256" + } + ], + "name": "setRedemptionFee", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "uint256", + "name": "_swapFee", + "type": "uint256" + } + ], + "name": "setSwapFee", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "surplus", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "_input", + "type": "address" + }, + { + "internalType": "address", + "name": "_output", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_quantity", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_recipient", + "type": "address" + } + ], + "name": "swap", + "outputs": [ + { + "internalType": "uint256", + "name": "output", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "swapFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "_newForgeValidator", + "type": "address" + } + ], + "name": "upgradeForgeValidator", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + } ] diff --git a/scripts/contractInteraction/bsc_testnet_contracts.json b/scripts/contractInteraction/bsc_testnet_contracts.json index 6bb790e3d..5cafebb82 100644 --- a/scripts/contractInteraction/bsc_testnet_contracts.json +++ b/scripts/contractInteraction/bsc_testnet_contracts.json @@ -1,3 +1,3 @@ { - "multisig": "0xCc653b64e8f4f2aDEa87490f11d090472E08838A" + "multisig": "0xCc653b64e8f4f2aDEa87490f11d090472E08838A" } diff --git a/scripts/contractInteraction/mainnet_contracts.json b/scripts/contractInteraction/mainnet_contracts.json index d7d0b4810..6fb504282 100644 --- a/scripts/contractInteraction/mainnet_contracts.json +++ b/scripts/contractInteraction/mainnet_contracts.json @@ -1,127 +1,127 @@ { - "iDOC": "0xd8D25f03EBbA94E15Df2eD4d6D38276B595593c1", - "iRBTC": "0xa9DcDC63eaBb8a2b6f39D7fF9429d88340044a7A", - "iXUSD": "0x8F77ecf69711a4b346f23109c40416BE3dC7f129", - "iUSDT": "0x849c47f9c259e9d62f289bf1b2729039698d8387", - "iBPro": "0x6e2fb26a60da535732f8149b25018c9c0823a715", - "LoanTokenLogicLM": "0xa29E87Bcef435f1F272caE7b6b816F0b1AbD4AED", - "LoanTokenLogicWrbtc": "0x8B8e0509F8c5A06C47c78d7b8705546374909e6d", - "LoanTokenLogicBeaconLM": "0xd7FB11F4b71C6bFE9Db50f3C02c821Af8F616501", - "LoanTokenLogicBeaconWrbtc": "0xF8c5a9B496cDda3A884A9381bF717D8F35d6a86A", - "sovrynProtocol": "0x5A0D867e0D70Fcc6Ade25C3F1B89d618b5B4Eaa7", - "Affiliates": "0x30bab63262ac59218116fc6278CFe81392657f6B", - "LoanClosingsBase": "0x712C33fde72E8DF893DA603b15f6a342388cEf60", - "LoanClosingsWith": "0x3EF7f01277C717705387A71b0c9A616C8CA070E0", - "LoanOpenings": "0x7CA74c6A438AAC20Eee837E6F250F4f5D597b1Ea", - "LoanMaintenance": "0x14238FF0f1f6140b7Cf22ba613d827BFB8Aa5b64", - "SwapsExtenral": "0x103C2C8905d3688d2B6b240291883fCe0722357d", - "LoanSettings": "0x4bDe885dec79c6A06b696B642993136f6B24cCbD", - "SwapsImplSovrynSwap": "0x23bA07E2c3302B517B2cFEea4C5CaDF6a7b946bA", - "ProtocolSettings": "0x1401B5b40759893F5E5D5826Da709E4fE35C438e", - "DoC": "0xe700691da7b9851f2f35f8b8182c69c53ccad9db", - "WRBTC": "0x542fda317318ebf1d3deaf76e0b632741a7e677d", - "MOC": "0x9ac7fe28967b30e3a4e6e03286d715b42b453d10", - "ETHs": "0x1D931Bf8656d795E50eF6D639562C5bD8Ac2B78f", - "XUSD": "0xb5999795BE0EbB5bAb23144AA5FD6A02D080299F", - "BNBs": "0x4283DE93D21a91D38A39E295E0991F23E0c62941", - "FISH": "0x055A902303746382FBB7D18f6aE0df56eFDc5213", - "USDT": "0xEf213441a85DF4d7acBdAe0Cf78004E1e486BB96", - "BPro": "0x440cd83c160de5c96ddb20246815ea44c7abbca8", - "RIF": "0x2acc95758f8b5f583470ba265eb685a8f45fc9d5", - "MYNT": "0x2e6B1d146064613E8f521Eb3c6e65070af964EbB", - "swapNetwork": "0x98aCE08D2b759a265ae326F010496bcD63C15afc", - "ConverterDOC": "0xd715192612F03D20BaE53a5054aF530C9Bb0fA3f", - "ConverterBPRO": "0x26463990196B74aD5644865E4d4567E4A411e065", - "ConverterUSDT": "0x448c2474b255576554EeD36c24430ccFac131cE3", - "ConverterSOV": "0xe76Ea314b32fCf641C6c57f14110c5Baa1e45ff4", - "ConverterXUSD": "0xa9c3d9681215ef7623dc28ea6b75bf87fdf285d9", - "ConverterETHs": "0xa57ec11497f45fe86eca50f4f1c9e75c8016a1af", - "ConverterMOC": "0xe321442DC4793c17F41Fe3fB192a856A4864cEAF", - "ConverterBNBs": "0x1684b871ec5f93de142e79a670b541d75be07ead", - "ConverterFISH": "0xdeb0894196863dbb2f2d4c683f6d33a2197056b5", - "ConverterRIF": "0x65528e06371635a338ca804cd65958a11cb11009", - "ConverterMYNT": "0x3a18e61d9c9f1546dea013478dd653c793098f17", - "(WR)BTC/USDT1": "0x9c4017D1C04cFa0F97FDc9505e33a0D8ac84817F", - "(WR)BTC/USDT2": "0x40580E31cc14DbF7a0859f38Ab36A84262df821D", - "(WR)BTC/DOC1": "0x840437BdE7346EC13B5451417Df50586F4dAF836", - "(WR)BTC/DOC2": "0x2dc80332C19FBCd5169ab4a579d87eE006Cb72c0", - "(WR)BTC/BPRO1": "0x75e327A83aD2BFD53da12EB718fCCFC68Bc57535", - "(WR)BTC/BPRO2": "0x9CE25371426763025C04a9FCd581fbb9E4593475", - "(WR)BTC/SOV": "0x09c5faf7723b13434abdf1a65ab1b667bc02a902", - "(WR)BTC/ETH": "0xF41Ed702df2B84AcE02772C6a0D8AE46465aA5F4", - "(WR)BTC/MOC": "0x7fef930ebaa90B2f8619722AdC55e3f1d965b79b", - "(WR)BTC/XUSD": "0x6f96096687952349Dd5944e0eb1Be327dcDeb705", - "(WR)BTC/BNB": "0x8f3d24ab3510294f1466aa105f78901b90d79d4d", - "(WR)BTC/FISH": "0x35A74a38Fd7728F1c6BC39aE3b18C974b7979ddD", - "(WR)BTC/RIF": "0xAE66117C8105a65D914fB47d37a127E879244319", - "(WR)BTC/MYNT": "0x36263Ac99ecdCf1aB20513d580b7d8D32d3c439D", - "LiquidityMiningConfigToken": "0x513B0f20027BDc6bc2fE9e28A9d4B40d20730Dca", - "FishPoolOracle": "0x2D78E4dc352872Aa6ca12B40d18301Ff6DBE5ECB", - "XUSDPoolOracle": "0xD08eDf687418dF0107bAbCc8Fcab9064F3A6fc05", - "ETHPoolOracle": "0xD6CcacF91c3ca6705aD3047a55dC7760c5ADF1F4", - "MOCPoolOracle": "0x8Cda228A6870D7B3a9Fe1e42604b35F39Fbc5D59", - "BNBPoolOracle": "0x66464BED2420Ab2e7c2d06A9eEb67d06D7E10eAC", - "SOVPoolOracle": "0x4290243b7F3aEF0F6922dAd4F9F8d321ee320fBd", - "RIFPoolOracle": "0x6C339235Bad0381C3bb6CaCecA68C941e81a4bDc", - "MYNTPoolOracle": "0x3f9fe40Cf3013d7B8E5517B166b909911d5AFcC2", - "og": "0x81d25201D044f178883599Be1934FF53FDA98acD", - "multisig": "0x924f5ad34698Fd20c90Fe5D5A8A0abd3b42dc711", - "PriceFeeds": "0x437AC62769f386b2d238409B7f0a7596d36506e4", - "medianizer": "0x7b19bb8e6c5188ec483b784d6fb5d807a77b21bf", - "RSKOracle": "0x99eD262dbd8842442cd22d4c6885936DB38245E6", - "PriceFeedRSKOracle": "0x54c33Cb8a3a32A716BeC40C3CEB5bA8B0fB92a57", - "MOCState": "0xb9C42EFc8ec54490a37cA91c423F7285Fa01e257", - "USDTPriceFeed": "0xed80ccde8baeff2dbfc70d3028a27e501fa0d7d5", - "PriceFeedsMOC": "0x391fe8a92a7FC626A25F30E8c19B92bf8BE37FD3", - "SOVPriceFeedOnProtocol": "0xA266aA67e2a25B0CCa460DEAfcacC81D17341a0D", - "CSOV1": "0x0106F2fFBF6A4f5DEcE323d20E16E2037E732790", - "CSOV2": "0x7f7Dcf9DF951C4A332740e9a125720DA242A34ff", - "governorVault": "0xC7A1637b37190a456b017897207bceb2A29f19b9", - "SOV": "0xEFc78fc7d48b64958315949279Ba181c2114ABBd", - "Staking": "0x5684a06CaB22Db16d901fEe2A5C081b4C91eA40e", - "StakingLogic": "0x5b87F01F665050d244543ca047317C26862E47f7", - "StakingLogic2": "0x962Ce6E2Fa1A4917DfF12Ad10135b1a4F16c2DB0", - "StakingLogic3": "0xcAB5028619839dbb92a193f59026383973F04008", - "StakingLogic4": "0x4BaBb34189a9CDa8213D24DD3984058Fb8A955D2", - "FeeSharingProxyOld": "0x12B1B0C67d9A771EB5Db7726d23fdc6848fd93ef", - "FeeSharingProxy": "0x115cAF168c51eD15ec535727F64684D33B7b08D1", - "FeeSharingLogic": "0x8289AF920cA3d63245740a20116e13aAe0F978e3", - "VestingRegistryProxy": "0xe24ABdB7DcaB57F3cbe4cBDDd850D52F143eE920", - "VestingRegistryLogic": "0x536416A9fbAc10A3EA0D7f7396d0eE8FaE8146D0", - "VestingCreator": "0xa003D9F781a498D90f489328612E74Af1027417f", - "TimelockOwner": "0x967c84b731679E36A344002b8E3CE50620A7F69f", - "GovernorOwner": "0x6496DF39D000478a7A7352C01E0E713835051CcD", - "GovernorVaultOwner": "0x05f4f068DF59a5aA7911f57cE4f41ebFBcB8E247", - "TimelockAdmin": "0x6c94c8aa97C08fC31fb06fbFDa90e1E09529FB13", - "GovernorAdmin": "0xfF25f66b7D7F385503D70574AE0170b6B1622dAd", - "GovernorVaultAdmin": "0x51C754330c6cD04B810014E769Dab0343E31409E", - "VestingLogic": "0x24fbA2281202C3aaE95A3440C08C0050448508A6", - "VestingRegistry": "0x80B036ae59B3e38B573837c01BB1DB95515b7E6B", - "AdoptionFund": "0x0f31cfd6aAb4d378668Ad74DeFa89d3f4DB26633", - "DevelopmentFund": "0x617866cC4a089c3653ddC31a618b078291839AeB", - "VestingRegistry2": "0x0a9bDbf5e104a30fb4c99f6812FB85B60Fd8D372", - "VestingRegistry3": "0x14F3FE332e21Ef3f5d244C45C8D5fbFcEF2FB5c9", - "OriginInvestorsClaim": "0xE0f5BF8d0C58d9c8A078DB75A9D379E6CDF3149E", - "OrigingVestingCreator": "0xea173A078bA12673a8bef6DFa47A8E8f130B4939", - "contributorNFT": "0x8ffB12De9e7602843e4792DB0bC2863e9d137d06", - "SOVTokenSender": "0x31Ec06eB090Af6EEbfF5cF52084e9BCc3D7d9248", - "GenericTokenSender": "0xeA0BC5b4868E776368DfE9be8837Be5b57a75b6e", - "RBTCWrapperProxy": "0xa917BF723433d020a15629eba71f6C2a6B38e52d", - "RBTCWrapperProxyWithoutLM": "0xA3B6E18B9A4ECAE44C7355458Ae7Db8874018C22", - "LockedSOV": "0xB4e4517cA4Edf591Dcafb702999F04f02E57D978", - "LiquidityMiningLogic": "0xbD50232e6fbFa43c95062D1a9d6ecf5439906C21", - "LiquidityMiningProxy": "0xf730af26e87D9F55E46A6C447ED2235C385E55e0", - "ETHAggregatorProxy": "0x4BF113905d7F69202106F613308bb02C84AaDF2F", - "XUSDAggregatorProxy": "0x1440d19436bEeaF8517896bffB957a88EC95a00F", - "BNBsAggregatorProxy": "0xafD905Fe2EdBF5A7367A73B0F1e6a62Cb5E27D3e", - "BridgeRSK": "0x1CcAd820B6d031B41C54f1F3dA11c0d48b399581", - "Aggregator-ETH-RSK": "0x4bF113905d7F69202106f613308bb02c84aaDF2F", - "RSK-ETHes": "0xFe878227c8F334038DAb20a99fC3B373fFe0a755", - "StakingRewardsProxy": "0x8304FB3614c728B712e94F9D4DF6719fede6517F", - "StakingRewards": "0x6CBE8C2dFfb88973E5cC2ff4e732a0D651041215", - "Watcher": "0x051B89f575fCd540F0a6a5B49c75f9a83BB2Cf07", - "FastBTC": "0xC9e14126E5796e999890a4344b8e4c99Ac7002A1", - "FastBTCBiDi": "0x0d5006330289336EBDf9D0ac9e0674F91B4851EA", - "MyntMarketMaker": "0x722935ff8a99d801d802bb3ee528408c11c18656", - "MyntController": "0xB576658700D32CCE28552349bCD52FaD8173ae32" + "iDOC": "0xd8D25f03EBbA94E15Df2eD4d6D38276B595593c1", + "iRBTC": "0xa9DcDC63eaBb8a2b6f39D7fF9429d88340044a7A", + "iXUSD": "0x8F77ecf69711a4b346f23109c40416BE3dC7f129", + "iUSDT": "0x849c47f9c259e9d62f289bf1b2729039698d8387", + "iBPro": "0x6e2fb26a60da535732f8149b25018c9c0823a715", + "LoanTokenLogicLM": "0xa29E87Bcef435f1F272caE7b6b816F0b1AbD4AED", + "LoanTokenLogicWrbtc": "0x8B8e0509F8c5A06C47c78d7b8705546374909e6d", + "LoanTokenLogicBeaconLM": "0xd7FB11F4b71C6bFE9Db50f3C02c821Af8F616501", + "LoanTokenLogicBeaconWrbtc": "0xF8c5a9B496cDda3A884A9381bF717D8F35d6a86A", + "sovrynProtocol": "0x5A0D867e0D70Fcc6Ade25C3F1B89d618b5B4Eaa7", + "Affiliates": "0x30bab63262ac59218116fc6278CFe81392657f6B", + "LoanClosingsBase": "0x712C33fde72E8DF893DA603b15f6a342388cEf60", + "LoanClosingsWith": "0x3EF7f01277C717705387A71b0c9A616C8CA070E0", + "LoanOpenings": "0x7CA74c6A438AAC20Eee837E6F250F4f5D597b1Ea", + "LoanMaintenance": "0x14238FF0f1f6140b7Cf22ba613d827BFB8Aa5b64", + "SwapsExtenral": "0x103C2C8905d3688d2B6b240291883fCe0722357d", + "LoanSettings": "0x4bDe885dec79c6A06b696B642993136f6B24cCbD", + "SwapsImplSovrynSwap": "0x23bA07E2c3302B517B2cFEea4C5CaDF6a7b946bA", + "ProtocolSettings": "0x1401B5b40759893F5E5D5826Da709E4fE35C438e", + "DoC": "0xe700691da7b9851f2f35f8b8182c69c53ccad9db", + "WRBTC": "0x542fda317318ebf1d3deaf76e0b632741a7e677d", + "MOC": "0x9ac7fe28967b30e3a4e6e03286d715b42b453d10", + "ETHs": "0x1D931Bf8656d795E50eF6D639562C5bD8Ac2B78f", + "XUSD": "0xb5999795BE0EbB5bAb23144AA5FD6A02D080299F", + "BNBs": "0x4283DE93D21a91D38A39E295E0991F23E0c62941", + "FISH": "0x055A902303746382FBB7D18f6aE0df56eFDc5213", + "USDT": "0xEf213441a85DF4d7acBdAe0Cf78004E1e486BB96", + "BPro": "0x440cd83c160de5c96ddb20246815ea44c7abbca8", + "RIF": "0x2acc95758f8b5f583470ba265eb685a8f45fc9d5", + "MYNT": "0x2e6B1d146064613E8f521Eb3c6e65070af964EbB", + "swapNetwork": "0x98aCE08D2b759a265ae326F010496bcD63C15afc", + "ConverterDOC": "0xd715192612F03D20BaE53a5054aF530C9Bb0fA3f", + "ConverterBPRO": "0x26463990196B74aD5644865E4d4567E4A411e065", + "ConverterUSDT": "0x448c2474b255576554EeD36c24430ccFac131cE3", + "ConverterSOV": "0xe76Ea314b32fCf641C6c57f14110c5Baa1e45ff4", + "ConverterXUSD": "0xa9c3d9681215ef7623dc28ea6b75bf87fdf285d9", + "ConverterETHs": "0xa57ec11497f45fe86eca50f4f1c9e75c8016a1af", + "ConverterMOC": "0xe321442DC4793c17F41Fe3fB192a856A4864cEAF", + "ConverterBNBs": "0x1684b871ec5f93de142e79a670b541d75be07ead", + "ConverterFISH": "0xdeb0894196863dbb2f2d4c683f6d33a2197056b5", + "ConverterRIF": "0x65528e06371635a338ca804cd65958a11cb11009", + "ConverterMYNT": "0x3a18e61d9c9f1546dea013478dd653c793098f17", + "(WR)BTC/USDT1": "0x9c4017D1C04cFa0F97FDc9505e33a0D8ac84817F", + "(WR)BTC/USDT2": "0x40580E31cc14DbF7a0859f38Ab36A84262df821D", + "(WR)BTC/DOC1": "0x840437BdE7346EC13B5451417Df50586F4dAF836", + "(WR)BTC/DOC2": "0x2dc80332C19FBCd5169ab4a579d87eE006Cb72c0", + "(WR)BTC/BPRO1": "0x75e327A83aD2BFD53da12EB718fCCFC68Bc57535", + "(WR)BTC/BPRO2": "0x9CE25371426763025C04a9FCd581fbb9E4593475", + "(WR)BTC/SOV": "0x09c5faf7723b13434abdf1a65ab1b667bc02a902", + "(WR)BTC/ETH": "0xF41Ed702df2B84AcE02772C6a0D8AE46465aA5F4", + "(WR)BTC/MOC": "0x7fef930ebaa90B2f8619722AdC55e3f1d965b79b", + "(WR)BTC/XUSD": "0x6f96096687952349Dd5944e0eb1Be327dcDeb705", + "(WR)BTC/BNB": "0x8f3d24ab3510294f1466aa105f78901b90d79d4d", + "(WR)BTC/FISH": "0x35A74a38Fd7728F1c6BC39aE3b18C974b7979ddD", + "(WR)BTC/RIF": "0xAE66117C8105a65D914fB47d37a127E879244319", + "(WR)BTC/MYNT": "0x36263Ac99ecdCf1aB20513d580b7d8D32d3c439D", + "LiquidityMiningConfigToken": "0x513B0f20027BDc6bc2fE9e28A9d4B40d20730Dca", + "FishPoolOracle": "0x2D78E4dc352872Aa6ca12B40d18301Ff6DBE5ECB", + "XUSDPoolOracle": "0xD08eDf687418dF0107bAbCc8Fcab9064F3A6fc05", + "ETHPoolOracle": "0xD6CcacF91c3ca6705aD3047a55dC7760c5ADF1F4", + "MOCPoolOracle": "0x8Cda228A6870D7B3a9Fe1e42604b35F39Fbc5D59", + "BNBPoolOracle": "0x66464BED2420Ab2e7c2d06A9eEb67d06D7E10eAC", + "SOVPoolOracle": "0x4290243b7F3aEF0F6922dAd4F9F8d321ee320fBd", + "RIFPoolOracle": "0x6C339235Bad0381C3bb6CaCecA68C941e81a4bDc", + "MYNTPoolOracle": "0x3f9fe40Cf3013d7B8E5517B166b909911d5AFcC2", + "og": "0x81d25201D044f178883599Be1934FF53FDA98acD", + "multisig": "0x924f5ad34698Fd20c90Fe5D5A8A0abd3b42dc711", + "PriceFeeds": "0x437AC62769f386b2d238409B7f0a7596d36506e4", + "medianizer": "0x7b19bb8e6c5188ec483b784d6fb5d807a77b21bf", + "RSKOracle": "0x99eD262dbd8842442cd22d4c6885936DB38245E6", + "PriceFeedRSKOracle": "0x54c33Cb8a3a32A716BeC40C3CEB5bA8B0fB92a57", + "MOCState": "0xb9C42EFc8ec54490a37cA91c423F7285Fa01e257", + "USDTPriceFeed": "0xed80ccde8baeff2dbfc70d3028a27e501fa0d7d5", + "PriceFeedsMOC": "0x391fe8a92a7FC626A25F30E8c19B92bf8BE37FD3", + "SOVPriceFeedOnProtocol": "0xA266aA67e2a25B0CCa460DEAfcacC81D17341a0D", + "CSOV1": "0x0106F2fFBF6A4f5DEcE323d20E16E2037E732790", + "CSOV2": "0x7f7Dcf9DF951C4A332740e9a125720DA242A34ff", + "governorVault": "0xC7A1637b37190a456b017897207bceb2A29f19b9", + "SOV": "0xEFc78fc7d48b64958315949279Ba181c2114ABBd", + "Staking": "0x5684a06CaB22Db16d901fEe2A5C081b4C91eA40e", + "StakingLogic": "0x5b87F01F665050d244543ca047317C26862E47f7", + "StakingLogic2": "0x962Ce6E2Fa1A4917DfF12Ad10135b1a4F16c2DB0", + "StakingLogic3": "0xcAB5028619839dbb92a193f59026383973F04008", + "StakingLogic4": "0x4BaBb34189a9CDa8213D24DD3984058Fb8A955D2", + "FeeSharingProxyOld": "0x12B1B0C67d9A771EB5Db7726d23fdc6848fd93ef", + "FeeSharingProxy": "0x115cAF168c51eD15ec535727F64684D33B7b08D1", + "FeeSharingLogic": "0x8289AF920cA3d63245740a20116e13aAe0F978e3", + "VestingRegistryProxy": "0xe24ABdB7DcaB57F3cbe4cBDDd850D52F143eE920", + "VestingRegistryLogic": "0x536416A9fbAc10A3EA0D7f7396d0eE8FaE8146D0", + "VestingCreator": "0xa003D9F781a498D90f489328612E74Af1027417f", + "TimelockOwner": "0x967c84b731679E36A344002b8E3CE50620A7F69f", + "GovernorOwner": "0x6496DF39D000478a7A7352C01E0E713835051CcD", + "GovernorVaultOwner": "0x05f4f068DF59a5aA7911f57cE4f41ebFBcB8E247", + "TimelockAdmin": "0x6c94c8aa97C08fC31fb06fbFDa90e1E09529FB13", + "GovernorAdmin": "0xfF25f66b7D7F385503D70574AE0170b6B1622dAd", + "GovernorVaultAdmin": "0x51C754330c6cD04B810014E769Dab0343E31409E", + "VestingLogic": "0x24fbA2281202C3aaE95A3440C08C0050448508A6", + "VestingRegistry": "0x80B036ae59B3e38B573837c01BB1DB95515b7E6B", + "AdoptionFund": "0x0f31cfd6aAb4d378668Ad74DeFa89d3f4DB26633", + "DevelopmentFund": "0x617866cC4a089c3653ddC31a618b078291839AeB", + "VestingRegistry2": "0x0a9bDbf5e104a30fb4c99f6812FB85B60Fd8D372", + "VestingRegistry3": "0x14F3FE332e21Ef3f5d244C45C8D5fbFcEF2FB5c9", + "OriginInvestorsClaim": "0xE0f5BF8d0C58d9c8A078DB75A9D379E6CDF3149E", + "OrigingVestingCreator": "0xea173A078bA12673a8bef6DFa47A8E8f130B4939", + "contributorNFT": "0x8ffB12De9e7602843e4792DB0bC2863e9d137d06", + "SOVTokenSender": "0x31Ec06eB090Af6EEbfF5cF52084e9BCc3D7d9248", + "GenericTokenSender": "0xeA0BC5b4868E776368DfE9be8837Be5b57a75b6e", + "RBTCWrapperProxy": "0xa917BF723433d020a15629eba71f6C2a6B38e52d", + "RBTCWrapperProxyWithoutLM": "0xA3B6E18B9A4ECAE44C7355458Ae7Db8874018C22", + "LockedSOV": "0xB4e4517cA4Edf591Dcafb702999F04f02E57D978", + "LiquidityMiningLogic": "0xbD50232e6fbFa43c95062D1a9d6ecf5439906C21", + "LiquidityMiningProxy": "0xf730af26e87D9F55E46A6C447ED2235C385E55e0", + "ETHAggregatorProxy": "0x4BF113905d7F69202106F613308bb02C84AaDF2F", + "XUSDAggregatorProxy": "0x1440d19436bEeaF8517896bffB957a88EC95a00F", + "BNBsAggregatorProxy": "0xafD905Fe2EdBF5A7367A73B0F1e6a62Cb5E27D3e", + "BridgeRSK": "0x1CcAd820B6d031B41C54f1F3dA11c0d48b399581", + "Aggregator-ETH-RSK": "0x4bF113905d7F69202106f613308bb02c84aaDF2F", + "RSK-ETHes": "0xFe878227c8F334038DAb20a99fC3B373fFe0a755", + "StakingRewardsProxy": "0x8304FB3614c728B712e94F9D4DF6719fede6517F", + "StakingRewards": "0x6CBE8C2dFfb88973E5cC2ff4e732a0D651041215", + "Watcher": "0x051B89f575fCd540F0a6a5B49c75f9a83BB2Cf07", + "FastBTC": "0xC9e14126E5796e999890a4344b8e4c99Ac7002A1", + "FastBTCBiDi": "0x0d5006330289336EBDf9D0ac9e0674F91B4851EA", + "MyntMarketMaker": "0x722935ff8a99d801d802bb3ee528408c11c18656", + "MyntController": "0xB576658700D32CCE28552349bCD52FaD8173ae32" } diff --git a/scripts/contractInteraction/testnet_contracts.json b/scripts/contractInteraction/testnet_contracts.json index 124de487f..2f78b7ba9 100644 --- a/scripts/contractInteraction/testnet_contracts.json +++ b/scripts/contractInteraction/testnet_contracts.json @@ -1,113 +1,113 @@ { - "iDOC": "0x74e00A8CeDdC752074aad367785bFae7034ed89f", - "iRBTC": "0xe67Fe227e0504e8e96A34C3594795756dC26e14B", - "iXUSD": "0x9bD0cE087b14ef67C3D37C891139AaE7d94a961A", - "iUSDT": "0xd1f225BEAE98ccc51c468d1E92d0331c4f93e566", - "iBPro": "0x6226b4B3F29Ecb5f9EEC3eC3391488173418dD5d", - "iDOCProxy": "", - "iRBTCProxy": "", - "iXUSDProxy": "", - "iUSDTProxy": "", - "iBProProxy": "", - "LoanTokenLogicLM": "0xFB0b4DBCDf49802bE24Da19f66AE9a62942aEC19", - "LoanTokenLogicBeaconLM": "0x961F75cb98BAaAA9Ce5efb707d2fe22E8CBdC074", - "LoanTokenLogicWrbtc": "0x3E686322ac52DF2380a96dC71264d296A8eBe289", - "LoanTokenLogicBeaconWrbtc": "0x4A3d7163F9Bc3D3Be0a467452A880D3c25a83e70", - "sovrynProtocol": "0x25380305f223B32FDB844152abD2E82BC5Ad99c3", - "DoC": "0xCB46c0ddc60D18eFEB0E586C17Af6ea36452Dae0", - "WRBTC": "0x69FE5cEC81D5eF92600c1A0dB1F11986AB3758Ab", - "MOC": "", - "ETHs": "0x0Fd0d8D78Ce9299Ee0e5676a8d51F938C234162c", - "XUSD": "0x74858FE37d391f81F89472e1D8BC8Ef9CF67B3b1", - "BNBs": "", - "FISH": "0xaa7038D80521351F243168FefE0352194e3f83C3", - "USDT": "0x4d5a316d23ebe168d8f887b4447bf8dbfa4901cc", - "BPro": "0x4dA7997A819bb46B6758b9102234c289Dd2ad3bf", - "BRZ": "0xe355c280131dfaf18bf1c3648aee3c396db6b5fd", - "MYNT": "0x139483e22575826183F5b56dd242f8f2C1AEf327", - "swapNetwork": "0x61172B53423E205a399640e5283e51FE60EC2256", - "ConverterDOC": "0x497b0517dd24F66C456e93bC0aDBB2A2bf159EC4", - "ConverterBPRO": "", - "ConverterUSDT": "0x133eBE9c8bA524C9B1B601E794dF527f390729bF", - "ConverterSOV": "0x1Cecc8B488abcF9A0932E54328dD51980cbe86Ea", - "ConverterXUSD": "0x169B7A8Fc9615797e118B464b4fF1f594Dcad7a4", - "ConverterETHs": "0xf46DC974edD1754D4815AaE44Ab4542fF39B898D", - "ConverterMOC": "0x478133b66B54e55bfA46b1182e274b5cCE47C60E", - "ConverterBNBs": "0x157F2d3702AF9AFF74fF1Ec9329850E596626f33", - "ConverterFISH": "0x179caA42B5024ec1C3D8513A262fC9986F565295", - "ConverterXUSD-BRZ": "0x6Ca500A8F39C452CE7533AA320c9b7752F04AA64", - "ConverterMYNT": "0x84953dAF0E7a9fFb8B4fDf7F948185e1cF85852e", - "(WR)BTC/USDT1": "0xffbbf93ecd27c8b500bd35d554802f7f349a1e9b", - "(WR)BTC/USDT2": "0x7274305bb36d66f70cb8824621ec26d52abe9069", - "(WR)BTC/DOC1": "0x7f433cc76298bb5099c15c1c7c8f2e89a8370111", - "(WR)BTC/DOC2": "0x6787161bc4f8d54e6ac6fcb9643af6f4a12dff28", - "(WR)BTC/BPRO1": "0x98e5f39d8c675972a66ea165040cb81803c440a3", - "(WR)BTC/BPRO2": "0xdaf6fd8370f5245d98e829c766e008cd39e8f060", - "(WR)BTC/SOV": "0xdf298421cb18740a7059b0af532167faa45e7a98", - "(WR)BTC/ETH": "0xBb5B900EDa0F1459F582aB2436EA825a927f5bA2", - "(WR)BTC/XUSD": "0x6601Ccd32342d644282e82Cb05A3Dd88964D18c1", - "(WR)BTC/FISH": "0xe41E262889f89b9a6331680606D9e9AabD01743e", - "XUSD/BRZ": "0x7107E42f4b59310D217333B544465d428395Affe", - "(WR)BTC/MYNT": "0xB12FA09a50c56e9a0C826b98e76DA7645017AB4D", - "LiquidityMiningConfigToken": "0x0F1694aFEF2B25c1C069582F23Bca73608348F50", - "SOVPoolOracle": "0x8A2a7F192DC39b70c2937C38559e704fcAB3F4CA", - "XUSDPoolOracle": "0xA30E5776c6Ae21E0CA28C6b4c39Fe7A9744d9a86", - "ETHPoolOracle": "0x9fDaA4E1AcFc243d29C5e2AE72fbC322a10C5530", - "MOCPoolOracle": "0xA60d29C03452b858C4580725D5e9047982A9517a", - "BNBPoolOracle": "0x73616dbc3A6fA63354d4dA0C3B74834D079BE46d", - "FishPoolOracle": "0x498E4D1d39968b0BB5DECD52D71055529150ba74", - "MYNTPoolOracle": "0xdE4a7AE6cd286a8eeC24e34D8705cbf3827524Ff", - "og": "0xC5452Dbb2E3956C1161cB9C2d6DB53C2b60E7805", - "multisig": "0x189ecD23E9e34CFC07bFC3b7f5711A23F43F8a57", - "PriceFeeds": "0x7f38c422b99075f63C9c919ECD200DF8d2Cf5BD4", - "medianizer": "0xbffBD993FF1d229B0FfE55668F2009d20d4F7C5f", - "PriceFeedsMOC": "0x873B33BAFcA43a813a109942958F207847dF4d09", - "USDTtoUSDTOracleAMM": "0x7734610e8822A68FdcFeB9061dBa8d28bD71d7aD", - "BTCtoUSDTOracleAMM": "0x066ba9453e230a260c2a753d9935d91187178C29", - "RSKOracle": "0xE00243Bc6912BF148302e8478996c98c22fE8739", - "PriceFeedRSKOracle": "0xF2B7440C89431DF82EC6c8F3D079059847565dF0", - "SOVPriceFeedOnProtocol": "0x0945E4d65Ad9AD7FB3d695c036CAdA63769079C7", - "CSOV1": "0x75bbf7f4d77777730eE35b94881B898113a93124", - "CSOV2": "0x1dA260149ffee6fD4443590ee58F65b8dC2106B9", - "governorVault": "0xE8276A1680CB970c2334B3201044Ddf7c492F52A", - "SOV": "0x6a9A07972D07e58F0daf5122d11E069288A375fb", - "Staking": "0xc37A85e35d7eECC82c4544dcba84CF7E61e1F1a3", - "StakingLogicOld": "0x0D2C30c4e48f186211e483950B5A57cb457f6847", - "StakingLogic": "0x44803822eA8fD2fF14F9E0B0aAAab18018F324C3", - "OldFeeSharingProxy": "0x740E6f892C0132D659Abcd2B6146D237A4B6b653", - "FeeSharingProxy": "0xedD92fb7C556E4A4faf8c4f5A90f471aDCD018f4", - "GovernorOwner": "0x058FD3F6a40b92b311B49E5e3E064300600021D7", - "GovernorAdmin": "0x1528f0341a1Ea546780caD690F54b4FBE1834ED4", - "VestingRegistry": "0x80ec7ADd6CC1003BBEa89527ce93722e1DaD5c2a", - "VestingRegistry2": "0x068fbb3Bef062C3daBA7a4B12f53Cd614FBcBF1d", - "VestingRegistry3": "0x52E4419b9D33C6e0ceb2e7c01D3aA1a04b21668C", - "VestingLogic": "0xc1cECAC06c7a5d5480F158043A150acf06e206cD", - "FourYearVestingLogic": "0x051D4F2ad54114Ec3b003f308B09640e39D362B1", - "FourYearVestingFactory": "0x914563Aca50218a070431f866803628F19E8CCdD", - "OriginInvestorsClaim": "0x9FBe4Bf89521088F790a4dD2F3e495B4f0dA7F42", - "TokenSender": "0x4D1903BaAd894Fc6Ff70483d8518Db78F163F9ff", - "RBTCWrapperProxy": "0x6b1a4735b1E25ccE9406B2d5D7417cE53d1cf90e", - "RBTCWrapperProxyWithoutLM": "0x106f117Af68586A994234E208c29DE0f1A764C60", - "LockedSOV": "0x6b94Da2d05039173d017359553D685Acfbaa782F", - "LiquidityMiningProxy": "0xe28aEbA913c34EC8F10DF0D9C92D2Aa27545870e", - "LiquidityMiningLogic": "0x4daf6b091dBF2921786120b8705260edc7cE5570", - "Aggregator-ETH-RSK": "0x04D92DaA8f3Ef7bD222195e8D1DbE8D89A8CebD3", - "BridgeRSK": "0xC0E7A7FfF4aBa5e7286D5d67dD016B719DCc9156", - "BridgeETH": "0x2b456e230225C4670FBF10b9dA506C019a24cAC7", - "BridgeRSKMultisig": "0x34055C3f23bFE1d8A45c9ABA53b66ffcA4353600", - "BridgeETHMultisig": "0x75Ea52aC8219a8F16a2DC6778874943ef2c24C45", - "RSK-DAIes": "0xcb92C8D49Ec01b92F2A766C7c3C9C501C45271E0", - "RSK-USDCes": "0xcc8Eec21ae75F1A2dE4aC7b32A7de888a45cF859", - "RSK-USDTes": "0x10C5A7930fC417e728574E334b1488b7895c4B81", - "RSK-ETHes": "0x4F2Fc8d55c1888A5AcA2503e2F3E5d74eef37C33", - "ETH-DAI": "0x974cf21396D4D29F8e63Ac07eCfcbaB51a739bc9", - "ETH-USDC": "0x4C68058992b8aD1243eE23A5923023C0e15Cf43F", - "ETH-USDT": "0xff364ffa4962cb172203a5be01d17cf3fef02419", - "ETH-eSOV": "0xce887e72f26b61c3ddf45bd6e65abbd58437ab04", - "WatcherContract": "0x3583155D5e87491dACDc15f7D0032C12D5D0ece0", - "StakingRewardsProxy": "0x18eF0ff12f1b4D30104B4680D485D026C26D164D", - "StakingRewards": "0x9762e0aA49248A58e14a5C09B4edE5c185b0d178", - "VestingRegistryProxy": "0x09e8659B6d204C6b1bED2BFF8E3F43F834A5Bbc4", - "VestingRegistryLogic": "0xc9F6D4723Ed9AFc52FE07c083690e39e4505ED73", - "SovrynSwapFormula": "0x7FF1C363b5600834bce7c514B01109eF1c103507" + "iDOC": "0x74e00A8CeDdC752074aad367785bFae7034ed89f", + "iRBTC": "0xe67Fe227e0504e8e96A34C3594795756dC26e14B", + "iXUSD": "0x9bD0cE087b14ef67C3D37C891139AaE7d94a961A", + "iUSDT": "0xd1f225BEAE98ccc51c468d1E92d0331c4f93e566", + "iBPro": "0x6226b4B3F29Ecb5f9EEC3eC3391488173418dD5d", + "iDOCProxy": "", + "iRBTCProxy": "", + "iXUSDProxy": "", + "iUSDTProxy": "", + "iBProProxy": "", + "LoanTokenLogicLM": "0xFB0b4DBCDf49802bE24Da19f66AE9a62942aEC19", + "LoanTokenLogicBeaconLM": "0x961F75cb98BAaAA9Ce5efb707d2fe22E8CBdC074", + "LoanTokenLogicWrbtc": "0x3E686322ac52DF2380a96dC71264d296A8eBe289", + "LoanTokenLogicBeaconWrbtc": "0x4A3d7163F9Bc3D3Be0a467452A880D3c25a83e70", + "sovrynProtocol": "0x25380305f223B32FDB844152abD2E82BC5Ad99c3", + "DoC": "0xCB46c0ddc60D18eFEB0E586C17Af6ea36452Dae0", + "WRBTC": "0x69FE5cEC81D5eF92600c1A0dB1F11986AB3758Ab", + "MOC": "", + "ETHs": "0x0Fd0d8D78Ce9299Ee0e5676a8d51F938C234162c", + "XUSD": "0x74858FE37d391f81F89472e1D8BC8Ef9CF67B3b1", + "BNBs": "", + "FISH": "0xaa7038D80521351F243168FefE0352194e3f83C3", + "USDT": "0x4d5a316d23ebe168d8f887b4447bf8dbfa4901cc", + "BPro": "0x4dA7997A819bb46B6758b9102234c289Dd2ad3bf", + "BRZ": "0xe355c280131dfaf18bf1c3648aee3c396db6b5fd", + "MYNT": "0x139483e22575826183F5b56dd242f8f2C1AEf327", + "swapNetwork": "0x61172B53423E205a399640e5283e51FE60EC2256", + "ConverterDOC": "0x497b0517dd24F66C456e93bC0aDBB2A2bf159EC4", + "ConverterBPRO": "", + "ConverterUSDT": "0x133eBE9c8bA524C9B1B601E794dF527f390729bF", + "ConverterSOV": "0x1Cecc8B488abcF9A0932E54328dD51980cbe86Ea", + "ConverterXUSD": "0x169B7A8Fc9615797e118B464b4fF1f594Dcad7a4", + "ConverterETHs": "0xf46DC974edD1754D4815AaE44Ab4542fF39B898D", + "ConverterMOC": "0x478133b66B54e55bfA46b1182e274b5cCE47C60E", + "ConverterBNBs": "0x157F2d3702AF9AFF74fF1Ec9329850E596626f33", + "ConverterFISH": "0x179caA42B5024ec1C3D8513A262fC9986F565295", + "ConverterXUSD-BRZ": "0x6Ca500A8F39C452CE7533AA320c9b7752F04AA64", + "ConverterMYNT": "0x84953dAF0E7a9fFb8B4fDf7F948185e1cF85852e", + "(WR)BTC/USDT1": "0xffbbf93ecd27c8b500bd35d554802f7f349a1e9b", + "(WR)BTC/USDT2": "0x7274305bb36d66f70cb8824621ec26d52abe9069", + "(WR)BTC/DOC1": "0x7f433cc76298bb5099c15c1c7c8f2e89a8370111", + "(WR)BTC/DOC2": "0x6787161bc4f8d54e6ac6fcb9643af6f4a12dff28", + "(WR)BTC/BPRO1": "0x98e5f39d8c675972a66ea165040cb81803c440a3", + "(WR)BTC/BPRO2": "0xdaf6fd8370f5245d98e829c766e008cd39e8f060", + "(WR)BTC/SOV": "0xdf298421cb18740a7059b0af532167faa45e7a98", + "(WR)BTC/ETH": "0xBb5B900EDa0F1459F582aB2436EA825a927f5bA2", + "(WR)BTC/XUSD": "0x6601Ccd32342d644282e82Cb05A3Dd88964D18c1", + "(WR)BTC/FISH": "0xe41E262889f89b9a6331680606D9e9AabD01743e", + "XUSD/BRZ": "0x7107E42f4b59310D217333B544465d428395Affe", + "(WR)BTC/MYNT": "0xB12FA09a50c56e9a0C826b98e76DA7645017AB4D", + "LiquidityMiningConfigToken": "0x0F1694aFEF2B25c1C069582F23Bca73608348F50", + "SOVPoolOracle": "0x8A2a7F192DC39b70c2937C38559e704fcAB3F4CA", + "XUSDPoolOracle": "0xA30E5776c6Ae21E0CA28C6b4c39Fe7A9744d9a86", + "ETHPoolOracle": "0x9fDaA4E1AcFc243d29C5e2AE72fbC322a10C5530", + "MOCPoolOracle": "0xA60d29C03452b858C4580725D5e9047982A9517a", + "BNBPoolOracle": "0x73616dbc3A6fA63354d4dA0C3B74834D079BE46d", + "FishPoolOracle": "0x498E4D1d39968b0BB5DECD52D71055529150ba74", + "MYNTPoolOracle": "0xdE4a7AE6cd286a8eeC24e34D8705cbf3827524Ff", + "og": "0xC5452Dbb2E3956C1161cB9C2d6DB53C2b60E7805", + "multisig": "0x189ecD23E9e34CFC07bFC3b7f5711A23F43F8a57", + "PriceFeeds": "0x7f38c422b99075f63C9c919ECD200DF8d2Cf5BD4", + "medianizer": "0xbffBD993FF1d229B0FfE55668F2009d20d4F7C5f", + "PriceFeedsMOC": "0x873B33BAFcA43a813a109942958F207847dF4d09", + "USDTtoUSDTOracleAMM": "0x7734610e8822A68FdcFeB9061dBa8d28bD71d7aD", + "BTCtoUSDTOracleAMM": "0x066ba9453e230a260c2a753d9935d91187178C29", + "RSKOracle": "0xE00243Bc6912BF148302e8478996c98c22fE8739", + "PriceFeedRSKOracle": "0xF2B7440C89431DF82EC6c8F3D079059847565dF0", + "SOVPriceFeedOnProtocol": "0x0945E4d65Ad9AD7FB3d695c036CAdA63769079C7", + "CSOV1": "0x75bbf7f4d77777730eE35b94881B898113a93124", + "CSOV2": "0x1dA260149ffee6fD4443590ee58F65b8dC2106B9", + "governorVault": "0xE8276A1680CB970c2334B3201044Ddf7c492F52A", + "SOV": "0x6a9A07972D07e58F0daf5122d11E069288A375fb", + "Staking": "0xc37A85e35d7eECC82c4544dcba84CF7E61e1F1a3", + "StakingLogicOld": "0x0D2C30c4e48f186211e483950B5A57cb457f6847", + "StakingLogic": "0x44803822eA8fD2fF14F9E0B0aAAab18018F324C3", + "OldFeeSharingProxy": "0x740E6f892C0132D659Abcd2B6146D237A4B6b653", + "FeeSharingProxy": "0xedD92fb7C556E4A4faf8c4f5A90f471aDCD018f4", + "GovernorOwner": "0x058FD3F6a40b92b311B49E5e3E064300600021D7", + "GovernorAdmin": "0x1528f0341a1Ea546780caD690F54b4FBE1834ED4", + "VestingRegistry": "0x80ec7ADd6CC1003BBEa89527ce93722e1DaD5c2a", + "VestingRegistry2": "0x068fbb3Bef062C3daBA7a4B12f53Cd614FBcBF1d", + "VestingRegistry3": "0x52E4419b9D33C6e0ceb2e7c01D3aA1a04b21668C", + "VestingLogic": "0xc1cECAC06c7a5d5480F158043A150acf06e206cD", + "FourYearVestingLogic": "0x051D4F2ad54114Ec3b003f308B09640e39D362B1", + "FourYearVestingFactory": "0x914563Aca50218a070431f866803628F19E8CCdD", + "OriginInvestorsClaim": "0x9FBe4Bf89521088F790a4dD2F3e495B4f0dA7F42", + "TokenSender": "0x4D1903BaAd894Fc6Ff70483d8518Db78F163F9ff", + "RBTCWrapperProxy": "0x6b1a4735b1E25ccE9406B2d5D7417cE53d1cf90e", + "RBTCWrapperProxyWithoutLM": "0x106f117Af68586A994234E208c29DE0f1A764C60", + "LockedSOV": "0x6b94Da2d05039173d017359553D685Acfbaa782F", + "LiquidityMiningProxy": "0xe28aEbA913c34EC8F10DF0D9C92D2Aa27545870e", + "LiquidityMiningLogic": "0x4daf6b091dBF2921786120b8705260edc7cE5570", + "Aggregator-ETH-RSK": "0x04D92DaA8f3Ef7bD222195e8D1DbE8D89A8CebD3", + "BridgeRSK": "0xC0E7A7FfF4aBa5e7286D5d67dD016B719DCc9156", + "BridgeETH": "0x2b456e230225C4670FBF10b9dA506C019a24cAC7", + "BridgeRSKMultisig": "0x34055C3f23bFE1d8A45c9ABA53b66ffcA4353600", + "BridgeETHMultisig": "0x75Ea52aC8219a8F16a2DC6778874943ef2c24C45", + "RSK-DAIes": "0xcb92C8D49Ec01b92F2A766C7c3C9C501C45271E0", + "RSK-USDCes": "0xcc8Eec21ae75F1A2dE4aC7b32A7de888a45cF859", + "RSK-USDTes": "0x10C5A7930fC417e728574E334b1488b7895c4B81", + "RSK-ETHes": "0x4F2Fc8d55c1888A5AcA2503e2F3E5d74eef37C33", + "ETH-DAI": "0x974cf21396D4D29F8e63Ac07eCfcbaB51a739bc9", + "ETH-USDC": "0x4C68058992b8aD1243eE23A5923023C0e15Cf43F", + "ETH-USDT": "0xff364ffa4962cb172203a5be01d17cf3fef02419", + "ETH-eSOV": "0xce887e72f26b61c3ddf45bd6e65abbd58437ab04", + "WatcherContract": "0x3583155D5e87491dACDc15f7D0032C12D5D0ece0", + "StakingRewardsProxy": "0x18eF0ff12f1b4D30104B4680D485D026C26D164D", + "StakingRewards": "0x9762e0aA49248A58e14a5C09B4edE5c185b0d178", + "VestingRegistryProxy": "0x09e8659B6d204C6b1bED2BFF8E3F43F834A5Bbc4", + "VestingRegistryLogic": "0xc9F6D4723Ed9AFc52FE07c083690e39e4505ED73", + "SovrynSwapFormula": "0x7FF1C363b5600834bce7c514B01109eF1c103507" } diff --git a/scripts/getSelectors.js b/scripts/getSelectors.js index 9d7537e44..f54997658 100644 --- a/scripts/getSelectors.js +++ b/scripts/getSelectors.js @@ -23,144 +23,144 @@ const keccak256 = require("keccak256"); // Get keccak256 in 0x string format for a given content var keccak256_0x = function (content) { - return "0x" + keccak256(content).toString("hex"); + return "0x" + keccak256(content).toString("hex"); }; const fs = require("fs"); // Parse a contract searching for function and event declarations var parseContract = function (fileContent) { - fileContent = fileContent - .replace(/\/\/[^\n]*\n/g, "\n") // remove comments like // - .replace(/\/\*[\s\S]*?\*\//g, ""); // remove comments like /* */ - - var signatureList = {}; - if (searchType == "functions") { - searchRegExp = new RegExp(/function [^\)]*\)/g); // focus on function declarations - } else if (searchType == "events") { - searchRegExp = new RegExp(/event [^\)]*\)/g); // focus on event declarations - } else { - console.log("Error: Unknown searchType", searchType); - process.exit(1); - } - - while (null != (f = searchRegExp.exec(fileContent))) { - // For every function or event found - signature = f[0] - .replace(/(function|event) /g, "") // remove "function " or "event " on every match - .replace(/([\(,])\s+/g, "$1") // remove whitespaces and newlines inmediatly after ( or , - .replace(/\s+\)/g, ")") // remove whitespaces and newlines inmediatly before ) - .replace(/\s.*?([,\)])/g, "$1") // remove var names and extra modifiers - .replace(/^(u?int[0-9]*|address|bool|string|bytes(32|4)*)/g, "address"); // every unknown type found is considered to be an address - if (!!signature) { - signatureList[signature] = keccak256_0x(signature); - } - } - - return signatureList; + fileContent = fileContent + .replace(/\/\/[^\n]*\n/g, "\n") // remove comments like // + .replace(/\/\*[\s\S]*?\*\//g, ""); // remove comments like /* */ + + var signatureList = {}; + if (searchType == "functions") { + searchRegExp = new RegExp(/function [^\)]*\)/g); // focus on function declarations + } else if (searchType == "events") { + searchRegExp = new RegExp(/event [^\)]*\)/g); // focus on event declarations + } else { + console.log("Error: Unknown searchType", searchType); + process.exit(1); + } + + while (null != (f = searchRegExp.exec(fileContent))) { + // For every function or event found + signature = f[0] + .replace(/(function|event) /g, "") // remove "function " or "event " on every match + .replace(/([\(,])\s+/g, "$1") // remove whitespaces and newlines inmediatly after ( or , + .replace(/\s+\)/g, ")") // remove whitespaces and newlines inmediatly before ) + .replace(/\s.*?([,\)])/g, "$1") // remove var names and extra modifiers + .replace(/^(u?int[0-9]*|address|bool|string|bytes(32|4)*)/g, "address"); // every unknown type found is considered to be an address + if (!!signature) { + signatureList[signature] = keccak256_0x(signature); + } + } + + return signatureList; }; // Parse a contract searching for interface declarations var parseInterfacesFromContract = function (fileContent) { - fileContent = fileContent - .replace(/\/\/[^\n]*\n/g, "\n") // remove comments like // - .replace(/\/\*[\s\S]*?\*\//g, ""); // remove comments like /* */ - - var interfaceList = []; - searchRegExp = new RegExp(/interface [^\}]*\}/g); // focus on interface declarations - - // Get all interfaces from repo - while (null != (f = searchRegExp.exec(fileContent))) { - // For every interface found - interfaceName = f[0] - .replace(/[\n\r\t\s]+/g, " ") // remove newlines and tabs - .replace(/^interface ([^\s]+).*$/g, "$1"); // leave only interface name - if (!!interfaceName) { - interfaceList.push(interfaceName); - } - } - - return interfaceList; + fileContent = fileContent + .replace(/\/\/[^\n]*\n/g, "\n") // remove comments like // + .replace(/\/\*[\s\S]*?\*\//g, ""); // remove comments like /* */ + + var interfaceList = []; + searchRegExp = new RegExp(/interface [^\}]*\}/g); // focus on interface declarations + + // Get all interfaces from repo + while (null != (f = searchRegExp.exec(fileContent))) { + // For every interface found + interfaceName = f[0] + .replace(/[\n\r\t\s]+/g, " ") // remove newlines and tabs + .replace(/^interface ([^\s]+).*$/g, "$1"); // leave only interface name + if (!!interfaceName) { + interfaceList.push(interfaceName); + } + } + + return interfaceList; }; // Parse a contract searching for struct declarations var parseStructsFromContract = function (fileContent) { - fileContent = fileContent - .replace(/\/\/[^\n]*\n/g, "\n") // remove comments like // - .replace(/\/\*[\s\S]*?\*\//g, ""); // remove comments like /* */ - - var structList = {}; - searchRegExp = new RegExp(/struct [^\}]*\}/g); // focus on struct declarations - - // Get all structs from repo - while (null != (f = searchRegExp.exec(fileContent))) { - // For every struct found - structName = f[0] - .replace(/[\n\r\t\s]+/g, " ") // remove newlines and tabs - .replace(/^struct ([^\s]+).*$/g, "$1"); // leave only struct name - structDeclaration = f[0] - .replace(/[\n\r\t\s]+/g, " ") // remove newlines and tabs - .replace(/^struct [^\s]+ \{(.*)\}/g, "$1"); // leave only struct declaration - if (!!structName) { - structList[structName] = structDeclaration; - } - } - - return structList; + fileContent = fileContent + .replace(/\/\/[^\n]*\n/g, "\n") // remove comments like // + .replace(/\/\*[\s\S]*?\*\//g, ""); // remove comments like /* */ + + var structList = {}; + searchRegExp = new RegExp(/struct [^\}]*\}/g); // focus on struct declarations + + // Get all structs from repo + while (null != (f = searchRegExp.exec(fileContent))) { + // For every struct found + structName = f[0] + .replace(/[\n\r\t\s]+/g, " ") // remove newlines and tabs + .replace(/^struct ([^\s]+).*$/g, "$1"); // leave only struct name + structDeclaration = f[0] + .replace(/[\n\r\t\s]+/g, " ") // remove newlines and tabs + .replace(/^struct [^\s]+ \{(.*)\}/g, "$1"); // leave only struct declaration + if (!!structName) { + structList[structName] = structDeclaration; + } + } + + return structList; }; // Loop through fileList and extract all interfaces from repo var getAllInterfaces = function (fileList) { - var interfaces = []; - for (let file of fileList) { - // console.log("\nFile: ", file); - let content = fs.readFileSync(file, { encoding: "utf8" }); - var interfacesToAdd = parseInterfacesFromContract(content); - interfaces.push(...interfacesToAdd); - } - - return interfaces; + var interfaces = []; + for (let file of fileList) { + // console.log("\nFile: ", file); + let content = fs.readFileSync(file, { encoding: "utf8" }); + var interfacesToAdd = parseInterfacesFromContract(content); + interfaces.push(...interfacesToAdd); + } + + return interfaces; }; // Loop through fileList and extract all structs from repo var getAllStructs = function (fileList) { - var structs = {}; - for (let file of fileList) { - // console.log("\nFile: ", file); - let content = fs.readFileSync(file, { encoding: "utf8" }); - var structsToAdd = parseStructsFromContract(content); - for (var key in structsToAdd) { - structs[key] = structsToAdd[key]; - } - } - - return structs; + var structs = {}; + for (let file of fileList) { + // console.log("\nFile: ", file); + let content = fs.readFileSync(file, { encoding: "utf8" }); + var structsToAdd = parseStructsFromContract(content); + for (var key in structsToAdd) { + structs[key] = structsToAdd[key]; + } + } + + return structs; }; // Loop through fileList and call parser on each one var parseContractList = function (fileList) { - var contractSignatures = {}; - for (let file of fileList) { - // console.log("\nFile: ", file); - let content = fs.readFileSync(file, { encoding: "utf8" }); - contractSignatures[file] = parseContract(content); - } - - return contractSignatures; + var contractSignatures = {}; + for (let file of fileList) { + // console.log("\nFile: ", file); + let content = fs.readFileSync(file, { encoding: "utf8" }); + contractSignatures[file] = parseContract(content); + } + + return contractSignatures; }; // Open files recursively const glob = require("glob"); var getDirectories = function (src, ext, callback) { - glob(src + "/**/*" + ext, callback); + glob(src + "/**/*" + ext, callback); }; getDirectories(path, ".sol", function (err, res) { - if (err) { - console.log("Error", err); - } else { - /* + if (err) { + console.log("Error", err); + } else { + /* interfaces = getAllInterfaces(res); console.log("Interfaces found: ", interfaces); @@ -170,14 +170,14 @@ process.exit(1); console.log("Structs found: ", structs); process.exit(1); */ - contractSignatures = parseContractList(res); - // console.log("contractSignatures: ", contractSignatures); - // Loop through results and apply tabulated format to copy/past on Google Docs - for (const [contract, functions] of Object.entries(contractSignatures)) { - console.log(contract); - for (const [signature, selector] of Object.entries(functions)) { - console.log("\t" + signature + "\t" + selector.slice(0, 10) + "\t" + selector); - } - } - } + contractSignatures = parseContractList(res); + // console.log("contractSignatures: ", contractSignatures); + // Loop through results and apply tabulated format to copy/past on Google Docs + for (const [contract, functions] of Object.entries(contractSignatures)) { + console.log(contract); + for (const [signature, selector] of Object.entries(functions)) { + console.log("\t" + signature + "\t" + selector.slice(0, 10) + "\t" + selector); + } + } + } }); diff --git a/scripts/governance/values.json b/scripts/governance/values.json index 504bbcbdc..a37b5853e 100644 --- a/scripts/governance/values.json +++ b/scripts/governance/values.json @@ -1,10 +1,10 @@ { - "account": "0x0000000000000000000000000000000000000000", - "delegatee": "0x0000000000000000000000000000000000000000", - "SOV_Amount_To_Stake": "1000000", - "Time_To_Stake": "31536000", - "Proposal_Target": "0x0000000000000000000000000000000000000000", - "Proposal_Signature": "name()", - "Proposal_Data": "0x", - "Proposal_Description": "SIP-000 : SIP_Name, Details: https://github.com/SIP-000.md, sha256: XXX" + "account": "0x0000000000000000000000000000000000000000", + "delegatee": "0x0000000000000000000000000000000000000000", + "SOV_Amount_To_Stake": "1000000", + "Time_To_Stake": "31536000", + "Proposal_Target": "0x0000000000000000000000000000000000000000", + "Proposal_Signature": "name()", + "Proposal_Data": "0x", + "Proposal_Description": "SIP-000 : SIP_Name, Details: https://github.com/SIP-000.md, sha256: XXX" } diff --git a/scripts/keccak256.js b/scripts/keccak256.js index 7316b84de..d838cc295 100644 --- a/scripts/keccak256.js +++ b/scripts/keccak256.js @@ -6,22 +6,22 @@ var myArgs = process.argv.slice(2); // console.log('arg: ', String(myArgs[0]).split(/\r?\n/)); let signatures = String(myArgs[0]) - .replace(/;/g, "") // remove final ; - .split(/\r?\n/) // split lines, every line is a signature - .sort(function (a, b) { - // sorted alphabetically - if (a < b) { - return -1; - } - if (a > b) { - return 1; - } - return 0; - }) - .filter((item, i, ar) => ar.indexOf(item) === i); // get unique + .replace(/;/g, "") // remove final ; + .split(/\r?\n/) // split lines, every line is a signature + .sort(function (a, b) { + // sorted alphabetically + if (a < b) { + return -1; + } + if (a > b) { + return 1; + } + return 0; + }) + .filter((item, i, ar) => ar.indexOf(item) === i); // get unique // console.log('signatures: ', signatures); const keccak256 = require("keccak256"); for (let s of signatures) { - if (s) console.log(s + "\t" + "0x" + keccak256(s).toString("hex")); + if (s) console.log(s + "\t" + "0x" + keccak256(s).toString("hex")); } diff --git a/scripts/staking/vestings_examples.json b/scripts/staking/vestings_examples.json index b2aec7d35..b1a730cb7 100644 --- a/scripts/staking/vestings_examples.json +++ b/scripts/staking/vestings_examples.json @@ -1,99 +1,121 @@ [ - { - "user": "0xF5366a3445Adf491311a6e86443bfb5392f08265", - "vesting": "0xD30381e7fEcE6D5b278c187370066f842Cd927C9", - "dates": [ - 1620383295, 1622802495, 1625221695, 1627640895, 1630060095, 1632479295, 1633688895, 1634898495, 1636108095, 1637317695, - 1638527295, 1639736895, 1640946495, 1642156095, 1643365695, 1644575295, 1645784895, 1646994495, 1648204095, 1649413695, - 1650623295, 1651832895, 1653042495, 1654252095, 1655461695, 1656671295, 1657880895, 1659090495, 1660300095, 1661509695, - 1662719295, 1663928895, 1665138495, 1666348095, 1667557695, 1668767295, 1669976895, 1671186495, 1672396095, 1673605695, - 1674815295, 1676024895, 1677234495, 1678444095, 1679653695, 1680863295, 1682072895, 1683282495, 1684492095, 1685701695, - 1686911295, 1689330495, 1691749695, 1694168895 - ], - "amounts": [ - 5873461538461538500, 11162859640346153860, 12565936563423076944, 12565936563423076920, 12565936563423076920, - 12565936563423076920, 1614230769230769250, 12565936563423076920, 1614230769230769230, 12565936563423076920, 1614230769230769230, - 12565936563423076920, 1614230769230769230, 12565936563423076920, 1614230769230769230, 12565936563423076920, 1614230769230769230, - 12565936563423076920, 1614230769230769230, 12565936563423076920, 1614230769230769230, 12565936563423076920, 1614230769230769230, - 12565936563423076920, 1614230769230769230, 12565936563423076920, 1614230769230769230, 12565936563423076920, 1614230769230769230, - 12565936563423076920, 1614230769230769230, 12565936563423076920, 1614230769230769230, 12565936563423076920, 1614230769230769230, - 12565936563423076920, 1614230769230769230, 12565936563423076920, 1614230769230769230, 12565936563423076920, 1614230769230769230, - 12565936563423076920, 1614230769230769230, 12565936563423076920, 1614230769230769230, 12565936563423076920, 1614230769230769230, - 6692475024961538460, 1614230769230769230, 1403076923076923076, 1614230769230769230, 1614230769230769230, 1614230769230769230, - 1614230769230769230 - ] - }, - { - "user": "0x3d91ad906C3969f86C588C988F721d5a735c6f81", - "vesting": "0x055f9921fc7F9474F75805dD4A82a99CDc40599b", - "dates": [ - 1633688895, 1636108095, 1638527295, 1640946495, 1643365695, 1645784895, 1648204095, 1650623295, 1653042495, 1655461695, - 1657880895, 1660300095, 1662719295, 1665138495, 1667557695, 1669976895, 1672396095, 1674815295, 1677234495, 1679653695, - 1682072895, 1684492095, 1686911295, 1689330495, 1691749695, 1694168895 - ], - "amounts": [ - 2018076923076923100, 2018076923076923076, 2018076923076923076, 2018076923076923076, 2018076923076923076, 2018076923076923076, - 2018076923076923076, 2018076923076923076, 2018076923076923076, 2018076923076923076, 2018076923076923076, 2018076923076923076, - 2018076923076923076, 2018076923076923076, 2018076923076923076, 2018076923076923076, 2018076923076923076, 2018076923076923076, - 2018076923076923076, 2018076923076923076, 2018076923076923076, 2018076923076923076, 2018076923076923076, 2018076923076923076, - 2018076923076923076, 2018076923076923076 - ] - }, - { - "user": "0x3d91ad906C3969f86C588C988F721d5a735c6f81", - "vesting": "0xb4c9DBaf4885063fd06eccFF821f76aF49f9B7C8", - "dates": [ - 1632479295, 1633688895, 1634898495, 1636108095, 1637317695, 1638527295, 1639736895, 1640946495, 1642156095, 1643365695, - 1644575295, 1645784895, 1646994495, 1648204095, 1649413695, 1650623295, 1651832895, 1653042495, 1654252095, 1655461695 - ], - "amounts": [ - 45668591586275745958, 36599600383168147936, 45668591586275745946, 36599600383168147921, 45668591586275745946, - 36599600383168147921, 45668591586275745946, 36599600383168147921, 45668591586275745946, 36599600383168147921, - 45668591586275745946, 36599600383168147921, 31980091586275745946, 35092066582373476215, 20909973849664128348, - 24080946941970523698, 13817324936461310003, 15334185839868350947, 7788805529050305208, 7442588257786551439 - ] - }, - { - "user": "0x9fb96c6556562E660811c99da2351d99DbFDB188", - "vesting": "0x66a5d36FC35424a22d64Ce4240d84E52d678554F", - "dates": [1631269695, 1633688895, 1636108095, 1638527295], - "amounts": [400000000000000000000, 400000000000000000000, 400000000000000000000, 400000000000000000000] - }, - { - "user": "0x9fb96c6556562E660811c99da2351d99DbFDB188", - "vesting": "0x5CBac41D9D997A1c3Cb77E4f19FA16d6e2d433a4", - "dates": [ - 1630060095, 1632479295, 1633688895, 1634898495, 1636108095, 1637317695, 1638527295, 1639736895, 1640946495, 1642156095, - 1643365695, 1644575295, 1645784895, 1646994495, 1648204095, 1649413695, 1650623295, 1651832895, 1653042495, 1654252095, - 1655461695, 1656671295, 1657880895, 1659090495, 1660300095, 1661509695, 1662719295, 1663928895, 1665138495, 1666348095, - 1667557695, 1668767295, 1669976895, 1671186495, 1672396095, 1673605695, 1674815295, 1676024895, 1677234495, 1678444095, - 1679653695, 1680863295, 1682072895, 1683282495, 1684492095, 1685701695, 1686911295, 1688120895, 1689330495, 1691749695, - 1694168895 - ], - "amounts": [ - 3650346320346153844, 3650346320346153844, 1614230769230769250, 3650346320346153844, 1614230769230769230, 3650346320346153844, - 1614230769230769230, 3650346320346153844, 1614230769230769230, 3650346320346153844, 1614230769230769230, 3650346320346153844, - 1614230769230769230, 3650346320346153844, 1614230769230769230, 3650346320346153844, 1614230769230769230, 3650346320346153844, - 1614230769230769230, 3650346320346153844, 1614230769230769230, 3650346320346153844, 1614230769230769230, 3650346320346153844, - 1614230769230769230, 3650346320346153844, 1614230769230769230, 3650346320346153844, 1614230769230769230, 3650346320346153844, - 1614230769230769230, 3650346320346153844, 1614230769230769230, 3650346320346153844, 1614230769230769230, 3650346320346153844, - 1614230769230769230, 3650346320346153844, 1614230769230769230, 3650346320346153844, 1614230769230769230, 3650346320346153844, - 1614230769230769230, 914961704961538460, 1614230769230769230, 748461538461538460, 1614230769230769230, 491153846153846153, - 1614230769230769230, 1614230769230769230, 1614230769230769230 - ] - }, - { - "user": "0x9fb96c6556562E660811c99da2351d99DbFDB188", - "vesting": "0x19740FF28227c751223099B5FBF6B742C5BD62BA", - "dates": [ - 1630060095, 1631269695, 1632479295, 1633688895, 1634898495, 1636108095, 1637317695, 1638527295, 1639736895, 1640946495, - 1642156095, 1643365695, 1644575295, 1645784895, 1646994495, 1648204095, 1650623295, 1653042495 - ], - "amounts": [ - 16871600397420458122, 51062829782590736121, 16871600397420458122, 51062829782590736114, 16871600397420458122, - 51062829782590736114, 16871600397420458122, 51062829782590736114, 16871600397420458122, 51062829782590736114, - 16871600397420458122, 51062829782590736114, 16871600397420458122, 51062829782590736114, 9260600397420458122, - 38429616474861235673, 38429616474861235673, 38429616474861235673 - ] - } + { + "user": "0xF5366a3445Adf491311a6e86443bfb5392f08265", + "vesting": "0xD30381e7fEcE6D5b278c187370066f842Cd927C9", + "dates": [ + 1620383295, 1622802495, 1625221695, 1627640895, 1630060095, 1632479295, 1633688895, + 1634898495, 1636108095, 1637317695, 1638527295, 1639736895, 1640946495, 1642156095, + 1643365695, 1644575295, 1645784895, 1646994495, 1648204095, 1649413695, 1650623295, + 1651832895, 1653042495, 1654252095, 1655461695, 1656671295, 1657880895, 1659090495, + 1660300095, 1661509695, 1662719295, 1663928895, 1665138495, 1666348095, 1667557695, + 1668767295, 1669976895, 1671186495, 1672396095, 1673605695, 1674815295, 1676024895, + 1677234495, 1678444095, 1679653695, 1680863295, 1682072895, 1683282495, 1684492095, + 1685701695, 1686911295, 1689330495, 1691749695, 1694168895 + ], + "amounts": [ + 5873461538461538500, 11162859640346153860, 12565936563423076944, 12565936563423076920, + 12565936563423076920, 12565936563423076920, 1614230769230769250, 12565936563423076920, + 1614230769230769230, 12565936563423076920, 1614230769230769230, 12565936563423076920, + 1614230769230769230, 12565936563423076920, 1614230769230769230, 12565936563423076920, + 1614230769230769230, 12565936563423076920, 1614230769230769230, 12565936563423076920, + 1614230769230769230, 12565936563423076920, 1614230769230769230, 12565936563423076920, + 1614230769230769230, 12565936563423076920, 1614230769230769230, 12565936563423076920, + 1614230769230769230, 12565936563423076920, 1614230769230769230, 12565936563423076920, + 1614230769230769230, 12565936563423076920, 1614230769230769230, 12565936563423076920, + 1614230769230769230, 12565936563423076920, 1614230769230769230, 12565936563423076920, + 1614230769230769230, 12565936563423076920, 1614230769230769230, 12565936563423076920, + 1614230769230769230, 12565936563423076920, 1614230769230769230, 6692475024961538460, + 1614230769230769230, 1403076923076923076, 1614230769230769230, 1614230769230769230, + 1614230769230769230, 1614230769230769230 + ] + }, + { + "user": "0x3d91ad906C3969f86C588C988F721d5a735c6f81", + "vesting": "0x055f9921fc7F9474F75805dD4A82a99CDc40599b", + "dates": [ + 1633688895, 1636108095, 1638527295, 1640946495, 1643365695, 1645784895, 1648204095, + 1650623295, 1653042495, 1655461695, 1657880895, 1660300095, 1662719295, 1665138495, + 1667557695, 1669976895, 1672396095, 1674815295, 1677234495, 1679653695, 1682072895, + 1684492095, 1686911295, 1689330495, 1691749695, 1694168895 + ], + "amounts": [ + 2018076923076923100, 2018076923076923076, 2018076923076923076, 2018076923076923076, + 2018076923076923076, 2018076923076923076, 2018076923076923076, 2018076923076923076, + 2018076923076923076, 2018076923076923076, 2018076923076923076, 2018076923076923076, + 2018076923076923076, 2018076923076923076, 2018076923076923076, 2018076923076923076, + 2018076923076923076, 2018076923076923076, 2018076923076923076, 2018076923076923076, + 2018076923076923076, 2018076923076923076, 2018076923076923076, 2018076923076923076, + 2018076923076923076, 2018076923076923076 + ] + }, + { + "user": "0x3d91ad906C3969f86C588C988F721d5a735c6f81", + "vesting": "0xb4c9DBaf4885063fd06eccFF821f76aF49f9B7C8", + "dates": [ + 1632479295, 1633688895, 1634898495, 1636108095, 1637317695, 1638527295, 1639736895, + 1640946495, 1642156095, 1643365695, 1644575295, 1645784895, 1646994495, 1648204095, + 1649413695, 1650623295, 1651832895, 1653042495, 1654252095, 1655461695 + ], + "amounts": [ + 45668591586275745958, 36599600383168147936, 45668591586275745946, 36599600383168147921, + 45668591586275745946, 36599600383168147921, 45668591586275745946, 36599600383168147921, + 45668591586275745946, 36599600383168147921, 45668591586275745946, 36599600383168147921, + 31980091586275745946, 35092066582373476215, 20909973849664128348, 24080946941970523698, + 13817324936461310003, 15334185839868350947, 7788805529050305208, 7442588257786551439 + ] + }, + { + "user": "0x9fb96c6556562E660811c99da2351d99DbFDB188", + "vesting": "0x66a5d36FC35424a22d64Ce4240d84E52d678554F", + "dates": [1631269695, 1633688895, 1636108095, 1638527295], + "amounts": [ + 400000000000000000000, 400000000000000000000, 400000000000000000000, + 400000000000000000000 + ] + }, + { + "user": "0x9fb96c6556562E660811c99da2351d99DbFDB188", + "vesting": "0x5CBac41D9D997A1c3Cb77E4f19FA16d6e2d433a4", + "dates": [ + 1630060095, 1632479295, 1633688895, 1634898495, 1636108095, 1637317695, 1638527295, + 1639736895, 1640946495, 1642156095, 1643365695, 1644575295, 1645784895, 1646994495, + 1648204095, 1649413695, 1650623295, 1651832895, 1653042495, 1654252095, 1655461695, + 1656671295, 1657880895, 1659090495, 1660300095, 1661509695, 1662719295, 1663928895, + 1665138495, 1666348095, 1667557695, 1668767295, 1669976895, 1671186495, 1672396095, + 1673605695, 1674815295, 1676024895, 1677234495, 1678444095, 1679653695, 1680863295, + 1682072895, 1683282495, 1684492095, 1685701695, 1686911295, 1688120895, 1689330495, + 1691749695, 1694168895 + ], + "amounts": [ + 3650346320346153844, 3650346320346153844, 1614230769230769250, 3650346320346153844, + 1614230769230769230, 3650346320346153844, 1614230769230769230, 3650346320346153844, + 1614230769230769230, 3650346320346153844, 1614230769230769230, 3650346320346153844, + 1614230769230769230, 3650346320346153844, 1614230769230769230, 3650346320346153844, + 1614230769230769230, 3650346320346153844, 1614230769230769230, 3650346320346153844, + 1614230769230769230, 3650346320346153844, 1614230769230769230, 3650346320346153844, + 1614230769230769230, 3650346320346153844, 1614230769230769230, 3650346320346153844, + 1614230769230769230, 3650346320346153844, 1614230769230769230, 3650346320346153844, + 1614230769230769230, 3650346320346153844, 1614230769230769230, 3650346320346153844, + 1614230769230769230, 3650346320346153844, 1614230769230769230, 3650346320346153844, + 1614230769230769230, 3650346320346153844, 1614230769230769230, 914961704961538460, + 1614230769230769230, 748461538461538460, 1614230769230769230, 491153846153846153, + 1614230769230769230, 1614230769230769230, 1614230769230769230 + ] + }, + { + "user": "0x9fb96c6556562E660811c99da2351d99DbFDB188", + "vesting": "0x19740FF28227c751223099B5FBF6B742C5BD62BA", + "dates": [ + 1630060095, 1631269695, 1632479295, 1633688895, 1634898495, 1636108095, 1637317695, + 1638527295, 1639736895, 1640946495, 1642156095, 1643365695, 1644575295, 1645784895, + 1646994495, 1648204095, 1650623295, 1653042495 + ], + "amounts": [ + 16871600397420458122, 51062829782590736121, 16871600397420458122, 51062829782590736114, + 16871600397420458122, 51062829782590736114, 16871600397420458122, 51062829782590736114, + 16871600397420458122, 51062829782590736114, 16871600397420458122, 51062829782590736114, + 16871600397420458122, 51062829782590736114, 9260600397420458122, 38429616474861235673, + 38429616474861235673, 38429616474861235673 + ] + } ] diff --git a/scripts/swapTest/swap_test.json b/scripts/swapTest/swap_test.json index 926a2286c..7c7971656 100644 --- a/scripts/swapTest/swap_test.json +++ b/scripts/swapTest/swap_test.json @@ -1,13 +1,13 @@ { - "mocOracleAddress": "0x19d0cB77b5CA190166680fDD2FD89d98e8ED829E", - "rskOracleAddress": "0x0A609957F468F4B3E0B10f0dE4e32A778707Bd98", - "mocState": "0xd78a78c6071270679AF17a2779C07f55A5105553", - "sovrynProtocol": "0x777aFf110993d84Cf0F220FFB62895E2B48B5701", - "PriceFeeds": "0x7c13eCC9f004824fDA6b22B9eFd80AA8C6B932Af", - "WRBTC": "0x20199DDe770fe0750008D17ECDBd20B0Cb7a324C", - "SUSD": "0x6Cc8A4Cb85de29eBAb663C18c64B19900028A60a", - "loanTokenSUSD": "0x0F891Ea4233196141b4d9A3Ac7e14Ff133C9F6fE", - "loanTokenRBTC": "0x094d0dEd1e5b3f943f91AAEa8237cd3FbfeD2459", - "SOV": "0x5F39E84F670B132F260e8d7Af155bE7c9dfa9f5b", - "lockedSOV": "0xd56D54832BCB0d2093898E57807ef40824E52635" + "mocOracleAddress": "0x19d0cB77b5CA190166680fDD2FD89d98e8ED829E", + "rskOracleAddress": "0x0A609957F468F4B3E0B10f0dE4e32A778707Bd98", + "mocState": "0xd78a78c6071270679AF17a2779C07f55A5105553", + "sovrynProtocol": "0x777aFf110993d84Cf0F220FFB62895E2B48B5701", + "PriceFeeds": "0x7c13eCC9f004824fDA6b22B9eFd80AA8C6B932Af", + "WRBTC": "0x20199DDe770fe0750008D17ECDBd20B0Cb7a324C", + "SUSD": "0x6Cc8A4Cb85de29eBAb663C18c64B19900028A60a", + "loanTokenSUSD": "0x0F891Ea4233196141b4d9A3Ac7e14Ff133C9F6fE", + "loanTokenRBTC": "0x094d0dEd1e5b3f943f91AAEa8237cd3FbfeD2459", + "SOV": "0x5F39E84F670B132F260e8d7Af155bE7c9dfa9f5b", + "lockedSOV": "0xd56D54832BCB0d2093898E57807ef40824E52635" } diff --git a/scripts/swapTest/swap_test_bpro.json b/scripts/swapTest/swap_test_bpro.json index 2476bb363..f74ad3230 100644 --- a/scripts/swapTest/swap_test_bpro.json +++ b/scripts/swapTest/swap_test_bpro.json @@ -1,9 +1,9 @@ { - "sovrynProtocol": "0x638c91EeC3Ab0BBc639752eFaB2Bc1114dA49c44", - "WRBTC": "0x612367368562cF6271CC94547746f852D798886b", - "UnderlyingToken": "0x457468D8F76308CE2E2deb8DD2B08c16232ABF59", - "loanToken": "0x9D789336a3c57bDaC0F1E2fd68030aAa7AF53f2b", - "loanTokenRBTC": "0xD502936015604fCfeb140de44e236b426fE2F2B0", - "UnderlyingTokenPriceFeed": "0x84551BE4696ef4061E4d60c6E90E5ad5dDAb7d3a", - "WRBTCPriceFeed": "0x23BeA2479c889a479fec21bd4c395FD234506E08" + "sovrynProtocol": "0x638c91EeC3Ab0BBc639752eFaB2Bc1114dA49c44", + "WRBTC": "0x612367368562cF6271CC94547746f852D798886b", + "UnderlyingToken": "0x457468D8F76308CE2E2deb8DD2B08c16232ABF59", + "loanToken": "0x9D789336a3c57bDaC0F1E2fd68030aAa7AF53f2b", + "loanTokenRBTC": "0xD502936015604fCfeb140de44e236b426fE2F2B0", + "UnderlyingTokenPriceFeed": "0x84551BE4696ef4061E4d60c6E90E5ad5dDAb7d3a", + "WRBTCPriceFeed": "0x23BeA2479c889a479fec21bd4c395FD234506E08" } diff --git a/scripts/swapTest/swap_test_usdt.json b/scripts/swapTest/swap_test_usdt.json index a01bb2b7a..643b11a83 100644 --- a/scripts/swapTest/swap_test_usdt.json +++ b/scripts/swapTest/swap_test_usdt.json @@ -1,11 +1,11 @@ { - "sovrynProtocol": "0x5A0D867e0D70Fcc6Ade25C3F1B89d618b5B4Eaa7", - "WRBTC": "0x542fda317318ebf1d3deaf76e0b632741a7e677d", - "UnderlyingToken": "0xef213441A85dF4d7ACbDaE0Cf78004e1E486bB96", - "loanTokenSettings": "0xacd90e8f092d91928Ed373F2Ed662F5213B31B75", - "loanToken": "0x849C47f9C259E9D62F289BF1b2729039698D8387", - "loanTokenSettingsWRBTC": "0x0E0E9F3AbCCa53D62d1721470B9dA8C89709960E", - "loanTokenRBTC": "0xa9DcDC63eaBb8a2b6f39D7fF9429d88340044a7A", - "UnderlyingTokenPriceFeed": "0xEd80Ccde8bAeFf2dBFC70d3028a27e501Fa0D7D5", - "WRBTCPriceFeed": "0x54c33Cb8a3a32A716BeC40C3CEB5bA8B0fB92a57" + "sovrynProtocol": "0x5A0D867e0D70Fcc6Ade25C3F1B89d618b5B4Eaa7", + "WRBTC": "0x542fda317318ebf1d3deaf76e0b632741a7e677d", + "UnderlyingToken": "0xef213441A85dF4d7ACbDaE0Cf78004e1E486bB96", + "loanTokenSettings": "0xacd90e8f092d91928Ed373F2Ed662F5213B31B75", + "loanToken": "0x849C47f9C259E9D62F289BF1b2729039698D8387", + "loanTokenSettingsWRBTC": "0x0E0E9F3AbCCa53D62d1721470B9dA8C89709960E", + "loanTokenRBTC": "0xa9DcDC63eaBb8a2b6f39D7fF9429d88340044a7A", + "UnderlyingTokenPriceFeed": "0xEd80Ccde8bAeFf2dBFC70d3028a27e501Fa0D7D5", + "WRBTCPriceFeed": "0x54c33Cb8a3a32A716BeC40C3CEB5bA8B0fB92a57" } diff --git a/scripts/swap_test.json b/scripts/swap_test.json index 3b2052116..fc3f48503 100644 --- a/scripts/swap_test.json +++ b/scripts/swap_test.json @@ -1,10 +1,10 @@ { - "medianizer": "0xe0aA552A10d7EC8760Fc6c246D391E698a82dDf9", - "sovrynProtocol": "0xcCB53c9429d32594F404d01fbe9E65ED1DCda8D9", - "WRBTC": "0x602C71e4DAC47a042Ee7f46E0aee17F94A3bA0B6", - "SUSD": "0x3194cBDC3dbcd3E11a07892e7bA5c3394048Cc87", - "loanTokenSettingsSUSD": "0x82c83b7f88aef2eD99d4869D547b6ED28e69C8df", - "loanTokenSUSD": "0x724Ca58E1e6e64BFB1E15d7Eec0fe1E5f581c7bD", - "loanTokenSettingsWRBTC": "0xaA7e46855e0506401214c9b1C35f3d889669609e", - "loanTokenRBTC": "0xa1910C6e0Fbd0E38cfBabAeC5D1C1D539F81CC63" + "medianizer": "0xe0aA552A10d7EC8760Fc6c246D391E698a82dDf9", + "sovrynProtocol": "0xcCB53c9429d32594F404d01fbe9E65ED1DCda8D9", + "WRBTC": "0x602C71e4DAC47a042Ee7f46E0aee17F94A3bA0B6", + "SUSD": "0x3194cBDC3dbcd3E11a07892e7bA5c3394048Cc87", + "loanTokenSettingsSUSD": "0x82c83b7f88aef2eD99d4869D547b6ED28e69C8df", + "loanTokenSUSD": "0x724Ca58E1e6e64BFB1E15d7Eec0fe1E5f581c7bD", + "loanTokenSettingsWRBTC": "0xaA7e46855e0506401214c9b1C35f3d889669609e", + "loanTokenRBTC": "0xa1910C6e0Fbd0E38cfBabAeC5D1C1D539F81CC63" } diff --git a/scripts/uniswap/eth_mainnet_contracts.json b/scripts/uniswap/eth_mainnet_contracts.json index 14f1d3207..d683b868b 100644 --- a/scripts/uniswap/eth_mainnet_contracts.json +++ b/scripts/uniswap/eth_mainnet_contracts.json @@ -1,12 +1,12 @@ { - "multisigOwners": [ - "0x4C3d3505d34213751c4b4d621cB6bDe7E664E222", - "0x9E0816a71B53ca67201a5088df960fE90910DE55", - "0x27D55f5668eF4438635bdCE0aDCA083507E77752", - "0x400D61743A96c73160682e8073A1ffa1AD1f1bC3", - "0xDBE9fDECb4510ab20BB5A6D32C720f1048704A44" - ], - "ethMultisig": "0xdD0E3546EEBf3f1Cc4454a16b4DC5b677923bDC1", - "UniswapV2Router02": "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", - "eSOV": "0xbdab72602e9ad40fc6a6852caf43258113b8f7a5" + "multisigOwners": [ + "0x4C3d3505d34213751c4b4d621cB6bDe7E664E222", + "0x9E0816a71B53ca67201a5088df960fE90910DE55", + "0x27D55f5668eF4438635bdCE0aDCA083507E77752", + "0x400D61743A96c73160682e8073A1ffa1AD1f1bC3", + "0xDBE9fDECb4510ab20BB5A6D32C720f1048704A44" + ], + "ethMultisig": "0xdD0E3546EEBf3f1Cc4454a16b4DC5b677923bDC1", + "UniswapV2Router02": "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + "eSOV": "0xbdab72602e9ad40fc6a6852caf43258113b8f7a5" } diff --git a/scripts/uniswap/eth_testnet_contracts.json b/scripts/uniswap/eth_testnet_contracts.json index 551872cfd..eb602013f 100644 --- a/scripts/uniswap/eth_testnet_contracts.json +++ b/scripts/uniswap/eth_testnet_contracts.json @@ -1,12 +1,12 @@ { - "multisigOwners": [ - "0x4C3d3505d34213751c4b4d621cB6bDe7E664E222", - "0x9E0816a71B53ca67201a5088df960fE90910DE55", - "0x27D55f5668eF4438635bdCE0aDCA083507E77752", - "0x400D61743A96c73160682e8073A1ffa1AD1f1bC3", - "0xDBE9fDECb4510ab20BB5A6D32C720f1048704A44" - ], - "ethMultisig": "0xf5972e2bcc10404367cbdca2a3319470fbea3ff7", - "UniswapV2Router02": "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", - "eSOV": "" + "multisigOwners": [ + "0x4C3d3505d34213751c4b4d621cB6bDe7E664E222", + "0x9E0816a71B53ca67201a5088df960fE90910DE55", + "0x27D55f5668eF4438635bdCE0aDCA083507E77752", + "0x400D61743A96c73160682e8073A1ffa1AD1f1bC3", + "0xDBE9fDECb4510ab20BB5A6D32C720f1048704A44" + ], + "ethMultisig": "0xf5972e2bcc10404367cbdca2a3319470fbea3ff7", + "UniswapV2Router02": "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + "eSOV": "" } diff --git a/tests/EscrowReward/anyone.test.js b/tests/EscrowReward/anyone.test.js index a0825c57c..1db7aa11c 100644 --- a/tests/EscrowReward/anyone.test.js +++ b/tests/EscrowReward/anyone.test.js @@ -28,9 +28,9 @@ const VestingFactory = artifacts.require("VestingFactory"); const VestingRegistry = artifacts.require("VestingRegistry3"); const { - BN, // Big Number support. - expectRevert, - constants, // Assertions for transactions that should fail. + BN, // Big Number support. + expectRevert, + constants, // Assertions for transactions that should fail. } = require("@openzeppelin/test-helpers"); const { assert } = require("chai"); @@ -50,7 +50,7 @@ const infiniteTokens = maxRandom * 100; // A lot of tokens, enough to run all te * @return {number} Random Value. */ function randomValue() { - return Math.floor(Math.random() * maxRandom); + return Math.floor(Math.random() * maxRandom); } /** @@ -60,230 +60,303 @@ function randomValue() { * @return {number} Current Timestamp. */ function currentTimestamp() { - return Math.floor(Date.now() / 1000); + return Math.floor(Date.now() / 1000); } contract("Escrow Rewards (Any User Functions)", (accounts) => { - let escrowReward, sov, lockedSOV; - let creator, multisig, newMultisig, safeVault, userOne, userTwo, userThree, userFour, userFive; - let value, valueOne, valueTwo, reward, rewardOneTwo; - - /// @dev Status flow: Deployed => Deposit - before("Initiating Accounts & Creating Test Token Instance.", async () => { - // Checking if we have enough accounts to test. - assert.isAtLeast(accounts.length, 9, "At least 9 accounts are required to test the contracts."); - [creator, multisig, newMultisig, safeVault, userOne, userTwo, userThree, userFour, userFive] = accounts; - - // Creating the instance of SOV Token. - sov = await SOV.new("Sovryn", "SOV", 18, zero); - - // Creating the Staking Instance. - stakingLogic = await StakingLogic.new(sov.address); - staking = await StakingProxy.new(sov.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingLogic.at(staking.address); - - // Creating the FeeSharing Instance. - feeSharingProxy = await FeeSharingProxy.new(constants.ZERO_ADDRESS, staking.address); - - // Creating the Vesting Instance. - vestingLogic = await VestingLogic.new(); - vestingFactory = await VestingFactory.new(vestingLogic.address); - vestingRegistry = await VestingRegistry.new( - vestingFactory.address, - sov.address, - staking.address, - feeSharingProxy.address, - creator // This should be Governance Timelock Contract. - ); - vestingFactory.transferOwnership(vestingRegistry.address); - - // Creating the instance of newLockedSOV Contract. - lockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [multisig]); - - // Creating the contract instance. - escrowReward = await EscrowReward.new(lockedSOV.address, sov.address, multisig, zero, depositLimit, { from: creator }); - - // Marking the contract as active. - await escrowReward.init({ from: multisig }); - - // Adding the contract as an admin in the lockedSOV. - await lockedSOV.addAdmin(escrowReward.address, { from: multisig }); - - /// @dev Minting and test values calculation moved here for optimization - value = randomValue() + 1; - valueOne = randomValue() + 100; - valueTwo = randomValue() + 100; - await sov.mint(userOne, infiniteTokens); - await sov.mint(userTwo, infiniteTokens); - reward = Math.ceil(value / 100); - rewardOneTwo = Math.ceil((valueOne + valueTwo) / 100); - await sov.mint(multisig, infiniteTokens); - }); - - /// @dev Status flow: Deposit => Deposit - it("Before anyone can deposit Tokens during Deposit State, they should approve the escrow contract with the amount to send.", async () => { - // let value = randomValue() + 1; - // await sov.mint(userOne, value); - await expectRevert(escrowReward.depositTokens(value, { from: userOne }), "invalid transfer"); - - /// @dev Approve more than needed, to be used on the following tests. - await sov.approve(escrowReward.address, infiniteTokens, { from: userOne }); - await sov.approve(escrowReward.address, infiniteTokens, { from: multisig }); - }); - - /// @dev Status flow: Deposit => Deposit - it("Except Multisig, no one should be able to call the init() function.", async () => { - await expectRevert(escrowReward.init({ from: userOne }), "Only Multisig can call this."); - }); - - /// @dev Status flow: Deposit => Deposit - it("Except Multisig, no one should be able to update the Multisig.", async () => { - await expectRevert(escrowReward.updateMultisig(newMultisig, { from: userOne }), "Only Multisig can call this."); - }); - - /// @dev Status flow: Deposit => Deposit - it("Except Multisig, no one should be able to update the release time.", async () => { - await expectRevert(escrowReward.updateReleaseTimestamp(currentTimestamp(), { from: userOne }), "Only Multisig can call this."); - }); - - /// @dev Status flow: Deposit => Deposit - it("Except Multisig, no one should be able to update the deposit limit.", async () => { - await expectRevert(escrowReward.updateDepositLimit(zero, { from: userOne }), "Only Multisig can call this."); - }); - - /// @dev Status flow: Deposit => Deposit - it("Anyone could deposit Tokens during Deposit State.", async () => { - // let value = randomValue() + 1; - // await sov.mint(userOne, value); - - // await sov.approve(escrowReward.address, value, { from: userOne }); - await escrowReward.depositTokens(value, { from: userOne }); - }); - - /// @dev Status flow: Deposit => Deposit - it("No one could deposit zero Tokens during Deposit State.", async () => { - await expectRevert(escrowReward.depositTokens(zero, { from: userOne }), "Amount needs to be bigger than zero."); - }); - - /// @dev Status flow: Deposit => Deposit - it("No one should be able to withdraw unless the Release Time has not passed.", async () => { - await escrowReward.updateReleaseTimestamp(currentTimestamp() + 3000, { from: multisig }); - await expectRevert(escrowReward.withdrawTokensAndReward({ from: userOne }), "The release time has not started yet."); - }); - - /// @dev Status flow: Deposit => Holding - it("No one could deposit Tokens during any other State other than Deposit.", async () => { - await escrowReward.changeStateToHolding({ from: multisig }); - - // let value = randomValue() + 1; - // await sov.mint(userOne, value); - // await sov.approve(escrowReward.address, value, { from: userOne }); - - await expectRevert(escrowReward.depositTokens(value, { from: userOne }), "The contract is not in the right state."); - }); - - /// @dev Status flow: Holding => Holding - it("Except Multisig, no one should be able to change the contract to Holding State.", async () => { - await expectRevert(escrowReward.changeStateToHolding({ from: userOne }), "Only Multisig can call this."); - }); - - /// @dev Status flow: Holding => Holding - it("Except Multisig, no one should be able to withdraw all token to safeVault.", async () => { - await expectRevert(escrowReward.withdrawTokensByMultisig(safeVault, { from: userOne }), "Only Multisig can call this."); - }); - - /// @dev Status flow: Holding => Holding - it("Except Multisig, no one should be able to deposit tokens using depositTokensByMultisig.", async () => { - await expectRevert(escrowReward.depositTokensByMultisig(zero, { from: userOne }), "Only Multisig can call this."); - }); - - /// @dev Status flow: Holding => Withdraw - it("No one should be able to withdraw unless the Release Time has not set (i.e. Zero).", async () => { - let oldSOVBal = new BN(await sov.balanceOf(multisig)); - await escrowReward.withdrawTokensByMultisig(constants.ZERO_ADDRESS, { from: multisig }); - let newSOVBal = new BN(await sov.balanceOf(multisig)); - let value = newSOVBal.sub(oldSOVBal); - - // await sov.approve(escrowReward.address, value, { from: multisig }); - - await escrowReward.depositTokensByMultisig(value, { from: multisig }); - - await expectRevert(escrowReward.withdrawTokensAndReward({ from: userOne }), "The release time has not started yet."); - }); - - /// @dev Status flow: NEW CONTRACT! Deployed => Deposit - it("No one should be able to withdraw unless in the Withdraw State.", async () => { - // Creating the contract instance. - escrowReward = await EscrowReward.new(lockedSOV.address, sov.address, multisig, zero, depositLimit, { from: creator }); - - // Marking the contract as active. - await escrowReward.init({ from: multisig }); - - // Adding the contract as an admin in the lockedSOV. - await lockedSOV.addAdmin(escrowReward.address, { from: multisig }); - - await escrowReward.updateReleaseTimestamp(currentTimestamp(), { from: multisig }); - - await expectRevert(escrowReward.withdrawTokensAndReward({ from: userOne }), "The contract is not in the right state."); - }); - - /// @dev Status flow: Deposit => Holding => Withdraw - it("Anyone should be able to withdraw all his tokens and bonus in the Withdraw State.", async () => { - // let value = randomValue() + 100; - // let reward = Math.ceil(value / 100); - // await sov.mint(userOne, value); - await sov.approve(escrowReward.address, value, { from: userOne }); - await escrowReward.depositTokens(value, { from: userOne }); - await escrowReward.updateReleaseTimestamp(currentTimestamp(), { from: multisig }); - await escrowReward.changeStateToHolding({ from: multisig }); - await escrowReward.withdrawTokensByMultisig(constants.ZERO_ADDRESS, { from: multisig }); - // await sov.mint(multisig, reward); - await sov.approve(escrowReward.address, reward, { from: multisig }); - await escrowReward.depositRewardByMultisig(reward, { from: multisig }); - await sov.approve(escrowReward.address, value, { from: multisig }); - await escrowReward.depositTokensByMultisig(value, { from: multisig }); - await escrowReward.withdrawTokensAndReward({ from: userOne }); - }); - - /// @dev Status flow: NEW CONTRACT! Deposit => Holding => Withdraw - it("Multiple users should be able to withdraw all their tokens and corresponding rewards in the Withdraw State.", async () => { - // Creating the contract instance. - escrowReward = await EscrowReward.new(lockedSOV.address, sov.address, multisig, zero, depositLimit, { from: creator }); - - // Marking the contract as active. - await escrowReward.init({ from: multisig }); - - // Adding the contract as an admin in the lockedSOV. - await lockedSOV.addAdmin(escrowReward.address, { from: multisig }); - - // let valueOne = randomValue() + 100; - // await sov.mint(userOne, valueOne); - await sov.approve(escrowReward.address, valueOne, { from: userOne }); - await escrowReward.depositTokens(valueOne, { from: userOne }); - - // let valueTwo = randomValue() + 100; - // await sov.mint(userTwo, valueTwo); - - /// @dev Approve more than needed, to be used on this test and potential future ones. - // await sov.approve(escrowReward.address, valueTwo, { from: userTwo }); - await sov.approve(escrowReward.address, infiniteTokens, { from: userTwo }); - - await escrowReward.depositTokens(valueTwo, { from: userTwo }); - await escrowReward.updateReleaseTimestamp(currentTimestamp(), { from: multisig }); - await escrowReward.changeStateToHolding({ from: multisig }); - await escrowReward.withdrawTokensByMultisig(constants.ZERO_ADDRESS, { from: multisig }); - - let totalDeposit = valueOne + valueTwo; - // let reward = Math.ceil(totalDeposit / 100); - // await sov.mint(multisig, reward); - await sov.approve(escrowReward.address, rewardOneTwo, { from: multisig }); - await escrowReward.depositRewardByMultisig(rewardOneTwo, { from: multisig }); - - await sov.approve(escrowReward.address, totalDeposit, { from: multisig }); - await escrowReward.depositTokensByMultisig(totalDeposit, { from: multisig }); - - await escrowReward.withdrawTokensAndReward({ from: userOne }); - await escrowReward.withdrawTokensAndReward({ from: userTwo }); - }); + let escrowReward, sov, lockedSOV; + let creator, multisig, newMultisig, safeVault, userOne, userTwo, userThree, userFour, userFive; + let value, valueOne, valueTwo, reward, rewardOneTwo; + + /// @dev Status flow: Deployed => Deposit + before("Initiating Accounts & Creating Test Token Instance.", async () => { + // Checking if we have enough accounts to test. + assert.isAtLeast( + accounts.length, + 9, + "At least 9 accounts are required to test the contracts." + ); + [ + creator, + multisig, + newMultisig, + safeVault, + userOne, + userTwo, + userThree, + userFour, + userFive, + ] = accounts; + + // Creating the instance of SOV Token. + sov = await SOV.new("Sovryn", "SOV", 18, zero); + + // Creating the Staking Instance. + stakingLogic = await StakingLogic.new(sov.address); + staking = await StakingProxy.new(sov.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); + + // Creating the FeeSharing Instance. + feeSharingProxy = await FeeSharingProxy.new(constants.ZERO_ADDRESS, staking.address); + + // Creating the Vesting Instance. + vestingLogic = await VestingLogic.new(); + vestingFactory = await VestingFactory.new(vestingLogic.address); + vestingRegistry = await VestingRegistry.new( + vestingFactory.address, + sov.address, + staking.address, + feeSharingProxy.address, + creator // This should be Governance Timelock Contract. + ); + vestingFactory.transferOwnership(vestingRegistry.address); + + // Creating the instance of newLockedSOV Contract. + lockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [ + multisig, + ]); + + // Creating the contract instance. + escrowReward = await EscrowReward.new( + lockedSOV.address, + sov.address, + multisig, + zero, + depositLimit, + { from: creator } + ); + + // Marking the contract as active. + await escrowReward.init({ from: multisig }); + + // Adding the contract as an admin in the lockedSOV. + await lockedSOV.addAdmin(escrowReward.address, { from: multisig }); + + /// @dev Minting and test values calculation moved here for optimization + value = randomValue() + 1; + valueOne = randomValue() + 100; + valueTwo = randomValue() + 100; + await sov.mint(userOne, infiniteTokens); + await sov.mint(userTwo, infiniteTokens); + reward = Math.ceil(value / 100); + rewardOneTwo = Math.ceil((valueOne + valueTwo) / 100); + await sov.mint(multisig, infiniteTokens); + }); + + /// @dev Status flow: Deposit => Deposit + it("Before anyone can deposit Tokens during Deposit State, they should approve the escrow contract with the amount to send.", async () => { + // let value = randomValue() + 1; + // await sov.mint(userOne, value); + await expectRevert( + escrowReward.depositTokens(value, { from: userOne }), + "invalid transfer" + ); + + /// @dev Approve more than needed, to be used on the following tests. + await sov.approve(escrowReward.address, infiniteTokens, { from: userOne }); + await sov.approve(escrowReward.address, infiniteTokens, { from: multisig }); + }); + + /// @dev Status flow: Deposit => Deposit + it("Except Multisig, no one should be able to call the init() function.", async () => { + await expectRevert(escrowReward.init({ from: userOne }), "Only Multisig can call this."); + }); + + /// @dev Status flow: Deposit => Deposit + it("Except Multisig, no one should be able to update the Multisig.", async () => { + await expectRevert( + escrowReward.updateMultisig(newMultisig, { from: userOne }), + "Only Multisig can call this." + ); + }); + + /// @dev Status flow: Deposit => Deposit + it("Except Multisig, no one should be able to update the release time.", async () => { + await expectRevert( + escrowReward.updateReleaseTimestamp(currentTimestamp(), { from: userOne }), + "Only Multisig can call this." + ); + }); + + /// @dev Status flow: Deposit => Deposit + it("Except Multisig, no one should be able to update the deposit limit.", async () => { + await expectRevert( + escrowReward.updateDepositLimit(zero, { from: userOne }), + "Only Multisig can call this." + ); + }); + + /// @dev Status flow: Deposit => Deposit + it("Anyone could deposit Tokens during Deposit State.", async () => { + // let value = randomValue() + 1; + // await sov.mint(userOne, value); + + // await sov.approve(escrowReward.address, value, { from: userOne }); + await escrowReward.depositTokens(value, { from: userOne }); + }); + + /// @dev Status flow: Deposit => Deposit + it("No one could deposit zero Tokens during Deposit State.", async () => { + await expectRevert( + escrowReward.depositTokens(zero, { from: userOne }), + "Amount needs to be bigger than zero." + ); + }); + + /// @dev Status flow: Deposit => Deposit + it("No one should be able to withdraw unless the Release Time has not passed.", async () => { + await escrowReward.updateReleaseTimestamp(currentTimestamp() + 3000, { from: multisig }); + await expectRevert( + escrowReward.withdrawTokensAndReward({ from: userOne }), + "The release time has not started yet." + ); + }); + + /// @dev Status flow: Deposit => Holding + it("No one could deposit Tokens during any other State other than Deposit.", async () => { + await escrowReward.changeStateToHolding({ from: multisig }); + + // let value = randomValue() + 1; + // await sov.mint(userOne, value); + // await sov.approve(escrowReward.address, value, { from: userOne }); + + await expectRevert( + escrowReward.depositTokens(value, { from: userOne }), + "The contract is not in the right state." + ); + }); + + /// @dev Status flow: Holding => Holding + it("Except Multisig, no one should be able to change the contract to Holding State.", async () => { + await expectRevert( + escrowReward.changeStateToHolding({ from: userOne }), + "Only Multisig can call this." + ); + }); + + /// @dev Status flow: Holding => Holding + it("Except Multisig, no one should be able to withdraw all token to safeVault.", async () => { + await expectRevert( + escrowReward.withdrawTokensByMultisig(safeVault, { from: userOne }), + "Only Multisig can call this." + ); + }); + + /// @dev Status flow: Holding => Holding + it("Except Multisig, no one should be able to deposit tokens using depositTokensByMultisig.", async () => { + await expectRevert( + escrowReward.depositTokensByMultisig(zero, { from: userOne }), + "Only Multisig can call this." + ); + }); + + /// @dev Status flow: Holding => Withdraw + it("No one should be able to withdraw unless the Release Time has not set (i.e. Zero).", async () => { + let oldSOVBal = new BN(await sov.balanceOf(multisig)); + await escrowReward.withdrawTokensByMultisig(constants.ZERO_ADDRESS, { from: multisig }); + let newSOVBal = new BN(await sov.balanceOf(multisig)); + let value = newSOVBal.sub(oldSOVBal); + + // await sov.approve(escrowReward.address, value, { from: multisig }); + + await escrowReward.depositTokensByMultisig(value, { from: multisig }); + + await expectRevert( + escrowReward.withdrawTokensAndReward({ from: userOne }), + "The release time has not started yet." + ); + }); + + /// @dev Status flow: NEW CONTRACT! Deployed => Deposit + it("No one should be able to withdraw unless in the Withdraw State.", async () => { + // Creating the contract instance. + escrowReward = await EscrowReward.new( + lockedSOV.address, + sov.address, + multisig, + zero, + depositLimit, + { from: creator } + ); + + // Marking the contract as active. + await escrowReward.init({ from: multisig }); + + // Adding the contract as an admin in the lockedSOV. + await lockedSOV.addAdmin(escrowReward.address, { from: multisig }); + + await escrowReward.updateReleaseTimestamp(currentTimestamp(), { from: multisig }); + + await expectRevert( + escrowReward.withdrawTokensAndReward({ from: userOne }), + "The contract is not in the right state." + ); + }); + + /// @dev Status flow: Deposit => Holding => Withdraw + it("Anyone should be able to withdraw all his tokens and bonus in the Withdraw State.", async () => { + // let value = randomValue() + 100; + // let reward = Math.ceil(value / 100); + // await sov.mint(userOne, value); + await sov.approve(escrowReward.address, value, { from: userOne }); + await escrowReward.depositTokens(value, { from: userOne }); + await escrowReward.updateReleaseTimestamp(currentTimestamp(), { from: multisig }); + await escrowReward.changeStateToHolding({ from: multisig }); + await escrowReward.withdrawTokensByMultisig(constants.ZERO_ADDRESS, { from: multisig }); + // await sov.mint(multisig, reward); + await sov.approve(escrowReward.address, reward, { from: multisig }); + await escrowReward.depositRewardByMultisig(reward, { from: multisig }); + await sov.approve(escrowReward.address, value, { from: multisig }); + await escrowReward.depositTokensByMultisig(value, { from: multisig }); + await escrowReward.withdrawTokensAndReward({ from: userOne }); + }); + + /// @dev Status flow: NEW CONTRACT! Deposit => Holding => Withdraw + it("Multiple users should be able to withdraw all their tokens and corresponding rewards in the Withdraw State.", async () => { + // Creating the contract instance. + escrowReward = await EscrowReward.new( + lockedSOV.address, + sov.address, + multisig, + zero, + depositLimit, + { from: creator } + ); + + // Marking the contract as active. + await escrowReward.init({ from: multisig }); + + // Adding the contract as an admin in the lockedSOV. + await lockedSOV.addAdmin(escrowReward.address, { from: multisig }); + + // let valueOne = randomValue() + 100; + // await sov.mint(userOne, valueOne); + await sov.approve(escrowReward.address, valueOne, { from: userOne }); + await escrowReward.depositTokens(valueOne, { from: userOne }); + + // let valueTwo = randomValue() + 100; + // await sov.mint(userTwo, valueTwo); + + /// @dev Approve more than needed, to be used on this test and potential future ones. + // await sov.approve(escrowReward.address, valueTwo, { from: userTwo }); + await sov.approve(escrowReward.address, infiniteTokens, { from: userTwo }); + + await escrowReward.depositTokens(valueTwo, { from: userTwo }); + await escrowReward.updateReleaseTimestamp(currentTimestamp(), { from: multisig }); + await escrowReward.changeStateToHolding({ from: multisig }); + await escrowReward.withdrawTokensByMultisig(constants.ZERO_ADDRESS, { from: multisig }); + + let totalDeposit = valueOne + valueTwo; + // let reward = Math.ceil(totalDeposit / 100); + // await sov.mint(multisig, reward); + await sov.approve(escrowReward.address, rewardOneTwo, { from: multisig }); + await escrowReward.depositRewardByMultisig(rewardOneTwo, { from: multisig }); + + await sov.approve(escrowReward.address, totalDeposit, { from: multisig }); + await escrowReward.depositTokensByMultisig(totalDeposit, { from: multisig }); + + await escrowReward.withdrawTokensAndReward({ from: userOne }); + await escrowReward.withdrawTokensAndReward({ from: userTwo }); + }); }); diff --git a/tests/EscrowReward/creator.test.js b/tests/EscrowReward/creator.test.js index d60fdbdac..a1633a72e 100644 --- a/tests/EscrowReward/creator.test.js +++ b/tests/EscrowReward/creator.test.js @@ -28,9 +28,9 @@ const FeeSharingProxy = artifacts.require("FeeSharingProxyMockup"); const SOV = artifacts.require("TestToken"); const { - BN, // Big Number support. - expectRevert, - constants, // Assertions for transactions that should fail. + BN, // Big Number support. + expectRevert, + constants, // Assertions for transactions that should fail. } = require("@openzeppelin/test-helpers"); const { assert } = require("chai"); @@ -51,7 +51,7 @@ let value; * @return {number} Random Value. */ function randomValue() { - return Math.floor(Math.random() * 1000000); + return Math.floor(Math.random() * 1000000); } /** @@ -61,139 +61,200 @@ function randomValue() { * @return {number} Current Timestamp. */ function currentTimestamp() { - return Math.floor(Date.now() / 1000); + return Math.floor(Date.now() / 1000); } contract("Escrow Rewards (Creator Functions)", (accounts) => { - let escrowReward, sov, lockedSOV; - let creator, multisig, newMultisig, safeVault, userOne, userTwo, userThree, userFour, userFive; - - before("Initiating Accounts & Creating Test Token Instance.", async () => { - // Checking if we have enough accounts to test. - assert.isAtLeast(accounts.length, 9, "At least 9 accounts are required to test the contracts."); - [creator, multisig, newMultisig, safeVault, userOne, userTwo, userThree, userFour, userFive] = accounts; - - // Creating the instance of SOV Token. - sov = await SOV.new("Sovryn", "SOV", 18, zero); - - // Creating the Staking Instance. - stakingLogic = await StakingLogic.new(sov.address); - staking = await StakingProxy.new(sov.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingLogic.at(staking.address); - - // Creating the FeeSharing Instance. - feeSharingProxy = await FeeSharingProxy.new(constants.ZERO_ADDRESS, staking.address); - - // Creating the Vesting Instance. - vestingLogic = await VestingLogic.new(); - vestingFactory = await VestingFactory.new(vestingLogic.address); - vestingRegistry = await VestingRegistry.new( - vestingFactory.address, - sov.address, - staking.address, - feeSharingProxy.address, - creator // This should be Governance Timelock Contract. - ); - vestingFactory.transferOwnership(vestingRegistry.address); - - // Creating the instance of newLockedSOV Contract. - lockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [multisig]); - - // Creating the contract instance. - escrowReward = await EscrowReward.new(lockedSOV.address, sov.address, multisig, zero, depositLimit, { from: creator }); - - // Marking the contract as active. - await escrowReward.init({ from: multisig }); - - // Adding the contract as an admin in the lockedSOV. - await lockedSOV.addAdmin(escrowReward.address, { from: multisig }); - - /// @dev Minting, approval and test values calculation moved here for optimization - value = randomValue() + 1; - await sov.mint(creator, value * 2); // To be spent along 2 tests - await sov.approve(escrowReward.address, value * 2, { from: creator }); - }); - - it("Creator should be able to create Escrow Contract without specifying the locked sov contract.", async () => { - // Creating the contract instance. - newEscrowReward = await EscrowReward.new(zeroAddress, sov.address, multisig, zero, depositLimit, { from: creator }); - }); - - it("Creator should not be able to create Escrow Contract without specifying the sov contract.", async () => { - // Creating the contract instance. - await expectRevert( - EscrowReward.new(lockedSOV.address, zeroAddress, multisig, zero, depositLimit, { from: creator }), - "Invalid SOV Address." - ); - }); - - it("Creator should not be able to create Escrow Contract without specifying the Multisig.", async () => { - // Creating the contract instance. - await expectRevert( - EscrowReward.new(lockedSOV.address, sov.address, zeroAddress, zero, depositLimit, { from: creator }), - "Invalid Multisig Address." - ); - }); - - it("Creator should not be able to call the init() function.", async () => { - await expectRevert(escrowReward.init({ from: creator }), "Only Multisig can call this."); - }); - - it("Creator should not be able to update the Multisig.", async () => { - await expectRevert(escrowReward.updateMultisig(newMultisig, { from: creator }), "Only Multisig can call this."); - }); - - it("Creator should not be able to update the release time.", async () => { - await expectRevert(escrowReward.updateReleaseTimestamp(currentTimestamp(), { from: creator }), "Only Multisig can call this."); - }); - - it("Creator should not be able to update the deposit limit.", async () => { - await expectRevert(escrowReward.updateDepositLimit(zero, { from: creator }), "Only Multisig can call this."); - }); - - it("Creator could deposit Tokens during Deposit State.", async () => { - await escrowReward.depositTokens(value, { from: creator }); - }); - - it("Creator could not deposit Tokens during any other State other than Deposit.", async () => { - await escrowReward.changeStateToHolding({ from: multisig }); - - await expectRevert(escrowReward.depositTokens(value, { from: creator }), "The contract is not in the right state."); - }); - - it("Creator should not be able to change the contract to Holding State.", async () => { - await expectRevert(escrowReward.changeStateToHolding({ from: creator }), "Only Multisig can call this."); - }); - - it("Creator should not be able to withdraw all token to safeVault.", async () => { - await expectRevert(escrowReward.withdrawTokensByMultisig(safeVault, { from: creator }), "Only Multisig can call this."); - }); - - it("Creator should not be able to deposit tokens using depositTokensByMultisig.", async () => { - await expectRevert(escrowReward.depositTokensByMultisig(zero, { from: creator }), "Only Multisig can call this."); - }); - - it("Creator should not be able to withdraw unless in the Withdraw State.", async () => { - await escrowReward.updateReleaseTimestamp(currentTimestamp(), { from: multisig }); - await expectRevert(escrowReward.withdrawTokensAndReward({ from: creator }), "The contract is not in the right state."); - }); - - it("Creator should not be able to withdraw unless the Release Time has not passed.", async () => { - await escrowReward.updateReleaseTimestamp(currentTimestamp() * 2, { from: multisig }); - - let oldSOVBal = new BN(await sov.balanceOf(multisig)); - await escrowReward.withdrawTokensByMultisig(constants.ZERO_ADDRESS, { from: multisig }); - let newSOVBal = new BN(await sov.balanceOf(multisig)); - let value = newSOVBal.sub(oldSOVBal); - await sov.approve(escrowReward.address, value, { from: multisig }); - await escrowReward.depositTokensByMultisig(value, { from: multisig }); - - await expectRevert(escrowReward.withdrawTokensAndReward({ from: creator }), "The release time has not started yet."); - }); - - it("Creator should be able to withdraw all his tokens and bonus in the Withdraw State.", async () => { - await escrowReward.updateReleaseTimestamp(currentTimestamp(), { from: multisig }); - await escrowReward.withdrawTokensAndReward({ from: creator }); - }); + let escrowReward, sov, lockedSOV; + let creator, multisig, newMultisig, safeVault, userOne, userTwo, userThree, userFour, userFive; + + before("Initiating Accounts & Creating Test Token Instance.", async () => { + // Checking if we have enough accounts to test. + assert.isAtLeast( + accounts.length, + 9, + "At least 9 accounts are required to test the contracts." + ); + [ + creator, + multisig, + newMultisig, + safeVault, + userOne, + userTwo, + userThree, + userFour, + userFive, + ] = accounts; + + // Creating the instance of SOV Token. + sov = await SOV.new("Sovryn", "SOV", 18, zero); + + // Creating the Staking Instance. + stakingLogic = await StakingLogic.new(sov.address); + staking = await StakingProxy.new(sov.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); + + // Creating the FeeSharing Instance. + feeSharingProxy = await FeeSharingProxy.new(constants.ZERO_ADDRESS, staking.address); + + // Creating the Vesting Instance. + vestingLogic = await VestingLogic.new(); + vestingFactory = await VestingFactory.new(vestingLogic.address); + vestingRegistry = await VestingRegistry.new( + vestingFactory.address, + sov.address, + staking.address, + feeSharingProxy.address, + creator // This should be Governance Timelock Contract. + ); + vestingFactory.transferOwnership(vestingRegistry.address); + + // Creating the instance of newLockedSOV Contract. + lockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [ + multisig, + ]); + + // Creating the contract instance. + escrowReward = await EscrowReward.new( + lockedSOV.address, + sov.address, + multisig, + zero, + depositLimit, + { from: creator } + ); + + // Marking the contract as active. + await escrowReward.init({ from: multisig }); + + // Adding the contract as an admin in the lockedSOV. + await lockedSOV.addAdmin(escrowReward.address, { from: multisig }); + + /// @dev Minting, approval and test values calculation moved here for optimization + value = randomValue() + 1; + await sov.mint(creator, value * 2); // To be spent along 2 tests + await sov.approve(escrowReward.address, value * 2, { from: creator }); + }); + + it("Creator should be able to create Escrow Contract without specifying the locked sov contract.", async () => { + // Creating the contract instance. + newEscrowReward = await EscrowReward.new( + zeroAddress, + sov.address, + multisig, + zero, + depositLimit, + { from: creator } + ); + }); + + it("Creator should not be able to create Escrow Contract without specifying the sov contract.", async () => { + // Creating the contract instance. + await expectRevert( + EscrowReward.new(lockedSOV.address, zeroAddress, multisig, zero, depositLimit, { + from: creator, + }), + "Invalid SOV Address." + ); + }); + + it("Creator should not be able to create Escrow Contract without specifying the Multisig.", async () => { + // Creating the contract instance. + await expectRevert( + EscrowReward.new(lockedSOV.address, sov.address, zeroAddress, zero, depositLimit, { + from: creator, + }), + "Invalid Multisig Address." + ); + }); + + it("Creator should not be able to call the init() function.", async () => { + await expectRevert(escrowReward.init({ from: creator }), "Only Multisig can call this."); + }); + + it("Creator should not be able to update the Multisig.", async () => { + await expectRevert( + escrowReward.updateMultisig(newMultisig, { from: creator }), + "Only Multisig can call this." + ); + }); + + it("Creator should not be able to update the release time.", async () => { + await expectRevert( + escrowReward.updateReleaseTimestamp(currentTimestamp(), { from: creator }), + "Only Multisig can call this." + ); + }); + + it("Creator should not be able to update the deposit limit.", async () => { + await expectRevert( + escrowReward.updateDepositLimit(zero, { from: creator }), + "Only Multisig can call this." + ); + }); + + it("Creator could deposit Tokens during Deposit State.", async () => { + await escrowReward.depositTokens(value, { from: creator }); + }); + + it("Creator could not deposit Tokens during any other State other than Deposit.", async () => { + await escrowReward.changeStateToHolding({ from: multisig }); + + await expectRevert( + escrowReward.depositTokens(value, { from: creator }), + "The contract is not in the right state." + ); + }); + + it("Creator should not be able to change the contract to Holding State.", async () => { + await expectRevert( + escrowReward.changeStateToHolding({ from: creator }), + "Only Multisig can call this." + ); + }); + + it("Creator should not be able to withdraw all token to safeVault.", async () => { + await expectRevert( + escrowReward.withdrawTokensByMultisig(safeVault, { from: creator }), + "Only Multisig can call this." + ); + }); + + it("Creator should not be able to deposit tokens using depositTokensByMultisig.", async () => { + await expectRevert( + escrowReward.depositTokensByMultisig(zero, { from: creator }), + "Only Multisig can call this." + ); + }); + + it("Creator should not be able to withdraw unless in the Withdraw State.", async () => { + await escrowReward.updateReleaseTimestamp(currentTimestamp(), { from: multisig }); + await expectRevert( + escrowReward.withdrawTokensAndReward({ from: creator }), + "The contract is not in the right state." + ); + }); + + it("Creator should not be able to withdraw unless the Release Time has not passed.", async () => { + await escrowReward.updateReleaseTimestamp(currentTimestamp() * 2, { from: multisig }); + + let oldSOVBal = new BN(await sov.balanceOf(multisig)); + await escrowReward.withdrawTokensByMultisig(constants.ZERO_ADDRESS, { from: multisig }); + let newSOVBal = new BN(await sov.balanceOf(multisig)); + let value = newSOVBal.sub(oldSOVBal); + await sov.approve(escrowReward.address, value, { from: multisig }); + await escrowReward.depositTokensByMultisig(value, { from: multisig }); + + await expectRevert( + escrowReward.withdrawTokensAndReward({ from: creator }), + "The release time has not started yet." + ); + }); + + it("Creator should be able to withdraw all his tokens and bonus in the Withdraw State.", async () => { + await escrowReward.updateReleaseTimestamp(currentTimestamp(), { from: multisig }); + await escrowReward.withdrawTokensAndReward({ from: creator }); + }); }); diff --git a/tests/EscrowReward/event.test.js b/tests/EscrowReward/event.test.js index 8b0e3e8ce..6b74db267 100644 --- a/tests/EscrowReward/event.test.js +++ b/tests/EscrowReward/event.test.js @@ -30,9 +30,9 @@ const StakingProxy = artifacts.require("StakingProxy"); const FeeSharingProxy = artifacts.require("FeeSharingProxyMockup"); const { - BN, // Big Number support. - expectEvent, - constants, // Assertions for transactions that should fail. + BN, // Big Number support. + expectEvent, + constants, // Assertions for transactions that should fail. } = require("@openzeppelin/test-helpers"); const { assert } = require("chai"); @@ -50,7 +50,7 @@ const depositLimit = 75000000; * @return {number} Random Value. */ function randomValue() { - return Math.floor(Math.random() * 1000000); + return Math.floor(Math.random() * 1000000); } /** @@ -60,185 +60,230 @@ function randomValue() { * @return {number} Current Timestamp. */ function currentTimestamp() { - return Math.floor(Date.now() / 1000); + return Math.floor(Date.now() / 1000); } contract("Escrow Rewards (Events)", (accounts) => { - let escrowReward, newEscrowReward, sov, lockedSOV; - let creator, multisig, newMultisig, safeVault, userOne, userTwo, userThree, userFour, userFive; - let txReceiptEscrowRewardDeployment; - - before("Initiating Accounts & Creating Test Token Instance.", async () => { - // Checking if we have enough accounts to test. - assert.isAtLeast(accounts.length, 9, "At least 9 accounts are required to test the contracts."); - [creator, multisig, newMultisig, safeVault, userOne, userTwo, userThree, userFour, userFive] = accounts; - - // Creating the instance of SOV Token. - sov = await SOV.new("Sovryn", "SOV", 18, zero); - - // Creating the Staking Instance. - stakingLogic = await StakingLogic.new(sov.address); - staking = await StakingProxy.new(sov.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingLogic.at(staking.address); - - // Creating the FeeSharing Instance. - feeSharingProxy = await FeeSharingProxy.new(constants.ZERO_ADDRESS, staking.address); - - // Creating the Vesting Instance. - vestingLogic = await VestingLogic.new(); - vestingFactory = await VestingFactory.new(vestingLogic.address); - vestingRegistry = await VestingRegistry.new( - vestingFactory.address, - sov.address, - staking.address, - feeSharingProxy.address, - creator // This should be Governance Timelock Contract. - ); - vestingFactory.transferOwnership(vestingRegistry.address); - - // Creating the instance of newLockedSOV Contract. - lockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [multisig]); - - // Creating the contract instance. - escrowReward = await EscrowReward.new(lockedSOV.address, sov.address, multisig, zero, depositLimit, { from: creator }); - newEscrowReward = await EscrowReward.new(lockedSOV.address, sov.address, multisig, zero, depositLimit, { from: creator }); - - // Marking the contract as active. - txReceiptEscrowRewardDeployment = await escrowReward.init({ from: multisig }); - - // Adding the contract as an admin in the lockedSOV. - await lockedSOV.addAdmin(escrowReward.address, { from: multisig }); - }); - - it("Calling the init() will emit EscrowActivated Event.", async () => { - /// @dev using the init call from the before hook, for optimization - expectEvent(txReceiptEscrowRewardDeployment, "EscrowActivated"); - }); - - it("Updating the release time should emit TokenReleaseUpdated Event.", async () => { - let timestamp = currentTimestamp(); - let txReceipt = await escrowReward.updateReleaseTimestamp(timestamp, { from: multisig }); - expectEvent(txReceipt, "TokenReleaseUpdated", { - _initiator: multisig, - _releaseTimestamp: new BN(timestamp), - }); - }); - - it("Updating the deposit limit should emit TokenDepositLimitUpdated Event.", async () => { - let txReceipt = await escrowReward.updateDepositLimit(zero, { from: multisig }); - expectEvent(txReceipt, "TokenDepositLimitUpdated", { - _initiator: multisig, - _depositLimit: new BN(zero), - }); - }); - - it("Depositing Tokens by Users should emit TokenDeposit Event.", async () => { - let value = randomValue() + 1; - await escrowReward.updateDepositLimit(value + 1, { from: multisig }); - await sov.mint(userOne, value); - await sov.approve(escrowReward.address, value, { from: userOne }); - let txReceipt = await escrowReward.depositTokens(value, { from: userOne }); - expectEvent(txReceipt, "TokenDeposit", { - _initiator: userOne, - _amount: new BN(value), - }); - }); - - it("Reaching the Deposit Limit should emit TokenDeposit Event.", async () => { - let value = randomValue() + 1; - - await sov.mint(userOne, value); - await sov.approve(escrowReward.address, value, { from: userOne }); - - let txReceipt = await escrowReward.depositTokens(value, { from: userOne }); - expectEvent(txReceipt, "DepositLimitReached"); - }); - - it("Changing the contract to Holding State should emit EscrowInHoldingState Event.", async () => { - let txReceipt = await escrowReward.changeStateToHolding({ from: multisig }); - expectEvent(txReceipt, "EscrowInHoldingState"); - }); - - it("Multisig token withdraw should emit TokenWithdrawByMultisig Event.", async () => { - let beforeSafeVaultSOVBalance = await sov.balanceOf(safeVault); - let txReceipt = await escrowReward.withdrawTokensByMultisig(safeVault, { from: multisig }); - let afterSafeVaultSOVBalance = await sov.balanceOf(safeVault); - expectEvent(txReceipt, "TokenWithdrawByMultisig", { - _initiator: multisig, - _amount: new BN(afterSafeVaultSOVBalance - beforeSafeVaultSOVBalance), - }); - }); - - it("Multisig reward token deposit should emit RewardDepositsByMultisig Event.", async () => { - let reward = randomValue() + 1; - await sov.mint(multisig, reward); - await sov.approve(escrowReward.address, reward, { from: multisig }); - let txReceipt = await escrowReward.depositRewardByMultisig(reward, { from: multisig }); - expectEvent(txReceipt, "RewardDepositByMultisig", { - _initiator: multisig, - _amount: new BN(reward), - }); - }); - - it("Multisig token deposit should emit TokenDepositByMultisig Event.", async () => { - let value = randomValue() + 1; - await sov.mint(multisig, value); - await sov.approve(escrowReward.address, value, { from: multisig }); - let txReceipt = await escrowReward.depositTokensByMultisig(value, { from: multisig }); - expectEvent(txReceipt, "TokenDepositByMultisig", { - _initiator: multisig, - _amount: new BN(value), - }); - }); - - it("Updating the Multisig should emit NewMultisig Event.", async () => { - let txReceipt = await escrowReward.updateMultisig(newMultisig, { from: multisig }); - expectEvent(txReceipt, "NewMultisig", { - _initiator: multisig, - _newMultisig: newMultisig, - }); - }); - - it("Updating the Locked SOV Contract Address should emit LockedSOVUpdated Event.", async () => { - let newLockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [multisig]); - let txReceipt = await newEscrowReward.updateLockedSOV(newLockedSOV.address, { from: multisig }); - expectEvent(txReceipt, "LockedSOVUpdated", { - _initiator: multisig, - _lockedSOV: newLockedSOV.address, - }); - }); - - it("SOV and Reward withdraw should emit TokenWithdraw and RewardTokenWithdraw Events", async () => { - // Creating the contract instance. - escrowReward = await EscrowReward.new(lockedSOV.address, sov.address, multisig, zero, depositLimit, { from: creator }); - - // Marking the contract as active. - await escrowReward.init({ from: multisig }); - - // Adding the contract as an admin in the lockedSOV. - await lockedSOV.addAdmin(escrowReward.address, { from: multisig }); - - let value = randomValue() + 100; - let reward = Math.ceil(value / 100); - await sov.mint(userOne, value); - await sov.approve(escrowReward.address, value, { from: userOne }); - await escrowReward.depositTokens(value, { from: userOne }); - await escrowReward.updateReleaseTimestamp(currentTimestamp(), { from: multisig }); - await escrowReward.changeStateToHolding({ from: multisig }); - await escrowReward.withdrawTokensByMultisig(constants.ZERO_ADDRESS, { from: multisig }); - await sov.mint(multisig, reward); - await sov.approve(escrowReward.address, reward, { from: multisig }); - await escrowReward.depositRewardByMultisig(reward, { from: multisig }); - await sov.approve(escrowReward.address, value, { from: multisig }); - await escrowReward.depositTokensByMultisig(value, { from: multisig }); - - let txReceipt = await escrowReward.withdrawTokensAndReward({ from: userOne }); - expectEvent(txReceipt, "TokenWithdraw", { - _initiator: userOne, - }); - expectEvent(txReceipt, "RewardTokenWithdraw", { - _initiator: userOne, - }); - }); + let escrowReward, newEscrowReward, sov, lockedSOV; + let creator, multisig, newMultisig, safeVault, userOne, userTwo, userThree, userFour, userFive; + let txReceiptEscrowRewardDeployment; + + before("Initiating Accounts & Creating Test Token Instance.", async () => { + // Checking if we have enough accounts to test. + assert.isAtLeast( + accounts.length, + 9, + "At least 9 accounts are required to test the contracts." + ); + [ + creator, + multisig, + newMultisig, + safeVault, + userOne, + userTwo, + userThree, + userFour, + userFive, + ] = accounts; + + // Creating the instance of SOV Token. + sov = await SOV.new("Sovryn", "SOV", 18, zero); + + // Creating the Staking Instance. + stakingLogic = await StakingLogic.new(sov.address); + staking = await StakingProxy.new(sov.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); + + // Creating the FeeSharing Instance. + feeSharingProxy = await FeeSharingProxy.new(constants.ZERO_ADDRESS, staking.address); + + // Creating the Vesting Instance. + vestingLogic = await VestingLogic.new(); + vestingFactory = await VestingFactory.new(vestingLogic.address); + vestingRegistry = await VestingRegistry.new( + vestingFactory.address, + sov.address, + staking.address, + feeSharingProxy.address, + creator // This should be Governance Timelock Contract. + ); + vestingFactory.transferOwnership(vestingRegistry.address); + + // Creating the instance of newLockedSOV Contract. + lockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [ + multisig, + ]); + + // Creating the contract instance. + escrowReward = await EscrowReward.new( + lockedSOV.address, + sov.address, + multisig, + zero, + depositLimit, + { from: creator } + ); + newEscrowReward = await EscrowReward.new( + lockedSOV.address, + sov.address, + multisig, + zero, + depositLimit, + { from: creator } + ); + + // Marking the contract as active. + txReceiptEscrowRewardDeployment = await escrowReward.init({ from: multisig }); + + // Adding the contract as an admin in the lockedSOV. + await lockedSOV.addAdmin(escrowReward.address, { from: multisig }); + }); + + it("Calling the init() will emit EscrowActivated Event.", async () => { + /// @dev using the init call from the before hook, for optimization + expectEvent(txReceiptEscrowRewardDeployment, "EscrowActivated"); + }); + + it("Updating the release time should emit TokenReleaseUpdated Event.", async () => { + let timestamp = currentTimestamp(); + let txReceipt = await escrowReward.updateReleaseTimestamp(timestamp, { from: multisig }); + expectEvent(txReceipt, "TokenReleaseUpdated", { + _initiator: multisig, + _releaseTimestamp: new BN(timestamp), + }); + }); + + it("Updating the deposit limit should emit TokenDepositLimitUpdated Event.", async () => { + let txReceipt = await escrowReward.updateDepositLimit(zero, { from: multisig }); + expectEvent(txReceipt, "TokenDepositLimitUpdated", { + _initiator: multisig, + _depositLimit: new BN(zero), + }); + }); + + it("Depositing Tokens by Users should emit TokenDeposit Event.", async () => { + let value = randomValue() + 1; + await escrowReward.updateDepositLimit(value + 1, { from: multisig }); + await sov.mint(userOne, value); + await sov.approve(escrowReward.address, value, { from: userOne }); + let txReceipt = await escrowReward.depositTokens(value, { from: userOne }); + expectEvent(txReceipt, "TokenDeposit", { + _initiator: userOne, + _amount: new BN(value), + }); + }); + + it("Reaching the Deposit Limit should emit TokenDeposit Event.", async () => { + let value = randomValue() + 1; + + await sov.mint(userOne, value); + await sov.approve(escrowReward.address, value, { from: userOne }); + + let txReceipt = await escrowReward.depositTokens(value, { from: userOne }); + expectEvent(txReceipt, "DepositLimitReached"); + }); + + it("Changing the contract to Holding State should emit EscrowInHoldingState Event.", async () => { + let txReceipt = await escrowReward.changeStateToHolding({ from: multisig }); + expectEvent(txReceipt, "EscrowInHoldingState"); + }); + + it("Multisig token withdraw should emit TokenWithdrawByMultisig Event.", async () => { + let beforeSafeVaultSOVBalance = await sov.balanceOf(safeVault); + let txReceipt = await escrowReward.withdrawTokensByMultisig(safeVault, { from: multisig }); + let afterSafeVaultSOVBalance = await sov.balanceOf(safeVault); + expectEvent(txReceipt, "TokenWithdrawByMultisig", { + _initiator: multisig, + _amount: new BN(afterSafeVaultSOVBalance - beforeSafeVaultSOVBalance), + }); + }); + + it("Multisig reward token deposit should emit RewardDepositsByMultisig Event.", async () => { + let reward = randomValue() + 1; + await sov.mint(multisig, reward); + await sov.approve(escrowReward.address, reward, { from: multisig }); + let txReceipt = await escrowReward.depositRewardByMultisig(reward, { from: multisig }); + expectEvent(txReceipt, "RewardDepositByMultisig", { + _initiator: multisig, + _amount: new BN(reward), + }); + }); + + it("Multisig token deposit should emit TokenDepositByMultisig Event.", async () => { + let value = randomValue() + 1; + await sov.mint(multisig, value); + await sov.approve(escrowReward.address, value, { from: multisig }); + let txReceipt = await escrowReward.depositTokensByMultisig(value, { from: multisig }); + expectEvent(txReceipt, "TokenDepositByMultisig", { + _initiator: multisig, + _amount: new BN(value), + }); + }); + + it("Updating the Multisig should emit NewMultisig Event.", async () => { + let txReceipt = await escrowReward.updateMultisig(newMultisig, { from: multisig }); + expectEvent(txReceipt, "NewMultisig", { + _initiator: multisig, + _newMultisig: newMultisig, + }); + }); + + it("Updating the Locked SOV Contract Address should emit LockedSOVUpdated Event.", async () => { + let newLockedSOV = await LockedSOV.new( + sov.address, + vestingRegistry.address, + cliff, + duration, + [multisig] + ); + let txReceipt = await newEscrowReward.updateLockedSOV(newLockedSOV.address, { + from: multisig, + }); + expectEvent(txReceipt, "LockedSOVUpdated", { + _initiator: multisig, + _lockedSOV: newLockedSOV.address, + }); + }); + + it("SOV and Reward withdraw should emit TokenWithdraw and RewardTokenWithdraw Events", async () => { + // Creating the contract instance. + escrowReward = await EscrowReward.new( + lockedSOV.address, + sov.address, + multisig, + zero, + depositLimit, + { from: creator } + ); + + // Marking the contract as active. + await escrowReward.init({ from: multisig }); + + // Adding the contract as an admin in the lockedSOV. + await lockedSOV.addAdmin(escrowReward.address, { from: multisig }); + + let value = randomValue() + 100; + let reward = Math.ceil(value / 100); + await sov.mint(userOne, value); + await sov.approve(escrowReward.address, value, { from: userOne }); + await escrowReward.depositTokens(value, { from: userOne }); + await escrowReward.updateReleaseTimestamp(currentTimestamp(), { from: multisig }); + await escrowReward.changeStateToHolding({ from: multisig }); + await escrowReward.withdrawTokensByMultisig(constants.ZERO_ADDRESS, { from: multisig }); + await sov.mint(multisig, reward); + await sov.approve(escrowReward.address, reward, { from: multisig }); + await escrowReward.depositRewardByMultisig(reward, { from: multisig }); + await sov.approve(escrowReward.address, value, { from: multisig }); + await escrowReward.depositTokensByMultisig(value, { from: multisig }); + + let txReceipt = await escrowReward.withdrawTokensAndReward({ from: userOne }); + expectEvent(txReceipt, "TokenWithdraw", { + _initiator: userOne, + }); + expectEvent(txReceipt, "RewardTokenWithdraw", { + _initiator: userOne, + }); + }); }); diff --git a/tests/EscrowReward/multisig.test.js b/tests/EscrowReward/multisig.test.js index 72ebc36e8..b88326083 100644 --- a/tests/EscrowReward/multisig.test.js +++ b/tests/EscrowReward/multisig.test.js @@ -24,9 +24,9 @@ const FeeSharingProxy = artifacts.require("FeeSharingProxyMockup"); const SOV = artifacts.require("TestToken"); const { - BN, // Big Number support. - expectRevert, - constants, // Assertions for transactions that should fail. + BN, // Big Number support. + expectRevert, + constants, // Assertions for transactions that should fail. } = require("@openzeppelin/test-helpers"); const { assert } = require("chai"); @@ -45,7 +45,7 @@ const depositLimit = 75000000; * @return {number} Random Value. */ function randomValue() { - return Math.floor(Math.random() * 1000000); + return Math.floor(Math.random() * 1000000); } /** @@ -55,179 +55,243 @@ function randomValue() { * @return {number} Current Timestamp. */ function currentTimestamp() { - return Math.floor(Date.now() / 1000); + return Math.floor(Date.now() / 1000); } contract("Escrow Rewards (Multisig Functions)", (accounts) => { - let escrowReward, newEscrowReward, sov, lockedSOV; - let creator, multisig, newMultisig, safeVault, userOne, userTwo, userThree, userFour, userFive; - let reward, value, approvalCheckValue; - - before("Initiating Accounts & Creating Test Token Instance.", async () => { - // Checking if we have enough accounts to test. - assert.isAtLeast(accounts.length, 9, "At least 9 accounts are required to test the contracts."); - [creator, multisig, newMultisig, safeVault, userOne, userTwo, userThree, userFour, userFive] = accounts; - - // Creating the instance of SOV Token. - sov = await SOV.new("Sovryn", "SOV", 18, zero); - - // Creating the Staking Instance. - stakingLogic = await StakingLogic.new(sov.address); - staking = await StakingProxy.new(sov.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingLogic.at(staking.address); - - // Creating the FeeSharing Instance. - feeSharingProxy = await FeeSharingProxy.new(constants.ZERO_ADDRESS, staking.address); - - // Creating the Vesting Instance. - vestingLogic = await VestingLogic.new(); - vestingFactory = await VestingFactory.new(vestingLogic.address); - vestingRegistry = await VestingRegistry.new( - vestingFactory.address, - sov.address, - staking.address, - feeSharingProxy.address, - creator // This should be Governance Timelock Contract. - ); - vestingFactory.transferOwnership(vestingRegistry.address); - - // Creating the instance of newLockedSOV Contract. - lockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [multisig]); - - // Creating the contract instance. - escrowReward = await EscrowReward.new(lockedSOV.address, sov.address, multisig, zero, depositLimit, { from: creator }); - - // Marking the contract as active. - await escrowReward.init({ from: multisig }); - - // Adding the contract as an admin in the lockedSOV. - await lockedSOV.addAdmin(escrowReward.address, { from: multisig }); - - /// @dev Minting, approval and test values calculation moved here for optimization - value = randomValue() + 1; - await sov.mint(multisig, value); - reward = randomValue() + 1; - await sov.mint(multisig, reward); - await sov.approve(escrowReward.address, value + reward, { from: multisig }); - approvalCheckValue = value + reward + randomValue(); /// @dev Setting a higher value than default approval - }); - - it("Multisig should be able to call the init() function.", async () => { - // Creating the contract instance. - newEscrowReward = await EscrowReward.new(lockedSOV.address, sov.address, multisig, zero, depositLimit, { from: creator }); - await newEscrowReward.init({ from: multisig }); - }); - - it("Multisig should be able to update the Multisig.", async () => { - await newEscrowReward.updateMultisig(newMultisig, { from: multisig }); - }); - - it("Multisig should not be able to update the Multisig with a Zero Address.", async () => { - await expectRevert(escrowReward.updateMultisig(zeroAddress, { from: multisig }), "New Multisig address invalid."); - }); - - it("Multisig should be able to update the release time.", async () => { - await escrowReward.updateReleaseTimestamp(currentTimestamp(), { from: multisig }); - }); - - it("Multisig should be able to update the deposit limit.", async () => { - await escrowReward.updateDepositLimit(zero, { from: multisig }); - }); - - it("Multisig should not be able to update the deposit limit lower than total deposits.", async () => { - await sov.mint(userOne, value); - await sov.approve(escrowReward.address, value, { from: userOne }); - await escrowReward.updateDepositLimit(value, { from: multisig }); - await escrowReward.depositTokens(value, { from: userOne }); - await expectRevert( - escrowReward.updateDepositLimit(value - 1, { from: multisig }), - "Deposit already higher than the limit trying to be set." - ); - }); - - it("Multisig should be able to change the contract to Holding State.", async () => { - await escrowReward.changeStateToHolding({ from: multisig }); - }); - - it("Multisig should not be able to change the contract to Holding State twice.", async () => { - await expectRevert(escrowReward.changeStateToHolding({ from: multisig }), "The contract is not in the right state."); - }); - - it("Multisig should be able to withdraw all token to safeVault.", async () => { - await escrowReward.withdrawTokensByMultisig(safeVault, { from: multisig }); - }); - - it("Multisig should not be able to withdraw all token to safeVault if not in Holding Phase.", async () => { - await expectRevert( - newEscrowReward.withdrawTokensByMultisig(safeVault, { from: newMultisig }), - "The contract is not in the right state." - ); - }); - - it("Multisig should be approved before depositing reward tokens using depositRewardByMultisig.", async () => { - await expectRevert(escrowReward.depositRewardByMultisig(approvalCheckValue, { from: multisig }), "invalid transfer"); - }); - - it("Multisig should not be able to deposit zero reward tokens using depositRewardByMultisig.", async () => { - await expectRevert(escrowReward.depositRewardByMultisig(zero, { from: multisig }), "Amount needs to be bigger than zero."); - }); - - it("Multisig should be able to deposit reward tokens using depositRewardByMultisig.", async () => { - await escrowReward.depositRewardByMultisig(reward, { from: multisig }); - }); - - it("Multisig should be able to deposit tokens using depositTokensByMultisig.", async () => { - await escrowReward.depositTokensByMultisig(value, { from: multisig }); - }); - - it("Multisig should not be able to deposit tokens using depositTokensByMultisig if not in Holding State.", async () => { - await expectRevert(newEscrowReward.depositTokensByMultisig(zero, { from: newMultisig }), "The contract is not in the right state."); - }); - - it("Multisig should not be able to deposit zero tokens using depositTokensByMultisig.", async () => { - await newEscrowReward.changeStateToHolding({ from: newMultisig }); - await expectRevert(newEscrowReward.depositTokensByMultisig(zero, { from: newMultisig }), "Amount needs to be bigger than zero."); - }); - - it("Multisig should be able to update the Locked SOV Address.", async () => { - let newLockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [multisig]); - await escrowReward.updateLockedSOV(newLockedSOV.address, { from: multisig }); - }); - - it("Multisig should not be able to update the Locked SOV Address as a Zero Address.", async () => { - await expectRevert(escrowReward.updateLockedSOV(zeroAddress, { from: multisig }), "Invalid Reward Token Address."); - }); - - it("Multisig should not be able to deposit reward tokens using depositRewardByMultisig during Withdraw State.", async () => { - // Creating the contract instance. - escrowReward = await EscrowReward.new(lockedSOV.address, sov.address, multisig, zero, depositLimit, { from: creator }); - - // Marking the contract as active. - await escrowReward.init({ from: multisig }); - - // Adding the contract as an admin in the lockedSOV. - await lockedSOV.addAdmin(escrowReward.address, { from: multisig }); - - let value = randomValue() + 1; - await sov.mint(userOne, value); - await sov.approve(escrowReward.address, value, { from: userOne }); - await escrowReward.depositTokens(value, { from: userOne }); - - await escrowReward.changeStateToHolding({ from: multisig }); - - await escrowReward.withdrawTokensByMultisig(safeVault, { from: multisig }); - - await sov.mint(multisig, value); - await sov.approve(escrowReward.address, value, { from: multisig }); - await escrowReward.depositTokensByMultisig(value, { from: multisig }); - - let reward = randomValue() + 1; - await sov.mint(multisig, reward); - await sov.approve(escrowReward.address, reward, { from: multisig }); - await expectRevert( - escrowReward.depositRewardByMultisig(reward, { from: multisig }), - "Reward Token deposit is only allowed before User Withdraw starts." - ); - }); + let escrowReward, newEscrowReward, sov, lockedSOV; + let creator, multisig, newMultisig, safeVault, userOne, userTwo, userThree, userFour, userFive; + let reward, value, approvalCheckValue; + + before("Initiating Accounts & Creating Test Token Instance.", async () => { + // Checking if we have enough accounts to test. + assert.isAtLeast( + accounts.length, + 9, + "At least 9 accounts are required to test the contracts." + ); + [ + creator, + multisig, + newMultisig, + safeVault, + userOne, + userTwo, + userThree, + userFour, + userFive, + ] = accounts; + + // Creating the instance of SOV Token. + sov = await SOV.new("Sovryn", "SOV", 18, zero); + + // Creating the Staking Instance. + stakingLogic = await StakingLogic.new(sov.address); + staking = await StakingProxy.new(sov.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); + + // Creating the FeeSharing Instance. + feeSharingProxy = await FeeSharingProxy.new(constants.ZERO_ADDRESS, staking.address); + + // Creating the Vesting Instance. + vestingLogic = await VestingLogic.new(); + vestingFactory = await VestingFactory.new(vestingLogic.address); + vestingRegistry = await VestingRegistry.new( + vestingFactory.address, + sov.address, + staking.address, + feeSharingProxy.address, + creator // This should be Governance Timelock Contract. + ); + vestingFactory.transferOwnership(vestingRegistry.address); + + // Creating the instance of newLockedSOV Contract. + lockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [ + multisig, + ]); + + // Creating the contract instance. + escrowReward = await EscrowReward.new( + lockedSOV.address, + sov.address, + multisig, + zero, + depositLimit, + { from: creator } + ); + + // Marking the contract as active. + await escrowReward.init({ from: multisig }); + + // Adding the contract as an admin in the lockedSOV. + await lockedSOV.addAdmin(escrowReward.address, { from: multisig }); + + /// @dev Minting, approval and test values calculation moved here for optimization + value = randomValue() + 1; + await sov.mint(multisig, value); + reward = randomValue() + 1; + await sov.mint(multisig, reward); + await sov.approve(escrowReward.address, value + reward, { from: multisig }); + approvalCheckValue = value + reward + randomValue(); /// @dev Setting a higher value than default approval + }); + + it("Multisig should be able to call the init() function.", async () => { + // Creating the contract instance. + newEscrowReward = await EscrowReward.new( + lockedSOV.address, + sov.address, + multisig, + zero, + depositLimit, + { from: creator } + ); + await newEscrowReward.init({ from: multisig }); + }); + + it("Multisig should be able to update the Multisig.", async () => { + await newEscrowReward.updateMultisig(newMultisig, { from: multisig }); + }); + + it("Multisig should not be able to update the Multisig with a Zero Address.", async () => { + await expectRevert( + escrowReward.updateMultisig(zeroAddress, { from: multisig }), + "New Multisig address invalid." + ); + }); + + it("Multisig should be able to update the release time.", async () => { + await escrowReward.updateReleaseTimestamp(currentTimestamp(), { from: multisig }); + }); + + it("Multisig should be able to update the deposit limit.", async () => { + await escrowReward.updateDepositLimit(zero, { from: multisig }); + }); + + it("Multisig should not be able to update the deposit limit lower than total deposits.", async () => { + await sov.mint(userOne, value); + await sov.approve(escrowReward.address, value, { from: userOne }); + await escrowReward.updateDepositLimit(value, { from: multisig }); + await escrowReward.depositTokens(value, { from: userOne }); + await expectRevert( + escrowReward.updateDepositLimit(value - 1, { from: multisig }), + "Deposit already higher than the limit trying to be set." + ); + }); + + it("Multisig should be able to change the contract to Holding State.", async () => { + await escrowReward.changeStateToHolding({ from: multisig }); + }); + + it("Multisig should not be able to change the contract to Holding State twice.", async () => { + await expectRevert( + escrowReward.changeStateToHolding({ from: multisig }), + "The contract is not in the right state." + ); + }); + + it("Multisig should be able to withdraw all token to safeVault.", async () => { + await escrowReward.withdrawTokensByMultisig(safeVault, { from: multisig }); + }); + + it("Multisig should not be able to withdraw all token to safeVault if not in Holding Phase.", async () => { + await expectRevert( + newEscrowReward.withdrawTokensByMultisig(safeVault, { from: newMultisig }), + "The contract is not in the right state." + ); + }); + + it("Multisig should be approved before depositing reward tokens using depositRewardByMultisig.", async () => { + await expectRevert( + escrowReward.depositRewardByMultisig(approvalCheckValue, { from: multisig }), + "invalid transfer" + ); + }); + + it("Multisig should not be able to deposit zero reward tokens using depositRewardByMultisig.", async () => { + await expectRevert( + escrowReward.depositRewardByMultisig(zero, { from: multisig }), + "Amount needs to be bigger than zero." + ); + }); + + it("Multisig should be able to deposit reward tokens using depositRewardByMultisig.", async () => { + await escrowReward.depositRewardByMultisig(reward, { from: multisig }); + }); + + it("Multisig should be able to deposit tokens using depositTokensByMultisig.", async () => { + await escrowReward.depositTokensByMultisig(value, { from: multisig }); + }); + + it("Multisig should not be able to deposit tokens using depositTokensByMultisig if not in Holding State.", async () => { + await expectRevert( + newEscrowReward.depositTokensByMultisig(zero, { from: newMultisig }), + "The contract is not in the right state." + ); + }); + + it("Multisig should not be able to deposit zero tokens using depositTokensByMultisig.", async () => { + await newEscrowReward.changeStateToHolding({ from: newMultisig }); + await expectRevert( + newEscrowReward.depositTokensByMultisig(zero, { from: newMultisig }), + "Amount needs to be bigger than zero." + ); + }); + + it("Multisig should be able to update the Locked SOV Address.", async () => { + let newLockedSOV = await LockedSOV.new( + sov.address, + vestingRegistry.address, + cliff, + duration, + [multisig] + ); + await escrowReward.updateLockedSOV(newLockedSOV.address, { from: multisig }); + }); + + it("Multisig should not be able to update the Locked SOV Address as a Zero Address.", async () => { + await expectRevert( + escrowReward.updateLockedSOV(zeroAddress, { from: multisig }), + "Invalid Reward Token Address." + ); + }); + + it("Multisig should not be able to deposit reward tokens using depositRewardByMultisig during Withdraw State.", async () => { + // Creating the contract instance. + escrowReward = await EscrowReward.new( + lockedSOV.address, + sov.address, + multisig, + zero, + depositLimit, + { from: creator } + ); + + // Marking the contract as active. + await escrowReward.init({ from: multisig }); + + // Adding the contract as an admin in the lockedSOV. + await lockedSOV.addAdmin(escrowReward.address, { from: multisig }); + + let value = randomValue() + 1; + await sov.mint(userOne, value); + await sov.approve(escrowReward.address, value, { from: userOne }); + await escrowReward.depositTokens(value, { from: userOne }); + + await escrowReward.changeStateToHolding({ from: multisig }); + + await escrowReward.withdrawTokensByMultisig(safeVault, { from: multisig }); + + await sov.mint(multisig, value); + await sov.approve(escrowReward.address, value, { from: multisig }); + await escrowReward.depositTokensByMultisig(value, { from: multisig }); + + let reward = randomValue() + 1; + await sov.mint(multisig, reward); + await sov.approve(escrowReward.address, reward, { from: multisig }); + await expectRevert( + escrowReward.depositRewardByMultisig(reward, { from: multisig }), + "Reward Token deposit is only allowed before User Withdraw starts." + ); + }); }); diff --git a/tests/EscrowReward/state.test.js b/tests/EscrowReward/state.test.js index 3eaefee80..13e8980d2 100644 --- a/tests/EscrowReward/state.test.js +++ b/tests/EscrowReward/state.test.js @@ -28,8 +28,8 @@ const FeeSharingProxy = artifacts.require("FeeSharingProxyMockup"); const SOV = artifacts.require("TestToken"); const { - BN, - constants, // Assertions for transactions that should fail. + BN, + constants, // Assertions for transactions that should fail. } = require("@openzeppelin/test-helpers"); const { assert } = require("chai"); @@ -40,7 +40,9 @@ let zeroAddress = constants.ZERO_ADDRESS; let cliff = 1; // This is in 4 weeks. i.e. 1 * 4 weeks. let duration = 11; // This is in 4 weeks. i.e. 11 * 4 weeks. const depositLimit = 75000000; -let [deployedStatus, depositStatus, holdingStatus, withdrawStatus, expiredStatus] = [0, 1, 2, 3, 4]; +let [deployedStatus, depositStatus, holdingStatus, withdrawStatus, expiredStatus] = [ + 0, 1, 2, 3, 4, +]; const maxRandom = 1000000; const infiniteTokens = maxRandom * 100; // A lot of tokens, enough to run all tests w/o extra minting @@ -51,7 +53,7 @@ const infiniteTokens = maxRandom * 100; // A lot of tokens, enough to run all te * @return {number} Random Value. */ function randomValue() { - return Math.floor(Math.random() * maxRandom); + return Math.floor(Math.random() * maxRandom); } /** @@ -61,7 +63,7 @@ function randomValue() { * @return {number} Current Timestamp. */ function currentTimestamp() { - return Math.floor(Date.now() / 1000); + return Math.floor(Date.now() / 1000); } /** @@ -82,66 +84,70 @@ function currentTimestamp() { * @param status The current contract status. */ async function checkStatus( - contractInstance, - checkArray, - userAddr, - totalDeposit, - releaseTime, - depositLimit, - totalRewardDeposit, - SOVAddress, - lockedSOVAddress, - multisigAddr, - userDeposit, - userReward, - status + contractInstance, + checkArray, + userAddr, + totalDeposit, + releaseTime, + depositLimit, + totalRewardDeposit, + SOVAddress, + lockedSOVAddress, + multisigAddr, + userDeposit, + userReward, + status ) { - if (checkArray[0] == 1) { - let cValue = await contractInstance.totalDeposit(); - assert.strictEqual(totalDeposit, cValue.toNumber(), "The total deposit does not match."); - } - if (checkArray[1] == 1) { - let cValue = await contractInstance.releaseTime(); - assert.strictEqual(releaseTime, cValue.toNumber(), "The release time does not match."); - } - if (checkArray[2] == 1) { - let cValue = await contractInstance.depositLimit(); - assert.strictEqual(depositLimit, cValue.toNumber(), "The deposit limit does not match."); - } - if (checkArray[3] == 1) { - let cValue = await contractInstance.totalRewardDeposit(); - assert.strictEqual(totalRewardDeposit, cValue.toNumber(), "The total reward deposit does not match."); - } - if (checkArray[4] == 1) { - let cValue = await contractInstance.SOV(); - assert.equal(SOVAddress, cValue, "The SOV Address does not match."); - } - if (checkArray[5] == 1) { - let cValue = await contractInstance.lockedSOV(); - assert.equal(lockedSOVAddress, cValue, "The reward token address does not match."); - } - if (checkArray[6] == 1) { - let cValue = await contractInstance.multisig(); - assert.equal(multisigAddr, cValue, "The multisig address does not match."); - } - if (checkArray[7] == 1) { - let cValue = 0; - await contractInstance.getUserBalance(userAddr).then((data) => { - cValue = data; - }); - assert.equal(userDeposit, cValue.toNumber(), "The user deposit does not match."); - } - if (checkArray[8] == 1) { - let cValue = 0; - await contractInstance.getReward(userAddr).then((data) => { - cValue = data; - }); - assert.equal(userReward, cValue.toNumber(), "The user reward does not match."); - } - if (checkArray[9] == 1) { - let cValue = await contractInstance.status(); - assert.equal(status, cValue.toNumber(), "The contract status does not match."); - } + if (checkArray[0] == 1) { + let cValue = await contractInstance.totalDeposit(); + assert.strictEqual(totalDeposit, cValue.toNumber(), "The total deposit does not match."); + } + if (checkArray[1] == 1) { + let cValue = await contractInstance.releaseTime(); + assert.strictEqual(releaseTime, cValue.toNumber(), "The release time does not match."); + } + if (checkArray[2] == 1) { + let cValue = await contractInstance.depositLimit(); + assert.strictEqual(depositLimit, cValue.toNumber(), "The deposit limit does not match."); + } + if (checkArray[3] == 1) { + let cValue = await contractInstance.totalRewardDeposit(); + assert.strictEqual( + totalRewardDeposit, + cValue.toNumber(), + "The total reward deposit does not match." + ); + } + if (checkArray[4] == 1) { + let cValue = await contractInstance.SOV(); + assert.equal(SOVAddress, cValue, "The SOV Address does not match."); + } + if (checkArray[5] == 1) { + let cValue = await contractInstance.lockedSOV(); + assert.equal(lockedSOVAddress, cValue, "The reward token address does not match."); + } + if (checkArray[6] == 1) { + let cValue = await contractInstance.multisig(); + assert.equal(multisigAddr, cValue, "The multisig address does not match."); + } + if (checkArray[7] == 1) { + let cValue = 0; + await contractInstance.getUserBalance(userAddr).then((data) => { + cValue = data; + }); + assert.equal(userDeposit, cValue.toNumber(), "The user deposit does not match."); + } + if (checkArray[8] == 1) { + let cValue = 0; + await contractInstance.getReward(userAddr).then((data) => { + cValue = data; + }); + assert.equal(userReward, cValue.toNumber(), "The user reward does not match."); + } + if (checkArray[9] == 1) { + let cValue = await contractInstance.status(); + assert.equal(status, cValue.toNumber(), "The contract status does not match."); + } } /** @@ -155,9 +161,9 @@ async function checkStatus( * @return [SOV Balance, Reward Token Balance]. */ async function getTokenBalances(addr, sovContract, lockedSOVContract) { - let sovBal = new BN(await sovContract.balanceOf(addr)); - let rewardBal = new BN(await lockedSOVContract.getLockedBalance(addr)); - return [sovBal, rewardBal]; + let sovBal = new BN(await sovContract.balanceOf(addr)); + let rewardBal = new BN(await lockedSOVContract.getLockedBalance(addr)); + return [sovBal, rewardBal]; } /** @@ -173,14 +179,28 @@ async function getTokenBalances(addr, sovContract, lockedSOVContract) { * * @returns values The values array which was deposited by each user. */ -async function userDeposits(sovContract, escrowRewardContract, userOne, userTwo, userThree, userFour, userFive) { - let values = [randomValue() + 1, randomValue() + 1, randomValue() + 1, randomValue() + 1, randomValue() + 1]; - await escrowRewardContract.depositTokens(values[0], { from: userOne }); - await escrowRewardContract.depositTokens(values[1], { from: userTwo }); - await escrowRewardContract.depositTokens(values[2], { from: userThree }); - await escrowRewardContract.depositTokens(values[3], { from: userFour }); - await escrowRewardContract.depositTokens(values[4], { from: userFive }); - return values; +async function userDeposits( + sovContract, + escrowRewardContract, + userOne, + userTwo, + userThree, + userFour, + userFive +) { + let values = [ + randomValue() + 1, + randomValue() + 1, + randomValue() + 1, + randomValue() + 1, + randomValue() + 1, + ]; + await escrowRewardContract.depositTokens(values[0], { from: userOne }); + await escrowRewardContract.depositTokens(values[1], { from: userTwo }); + await escrowRewardContract.depositTokens(values[2], { from: userThree }); + await escrowRewardContract.depositTokens(values[3], { from: userFour }); + await escrowRewardContract.depositTokens(values[4], { from: userFive }); + return values; } /** @@ -195,31 +215,46 @@ async function userDeposits(sovContract, escrowRewardContract, userOne, userTwo, * @param totalValue The total deposits calculated from values. * @param reward The reward tokens deposited. */ -async function checkUserWithdraw(sovContract, lockedSOVContract, escrowRewardContract, user, index, values, totalValue, reward) { - let beforeUserBalance = await getTokenBalances(user, sovContract, lockedSOVContract); - let userReward = Math.floor(Math.floor(values[index] * reward) / totalValue); - let userContractReward = await escrowRewardContract.getReward(user); - assert.equal(userReward, userContractReward, "The user reward does not match."); - // checkStatus(escrowRewardContract, [0,0,0,0,0,0,0,1,1,0], user, zero, zero, zero, zeroAddress, zeroAddress, zeroAddress, values[index], userReward, zero); - await escrowRewardContract.withdrawTokensAndReward({ from: user }); - let afterUserBalance = await getTokenBalances(user, sovContract, lockedSOVContract); - assert(afterUserBalance[0].eq(beforeUserBalance[0].add(new BN(values[index]))), "User One SOV Token balance is not correct."); - assert(afterUserBalance[1].eq(beforeUserBalance[1].add(new BN(userReward))), "User One Reward Token balance is not correct."); - await checkStatus( - escrowRewardContract, - [0, 0, 0, 0, 0, 0, 0, 1, 1, 0], - user, - zero, - zero, - zero, - zero, - zeroAddress, - zeroAddress, - zeroAddress, - zero, - zero, - zero - ); +async function checkUserWithdraw( + sovContract, + lockedSOVContract, + escrowRewardContract, + user, + index, + values, + totalValue, + reward +) { + let beforeUserBalance = await getTokenBalances(user, sovContract, lockedSOVContract); + let userReward = Math.floor(Math.floor(values[index] * reward) / totalValue); + let userContractReward = await escrowRewardContract.getReward(user); + assert.equal(userReward, userContractReward, "The user reward does not match."); + // checkStatus(escrowRewardContract, [0,0,0,0,0,0,0,1,1,0], user, zero, zero, zero, zeroAddress, zeroAddress, zeroAddress, values[index], userReward, zero); + await escrowRewardContract.withdrawTokensAndReward({ from: user }); + let afterUserBalance = await getTokenBalances(user, sovContract, lockedSOVContract); + assert( + afterUserBalance[0].eq(beforeUserBalance[0].add(new BN(values[index]))), + "User One SOV Token balance is not correct." + ); + assert( + afterUserBalance[1].eq(beforeUserBalance[1].add(new BN(userReward))), + "User One Reward Token balance is not correct." + ); + await checkStatus( + escrowRewardContract, + [0, 0, 0, 0, 0, 0, 0, 1, 1, 0], + user, + zero, + zero, + zero, + zero, + zeroAddress, + zeroAddress, + zeroAddress, + zero, + zero, + zero + ); } /** @@ -236,24 +271,52 @@ async function checkUserWithdraw(sovContract, lockedSOVContract, escrowRewardCon * @param userFive Different Users. * @param percentage The percentage of reward compared to the total SOV Deposit. */ -async function sovAndRewardWithdraw(sov, lockedSOV, escrowReward, multisig, userOne, userTwo, userThree, userFour, userFive, percentage) { - let values = await userDeposits(sov, escrowReward, userOne, userTwo, userThree, userFour, userFive); - let totalValue = values.reduce((a, b) => a + b, 0); - let reward = Math.ceil((totalValue * percentage) / 100); - - await escrowReward.updateReleaseTimestamp(currentTimestamp(), { from: multisig }); - await escrowReward.changeStateToHolding({ from: multisig }); - - await escrowReward.withdrawTokensByMultisig(constants.ZERO_ADDRESS, { from: multisig }); - - await escrowReward.depositRewardByMultisig(reward, { from: multisig }); - await escrowReward.depositTokensByMultisig(totalValue, { from: multisig }); - - await checkUserWithdraw(sov, lockedSOV, escrowReward, userOne, 0, values, totalValue, reward); - await checkUserWithdraw(sov, lockedSOV, escrowReward, userTwo, 1, values, totalValue, reward); - await checkUserWithdraw(sov, lockedSOV, escrowReward, userThree, 2, values, totalValue, reward); - await checkUserWithdraw(sov, lockedSOV, escrowReward, userFour, 3, values, totalValue, reward); - await checkUserWithdraw(sov, lockedSOV, escrowReward, userFive, 4, values, totalValue, reward); +async function sovAndRewardWithdraw( + sov, + lockedSOV, + escrowReward, + multisig, + userOne, + userTwo, + userThree, + userFour, + userFive, + percentage +) { + let values = await userDeposits( + sov, + escrowReward, + userOne, + userTwo, + userThree, + userFour, + userFive + ); + let totalValue = values.reduce((a, b) => a + b, 0); + let reward = Math.ceil((totalValue * percentage) / 100); + + await escrowReward.updateReleaseTimestamp(currentTimestamp(), { from: multisig }); + await escrowReward.changeStateToHolding({ from: multisig }); + + await escrowReward.withdrawTokensByMultisig(constants.ZERO_ADDRESS, { from: multisig }); + + await escrowReward.depositRewardByMultisig(reward, { from: multisig }); + await escrowReward.depositTokensByMultisig(totalValue, { from: multisig }); + + await checkUserWithdraw(sov, lockedSOV, escrowReward, userOne, 0, values, totalValue, reward); + await checkUserWithdraw(sov, lockedSOV, escrowReward, userTwo, 1, values, totalValue, reward); + await checkUserWithdraw( + sov, + lockedSOV, + escrowReward, + userThree, + 2, + values, + totalValue, + reward + ); + await checkUserWithdraw(sov, lockedSOV, escrowReward, userFour, 3, values, totalValue, reward); + await checkUserWithdraw(sov, lockedSOV, escrowReward, userFive, 4, values, totalValue, reward); } /** @@ -268,406 +331,536 @@ async function sovAndRewardWithdraw(sov, lockedSOV, escrowReward, multisig, user * @returns */ async function createEscrowReward(lockedSOV, sov, multisig, releaseTime, depositLimit, creator) { - // Creating the contract instance. - let escrowReward = await EscrowReward.new(lockedSOV.address, sov.address, multisig, releaseTime, depositLimit, { from: creator }); - - // Marking the contract as active. - await escrowReward.init({ from: multisig }); - - // Adding the contract as an admin in the lockedSOV. - await lockedSOV.addAdmin(escrowReward.address, { from: multisig }); - - return escrowReward; + // Creating the contract instance. + let escrowReward = await EscrowReward.new( + lockedSOV.address, + sov.address, + multisig, + releaseTime, + depositLimit, + { from: creator } + ); + + // Marking the contract as active. + await escrowReward.init({ from: multisig }); + + // Adding the contract as an admin in the lockedSOV. + await lockedSOV.addAdmin(escrowReward.address, { from: multisig }); + + return escrowReward; } contract("Escrow Rewards (State)", (accounts) => { - let escrowReward, newEscrowReward, sov, lockedSOV; - let creator, multisig, newMultisig, safeVault, userOne, userTwo, userThree, userFour, userFive; - - async function deploymentAndInitFixture(_wallets, _provider) { - // Checking if we have enough accounts to test. - assert.isAtLeast(accounts.length, 9, "At least 9 accounts are required to test the contracts."); - [creator, multisig, newMultisig, safeVault, userOne, userTwo, userThree, userFour, userFive] = accounts; - - // Creating the instance of SOV Token. - sov = await SOV.new("Sovryn", "SOV", 18, zero); - - // Creating the Staking Instance. - stakingLogic = await StakingLogic.new(sov.address); - staking = await StakingProxy.new(sov.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingLogic.at(staking.address); - - // Creating the FeeSharing Instance. - feeSharingProxy = await FeeSharingProxy.new(constants.ZERO_ADDRESS, staking.address); - - // Creating the Vesting Instance. - vestingLogic = await VestingLogic.new(); - vestingFactory = await VestingFactory.new(vestingLogic.address); - vestingRegistry = await VestingRegistry.new( - vestingFactory.address, - sov.address, - staking.address, - feeSharingProxy.address, - creator // This should be Governance Timelock Contract. - ); - vestingFactory.transferOwnership(vestingRegistry.address); - - // Creating the instance of newLockedSOV Contract. - lockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [multisig]); - - // Creating the contract instance. - escrowReward = await createEscrowReward(lockedSOV, sov, multisig, zero, depositLimit); - - /// @dev Minting, approval calculation moved here for optimization - await sov.mint(userOne, infiniteTokens); - await sov.mint(userTwo, infiniteTokens); - await sov.mint(userThree, infiniteTokens); - await sov.mint(userFour, infiniteTokens); - await sov.mint(userFive, infiniteTokens); - await sov.approve(escrowReward.address, infiniteTokens, { from: userOne }); - await sov.approve(escrowReward.address, infiniteTokens, { from: userTwo }); - await sov.approve(escrowReward.address, infiniteTokens, { from: userThree }); - await sov.approve(escrowReward.address, infiniteTokens, { from: userFour }); - await sov.approve(escrowReward.address, infiniteTokens, { from: userFive }); - - await sov.mint(multisig, infiniteTokens); - await sov.approve(escrowReward.address, infiniteTokens, { from: multisig }); - } - - before("Initiating Accounts & Creating Test Token Instance.", async () => { - await loadFixture(deploymentAndInitFixture); - }); - - it("Creating an instance should set all the values correctly.", async () => { - let timestamp = currentTimestamp() + 1000; - newEscrowReward = await EscrowReward.new(lockedSOV.address, sov.address, multisig, timestamp, depositLimit, { - from: creator, - }); - await checkStatus( - newEscrowReward, - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - creator, - zero, - timestamp, - depositLimit, - zero, - sov.address, - lockedSOV.address, - multisig, - zero, - zero, - deployedStatus - ); - await newEscrowReward.init({ from: multisig }); - }); - - it("Calling the init() should update the contract status to Deposit.", async () => { - await checkStatus( - escrowReward, - [0, 0, 0, 0, 0, 0, 0, 0, 0, 1], - zeroAddress, - zero, - zero, - zero, - zero, - zeroAddress, - zeroAddress, - zeroAddress, - zero, - zero, - depositStatus - ); - }); - - it("Updating the Multisig should update the multisig in Contract.", async () => { - await newEscrowReward.updateMultisig(newMultisig, { from: multisig }); - await checkStatus( - newEscrowReward, - [0, 0, 0, 0, 0, 0, 1, 0, 0, 0], - zeroAddress, - zero, - zero, - zero, - zero, - zeroAddress, - zeroAddress, - newMultisig, - zero, - zero, - zero - ); - }); - - it("Updating the release time should update the release timestamp in contract.", async () => { - let timestamp = currentTimestamp(); - await escrowReward.updateReleaseTimestamp(timestamp, { from: multisig }); - await checkStatus( - escrowReward, - [0, 1, 0, 0, 0, 0, 0, 0, 0, 0], - zeroAddress, - zero, - timestamp, - zero, - zero, - zeroAddress, - zeroAddress, - zeroAddress, - zero, - zero, - zero - ); - }); - - it("Updating the deposit limit should update the deposit limit in contract.", async () => { - let value = randomValue() + 1; - await newEscrowReward.updateDepositLimit(value, { from: newMultisig }); - await checkStatus( - newEscrowReward, - [0, 0, 1, 0, 0, 0, 0, 0, 0, 0], - zeroAddress, - zero, - zero, - value, - zero, - zeroAddress, - zeroAddress, - zeroAddress, - zero, - zero, - zero - ); - }); - - it("Depositing Tokens by Users should update the user balance.", async () => { - let value = randomValue() + 1; - await newEscrowReward.updateDepositLimit(value, { from: newMultisig }); - await sov.mint(userOne, value); - await sov.approve(newEscrowReward.address, value, { from: userOne }); - await newEscrowReward.depositTokens(value, { from: userOne }); - await checkStatus( - newEscrowReward, - [0, 0, 0, 0, 0, 0, 0, 1, 0, 0], - userOne, - zero, - zero, - zero, - zero, - zeroAddress, - zeroAddress, - zeroAddress, - value, - zero, - zero - ); - }); - - it("Trying to deposit Tokens higher than the deposit limit should only take till the deposit limit.", async () => { - let limit = randomValue() + 1; - let value = randomValue() + limit; - await escrowReward.updateDepositLimit(limit, { from: multisig }); - - await sov.mint(userOne, value); - await sov.approve(escrowReward.address, value, { from: userOne }); - - let [beforeUserTokenBalance] = await getTokenBalances(userOne, sov, lockedSOV); - await escrowReward.depositTokens(value, { from: userOne }); - let [afterUserTokenBalance] = await getTokenBalances(userOne, sov, lockedSOV); - - assert(beforeUserTokenBalance.eq(afterUserTokenBalance.add(new BN(limit))), "The user SOV balance is not right."); - await checkStatus( - escrowReward, - [1, 0, 0, 0, 0, 0, 0, 1, 0, 0], - userOne, - limit, - zero, - zero, - zero, - zeroAddress, - zeroAddress, - zeroAddress, - limit, - zero, - zero - ); - }); - - it("Trying to deposit Tokens after the deposit limit has reached should refund entire amount.", async () => { - let value = randomValue() + 1; - - await sov.mint(userTwo, value); - await sov.approve(escrowReward.address, value, { from: userTwo }); - let [beforeUserTokenBalance] = await getTokenBalances(userTwo, sov, lockedSOV); - await escrowReward.depositTokens(value, { from: userTwo }); - let [afterUserTokenBalance] = await getTokenBalances(userTwo, sov, lockedSOV); - - assert(beforeUserTokenBalance.eq(afterUserTokenBalance), "The userTwo SOV balance is not right."); - }); - - it("Changing the contract to Holding State should update the contract state.", async () => { - await escrowReward.changeStateToHolding({ from: multisig }); - await checkStatus( - escrowReward, - [0, 0, 0, 0, 0, 0, 0, 0, 0, 1], - zeroAddress, - zero, - zero, - zero, - zero, - zeroAddress, - zeroAddress, - zeroAddress, - zero, - zero, - holdingStatus - ); - }); - - it("Multisig token withdraw should update the receiver and escrow token balance.", async () => { - let totalValue = await sov.balanceOf(escrowReward.address); - - await escrowReward.withdrawTokensByMultisig(safeVault, { from: multisig }); - let [contractBalance] = await getTokenBalances(escrowReward.address, sov, lockedSOV); - assert.equal(contractBalance, zero, "Contract SOV Token balance should be zero."); - let [safeVaultBalance] = await getTokenBalances(safeVault, sov, lockedSOV); - assert(safeVaultBalance.eq(totalValue), "SafeVault SOV Token balance is not correct."); - }); - - it("Multisig token deposit should change the contract state to Withdraw.", async () => { - let value = await sov.balanceOf(safeVault); - let totalValueOne = Math.ceil(value / 2); - let totalValueTwo = Math.floor(value / 2); - - await sov.mint(multisig, value); - await sov.approve(escrowReward.address, value, { from: multisig }); - await escrowReward.depositTokensByMultisig(totalValueOne, { from: multisig }); - - await checkStatus( - escrowReward, - [0, 0, 0, 0, 0, 0, 0, 0, 0, 1], - zeroAddress, - zero, - zero, - zero, - zero, - zeroAddress, - zeroAddress, - zeroAddress, - zero, - zero, - holdingStatus - ); - let [contractBalance] = await getTokenBalances(escrowReward.address, sov, lockedSOV); - assert.equal(contractBalance, totalValueOne, "Contract SOV Token balance is not correct."); - - await escrowReward.depositTokensByMultisig(totalValueTwo, { from: multisig }); - - await checkStatus( - escrowReward, - [0, 0, 0, 0, 0, 0, 0, 0, 0, 1], - zeroAddress, - zero, - zero, - zero, - zero, - zeroAddress, - zeroAddress, - zeroAddress, - zero, - zero, - withdrawStatus - ); - [contractBalance] = await getTokenBalances(escrowReward.address, sov, lockedSOV); - assert(contractBalance.eq(value), "Contract SOV Token balance is not correct."); - }); - - it("Updating the Reward Token Address should update the contract state.", async () => { - let newLockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [multisig]); - await escrowReward.updateLockedSOV(newLockedSOV.address, { from: multisig }); - checkStatus( - escrowReward, - [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], - zeroAddress, - zero, - zero, - zero, - zero, - zeroAddress, - newLockedSOV.address, - zeroAddress, - zero, - zero, - zero - ); - }); - - it("Multisig reward token deposit should update the contract state.", async () => { - let reward = randomValue() + 1; - await sov.mint(newMultisig, reward); - await sov.approve(newEscrowReward.address, reward, { from: newMultisig }); - await newEscrowReward.depositRewardByMultisig(reward, { from: newMultisig }); - await checkStatus( - newEscrowReward, - [0, 0, 0, 1, 0, 0, 0, 0, 0, 0], - zeroAddress, - zero, - zero, - zero, - reward, - zeroAddress, - zeroAddress, - zeroAddress, - zero, - zero, - zero - ); - }); - - it("SOV and Reward (0.001%) withdraw should update the contract state.", async () => { - let percentage = 0.001; - await loadFixture(deploymentAndInitFixture); - await sovAndRewardWithdraw(sov, lockedSOV, escrowReward, multisig, userOne, userTwo, userThree, userFour, userFive, percentage); - }); - - it("SOV and Reward (0.01%) withdraw should update the contract state.", async () => { - let percentage = 0.01; - await loadFixture(deploymentAndInitFixture); - await sovAndRewardWithdraw(sov, lockedSOV, escrowReward, multisig, userOne, userTwo, userThree, userFour, userFive, percentage); - }); - - it("SOV and Reward (0.1%) withdraw should update the contract state.", async () => { - let percentage = 0.1; - await loadFixture(deploymentAndInitFixture); - await sovAndRewardWithdraw(sov, lockedSOV, escrowReward, multisig, userOne, userTwo, userThree, userFour, userFive, percentage); - }); - - it("SOV and Reward (1%) withdraw should update the contract state.", async () => { - let percentage = 1; - await loadFixture(deploymentAndInitFixture); - await sovAndRewardWithdraw(sov, lockedSOV, escrowReward, multisig, userOne, userTwo, userThree, userFour, userFive, percentage); - }); - - it("SOV and Reward (10%) withdraw should update the contract state.", async () => { - let percentage = 10; - await loadFixture(deploymentAndInitFixture); - await sovAndRewardWithdraw(sov, lockedSOV, escrowReward, multisig, userOne, userTwo, userThree, userFour, userFive, percentage); - }); - - it("SOV and Reward (50%) withdraw should update the contract state.", async () => { - let percentage = 50; - await loadFixture(deploymentAndInitFixture); - await sovAndRewardWithdraw(sov, lockedSOV, escrowReward, multisig, userOne, userTwo, userThree, userFour, userFive, percentage); - }); - - it("SOV and Reward (100%) withdraw should update the contract state.", async () => { - let percentage = 100; - await loadFixture(deploymentAndInitFixture); - await sovAndRewardWithdraw(sov, lockedSOV, escrowReward, multisig, userOne, userTwo, userThree, userFour, userFive, percentage); - }); - - it("SOV and Reward (200%) withdraw should update the contract state.", async () => { - let percentage = 200; - await loadFixture(deploymentAndInitFixture); - await sovAndRewardWithdraw(sov, lockedSOV, escrowReward, multisig, userOne, userTwo, userThree, userFour, userFive, percentage); - }); + let escrowReward, newEscrowReward, sov, lockedSOV; + let creator, multisig, newMultisig, safeVault, userOne, userTwo, userThree, userFour, userFive; + + async function deploymentAndInitFixture(_wallets, _provider) { + // Checking if we have enough accounts to test. + assert.isAtLeast( + accounts.length, + 9, + "At least 9 accounts are required to test the contracts." + ); + [ + creator, + multisig, + newMultisig, + safeVault, + userOne, + userTwo, + userThree, + userFour, + userFive, + ] = accounts; + + // Creating the instance of SOV Token. + sov = await SOV.new("Sovryn", "SOV", 18, zero); + + // Creating the Staking Instance. + stakingLogic = await StakingLogic.new(sov.address); + staking = await StakingProxy.new(sov.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); + + // Creating the FeeSharing Instance. + feeSharingProxy = await FeeSharingProxy.new(constants.ZERO_ADDRESS, staking.address); + + // Creating the Vesting Instance. + vestingLogic = await VestingLogic.new(); + vestingFactory = await VestingFactory.new(vestingLogic.address); + vestingRegistry = await VestingRegistry.new( + vestingFactory.address, + sov.address, + staking.address, + feeSharingProxy.address, + creator // This should be Governance Timelock Contract. + ); + vestingFactory.transferOwnership(vestingRegistry.address); + + // Creating the instance of newLockedSOV Contract. + lockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [ + multisig, + ]); + + // Creating the contract instance. + escrowReward = await createEscrowReward(lockedSOV, sov, multisig, zero, depositLimit); + + /// @dev Minting, approval calculation moved here for optimization + await sov.mint(userOne, infiniteTokens); + await sov.mint(userTwo, infiniteTokens); + await sov.mint(userThree, infiniteTokens); + await sov.mint(userFour, infiniteTokens); + await sov.mint(userFive, infiniteTokens); + await sov.approve(escrowReward.address, infiniteTokens, { from: userOne }); + await sov.approve(escrowReward.address, infiniteTokens, { from: userTwo }); + await sov.approve(escrowReward.address, infiniteTokens, { from: userThree }); + await sov.approve(escrowReward.address, infiniteTokens, { from: userFour }); + await sov.approve(escrowReward.address, infiniteTokens, { from: userFive }); + + await sov.mint(multisig, infiniteTokens); + await sov.approve(escrowReward.address, infiniteTokens, { from: multisig }); + } + + before("Initiating Accounts & Creating Test Token Instance.", async () => { + await loadFixture(deploymentAndInitFixture); + }); + + it("Creating an instance should set all the values correctly.", async () => { + let timestamp = currentTimestamp() + 1000; + newEscrowReward = await EscrowReward.new( + lockedSOV.address, + sov.address, + multisig, + timestamp, + depositLimit, + { + from: creator, + } + ); + await checkStatus( + newEscrowReward, + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + creator, + zero, + timestamp, + depositLimit, + zero, + sov.address, + lockedSOV.address, + multisig, + zero, + zero, + deployedStatus + ); + await newEscrowReward.init({ from: multisig }); + }); + + it("Calling the init() should update the contract status to Deposit.", async () => { + await checkStatus( + escrowReward, + [0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + zeroAddress, + zero, + zero, + zero, + zero, + zeroAddress, + zeroAddress, + zeroAddress, + zero, + zero, + depositStatus + ); + }); + + it("Updating the Multisig should update the multisig in Contract.", async () => { + await newEscrowReward.updateMultisig(newMultisig, { from: multisig }); + await checkStatus( + newEscrowReward, + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + zeroAddress, + zero, + zero, + zero, + zero, + zeroAddress, + zeroAddress, + newMultisig, + zero, + zero, + zero + ); + }); + + it("Updating the release time should update the release timestamp in contract.", async () => { + let timestamp = currentTimestamp(); + await escrowReward.updateReleaseTimestamp(timestamp, { from: multisig }); + await checkStatus( + escrowReward, + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0], + zeroAddress, + zero, + timestamp, + zero, + zero, + zeroAddress, + zeroAddress, + zeroAddress, + zero, + zero, + zero + ); + }); + + it("Updating the deposit limit should update the deposit limit in contract.", async () => { + let value = randomValue() + 1; + await newEscrowReward.updateDepositLimit(value, { from: newMultisig }); + await checkStatus( + newEscrowReward, + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0], + zeroAddress, + zero, + zero, + value, + zero, + zeroAddress, + zeroAddress, + zeroAddress, + zero, + zero, + zero + ); + }); + + it("Depositing Tokens by Users should update the user balance.", async () => { + let value = randomValue() + 1; + await newEscrowReward.updateDepositLimit(value, { from: newMultisig }); + await sov.mint(userOne, value); + await sov.approve(newEscrowReward.address, value, { from: userOne }); + await newEscrowReward.depositTokens(value, { from: userOne }); + await checkStatus( + newEscrowReward, + [0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + userOne, + zero, + zero, + zero, + zero, + zeroAddress, + zeroAddress, + zeroAddress, + value, + zero, + zero + ); + }); + + it("Trying to deposit Tokens higher than the deposit limit should only take till the deposit limit.", async () => { + let limit = randomValue() + 1; + let value = randomValue() + limit; + await escrowReward.updateDepositLimit(limit, { from: multisig }); + + await sov.mint(userOne, value); + await sov.approve(escrowReward.address, value, { from: userOne }); + + let [beforeUserTokenBalance] = await getTokenBalances(userOne, sov, lockedSOV); + await escrowReward.depositTokens(value, { from: userOne }); + let [afterUserTokenBalance] = await getTokenBalances(userOne, sov, lockedSOV); + + assert( + beforeUserTokenBalance.eq(afterUserTokenBalance.add(new BN(limit))), + "The user SOV balance is not right." + ); + await checkStatus( + escrowReward, + [1, 0, 0, 0, 0, 0, 0, 1, 0, 0], + userOne, + limit, + zero, + zero, + zero, + zeroAddress, + zeroAddress, + zeroAddress, + limit, + zero, + zero + ); + }); + + it("Trying to deposit Tokens after the deposit limit has reached should refund entire amount.", async () => { + let value = randomValue() + 1; + + await sov.mint(userTwo, value); + await sov.approve(escrowReward.address, value, { from: userTwo }); + let [beforeUserTokenBalance] = await getTokenBalances(userTwo, sov, lockedSOV); + await escrowReward.depositTokens(value, { from: userTwo }); + let [afterUserTokenBalance] = await getTokenBalances(userTwo, sov, lockedSOV); + + assert( + beforeUserTokenBalance.eq(afterUserTokenBalance), + "The userTwo SOV balance is not right." + ); + }); + + it("Changing the contract to Holding State should update the contract state.", async () => { + await escrowReward.changeStateToHolding({ from: multisig }); + await checkStatus( + escrowReward, + [0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + zeroAddress, + zero, + zero, + zero, + zero, + zeroAddress, + zeroAddress, + zeroAddress, + zero, + zero, + holdingStatus + ); + }); + + it("Multisig token withdraw should update the receiver and escrow token balance.", async () => { + let totalValue = await sov.balanceOf(escrowReward.address); + + await escrowReward.withdrawTokensByMultisig(safeVault, { from: multisig }); + let [contractBalance] = await getTokenBalances(escrowReward.address, sov, lockedSOV); + assert.equal(contractBalance, zero, "Contract SOV Token balance should be zero."); + let [safeVaultBalance] = await getTokenBalances(safeVault, sov, lockedSOV); + assert(safeVaultBalance.eq(totalValue), "SafeVault SOV Token balance is not correct."); + }); + + it("Multisig token deposit should change the contract state to Withdraw.", async () => { + let value = await sov.balanceOf(safeVault); + let totalValueOne = Math.ceil(value / 2); + let totalValueTwo = Math.floor(value / 2); + + await sov.mint(multisig, value); + await sov.approve(escrowReward.address, value, { from: multisig }); + await escrowReward.depositTokensByMultisig(totalValueOne, { from: multisig }); + + await checkStatus( + escrowReward, + [0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + zeroAddress, + zero, + zero, + zero, + zero, + zeroAddress, + zeroAddress, + zeroAddress, + zero, + zero, + holdingStatus + ); + let [contractBalance] = await getTokenBalances(escrowReward.address, sov, lockedSOV); + assert.equal(contractBalance, totalValueOne, "Contract SOV Token balance is not correct."); + + await escrowReward.depositTokensByMultisig(totalValueTwo, { from: multisig }); + + await checkStatus( + escrowReward, + [0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + zeroAddress, + zero, + zero, + zero, + zero, + zeroAddress, + zeroAddress, + zeroAddress, + zero, + zero, + withdrawStatus + ); + [contractBalance] = await getTokenBalances(escrowReward.address, sov, lockedSOV); + assert(contractBalance.eq(value), "Contract SOV Token balance is not correct."); + }); + + it("Updating the Reward Token Address should update the contract state.", async () => { + let newLockedSOV = await LockedSOV.new( + sov.address, + vestingRegistry.address, + cliff, + duration, + [multisig] + ); + await escrowReward.updateLockedSOV(newLockedSOV.address, { from: multisig }); + checkStatus( + escrowReward, + [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], + zeroAddress, + zero, + zero, + zero, + zero, + zeroAddress, + newLockedSOV.address, + zeroAddress, + zero, + zero, + zero + ); + }); + + it("Multisig reward token deposit should update the contract state.", async () => { + let reward = randomValue() + 1; + await sov.mint(newMultisig, reward); + await sov.approve(newEscrowReward.address, reward, { from: newMultisig }); + await newEscrowReward.depositRewardByMultisig(reward, { from: newMultisig }); + await checkStatus( + newEscrowReward, + [0, 0, 0, 1, 0, 0, 0, 0, 0, 0], + zeroAddress, + zero, + zero, + zero, + reward, + zeroAddress, + zeroAddress, + zeroAddress, + zero, + zero, + zero + ); + }); + + it("SOV and Reward (0.001%) withdraw should update the contract state.", async () => { + let percentage = 0.001; + await loadFixture(deploymentAndInitFixture); + await sovAndRewardWithdraw( + sov, + lockedSOV, + escrowReward, + multisig, + userOne, + userTwo, + userThree, + userFour, + userFive, + percentage + ); + }); + + it("SOV and Reward (0.01%) withdraw should update the contract state.", async () => { + let percentage = 0.01; + await loadFixture(deploymentAndInitFixture); + await sovAndRewardWithdraw( + sov, + lockedSOV, + escrowReward, + multisig, + userOne, + userTwo, + userThree, + userFour, + userFive, + percentage + ); + }); + + it("SOV and Reward (0.1%) withdraw should update the contract state.", async () => { + let percentage = 0.1; + await loadFixture(deploymentAndInitFixture); + await sovAndRewardWithdraw( + sov, + lockedSOV, + escrowReward, + multisig, + userOne, + userTwo, + userThree, + userFour, + userFive, + percentage + ); + }); + + it("SOV and Reward (1%) withdraw should update the contract state.", async () => { + let percentage = 1; + await loadFixture(deploymentAndInitFixture); + await sovAndRewardWithdraw( + sov, + lockedSOV, + escrowReward, + multisig, + userOne, + userTwo, + userThree, + userFour, + userFive, + percentage + ); + }); + + it("SOV and Reward (10%) withdraw should update the contract state.", async () => { + let percentage = 10; + await loadFixture(deploymentAndInitFixture); + await sovAndRewardWithdraw( + sov, + lockedSOV, + escrowReward, + multisig, + userOne, + userTwo, + userThree, + userFour, + userFive, + percentage + ); + }); + + it("SOV and Reward (50%) withdraw should update the contract state.", async () => { + let percentage = 50; + await loadFixture(deploymentAndInitFixture); + await sovAndRewardWithdraw( + sov, + lockedSOV, + escrowReward, + multisig, + userOne, + userTwo, + userThree, + userFour, + userFive, + percentage + ); + }); + + it("SOV and Reward (100%) withdraw should update the contract state.", async () => { + let percentage = 100; + await loadFixture(deploymentAndInitFixture); + await sovAndRewardWithdraw( + sov, + lockedSOV, + escrowReward, + multisig, + userOne, + userTwo, + userThree, + userFour, + userFive, + percentage + ); + }); + + it("SOV and Reward (200%) withdraw should update the contract state.", async () => { + let percentage = 200; + await loadFixture(deploymentAndInitFixture); + await sovAndRewardWithdraw( + sov, + lockedSOV, + escrowReward, + multisig, + userOne, + userTwo, + userThree, + userFour, + userFive, + percentage + ); + }); }); diff --git a/tests/FeeSharingProxyTest.js b/tests/FeeSharingProxyTest.js index c97767d13..8a8d4d1f2 100644 --- a/tests/FeeSharingProxyTest.js +++ b/tests/FeeSharingProxyTest.js @@ -89,1629 +89,2061 @@ let cliff = 1; // This is in 4 weeks. i.e. 1 * 4 weeks. let duration = 11; // This is in 4 weeks. i.e. 11 * 4 weeks. const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getLoanTokenLogic, - getLoanToken, - getLoanTokenLogicWrbtc, - getLoanTokenWRBTC, - loan_pool_setup, - set_demand_curve, - getPriceFeeds, - getSovryn, - decodeLogs, - getSOV, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getLoanTokenLogic, + getLoanToken, + getLoanTokenLogicWrbtc, + getLoanTokenWRBTC, + loan_pool_setup, + set_demand_curve, + getPriceFeeds, + getSovryn, + decodeLogs, + getSOV, } = require("./Utils/initializer.js"); contract("FeeSharingProxy:", (accounts) => { - const name = "Test SOVToken"; - const symbol = "TST"; - - let root, account1, account2, account3, account4; - let SOVToken, SUSD, WRBTC, sovryn, staking; - let loanTokenSettings, loanTokenLogic, loanToken; - let feeSharingProxyObj; - let feeSharingProxy; - let feeSharingLogic; - let loanTokenWrbtc; - let tradingFeePercent; - let mockPrice; - let liquidityPoolV1Converter; - - before(async () => { - [root, account1, account2, account3, account4, ...accounts] = accounts; - }); - - async function protocolDeploymentFixture(_wallets, _provider) { - // Token - SOVToken = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); - - // Staking - let stakingLogic = await StakingLogic.new(SOVToken.address); - staking = await StakingProxy.new(SOVToken.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingLogic.at(staking.address); - - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - - // Deploying sovrynProtocol w/ generic function from initializer.js - /// @dev Tried but no success so far. When using the getSovryn function - /// , contracts revert w/ "target not active" error. - /// The weird thing is that deployment code below is exactly the same as - /// the code from getSovryn function at initializer.js. - /// Inline code works ok, but when calling the function it does not. - // sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - // await sovryn.setSovrynProtocolAddress(sovryn.address); - - const sovrynproxy = await Protocol.new(); - sovryn = await ISovryn.at(sovrynproxy.address); - - await sovryn.replaceContract((await ProtocolSettings.new()).address); - await sovryn.replaceContract((await LoanSettings.new()).address); - await sovryn.replaceContract((await LoanMaintenance.new()).address); - await sovryn.replaceContract((await SwapsExternal.new()).address); - - await sovryn.setWrbtcToken(WRBTC.address); - - await sovryn.replaceContract((await LoanClosingsWith.new()).address); - await sovryn.replaceContract((await LoanClosingsLiquidation.new()).address); - await sovryn.replaceContract((await LoanClosingsRollover.new()).address); - - await sovryn.replaceContract((await Affiliates.new()).address); - - sovryn = await ProtocolSettings.at(sovryn.address); - - // Loan token - const initLoanTokenLogic = await getLoanTokenLogic(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] - loanTokenLogic = initLoanTokenLogic[0]; - loanTokenLogicBeacon = initLoanTokenLogic[1]; - - loanToken = await LoanToken.new(root, loanTokenLogic.address, sovryn.address, WRBTC.address); - await loanToken.initialize(SUSD.address, "iSUSD", "iSUSD"); - - /** Initialize the loan token logic proxy */ - loanToken = await ILoanTokenLogicProxy.at(loanToken.address); - await loanToken.setBeaconAddress(loanTokenLogicBeacon.address); - - /** Use interface of LoanTokenModules */ - loanToken = await ILoanTokenModules.at(loanToken.address); - - await loanToken.setAdmin(root); - await sovryn.setLoanPool([loanToken.address], [SUSD.address]); - - // FeeSharingProxy - feeSharingLogic = await FeeSharingLogic.new(); - feeSharingProxyObj = await FeeSharingProxy.new(sovryn.address, staking.address); - await feeSharingProxyObj.setImplementation(feeSharingLogic.address); - feeSharingProxy = await FeeSharingLogic.at(feeSharingProxyObj.address); - await sovryn.setFeesController(feeSharingProxy.address); - - // Set loan pool for wRBTC -- because our fee sharing proxy required the loanPool of wRBTC - loanTokenLogicWrbtc = await LoanTokenLogicWrbtc.new(); - loanTokenWrbtc = await LoanToken.new(root, loanTokenLogicWrbtc.address, sovryn.address, WRBTC.address); - await loanTokenWrbtc.initialize(WRBTC.address, "iWRBTC", "iWRBTC"); - - loanTokenWrbtc = await LoanTokenLogicWrbtc.at(loanTokenWrbtc.address); - const loanTokenAddressWrbtc = await loanTokenWrbtc.loanTokenAddress(); - await sovryn.setLoanPool([loanTokenWrbtc.address], [loanTokenAddressWrbtc]); - - await WRBTC.mint(sovryn.address, wei("500", "ether")); - - await sovryn.setWrbtcToken(WRBTC.address); - await sovryn.setSOVTokenAddress(SOVToken.address); - await sovryn.setSovrynProtocolAddress(sovryn.address); - - // Creating the Vesting Instance. - vestingLogic = await VestingLogic.new(); - vestingFactory = await VestingFactory.new(vestingLogic.address); - vestingRegistry = await VestingRegistry.new( - vestingFactory.address, - SOVToken.address, - staking.address, - feeSharingProxy.address, - root // This should be Governance Timelock Contract. - ); - vestingFactory.transferOwnership(vestingRegistry.address); - - await sovryn.setLockedSOVAddress((await LockedSOV.new(SOVToken.address, vestingRegistry.address, cliff, duration, [root])).address); - - // Set PriceFeeds - feeds = await PriceFeedsLocal.new(WRBTC.address, sovryn.address); - mockPrice = "1"; - await feeds.setRates(SUSD.address, WRBTC.address, wei(mockPrice, "ether")); - const swaps = await SwapsImplSovrynSwap.new(); - const sovrynSwapSimulator = await TestSovrynSwap.new(feeds.address); - await sovryn.setSovrynSwapContractRegistryAddress(sovrynSwapSimulator.address); - await sovryn.setSupportedTokens([SUSD.address, WRBTC.address], [true, true]); - await sovryn.setPriceFeedContract( - feeds.address // priceFeeds - ); - await sovryn.setSwapsImplContract( - swaps.address // swapsImpl - ); - - tradingFeePercent = await sovryn.tradingFeePercent(); - await lend_btc_before_cashout(loanTokenWrbtc, new BN(wei("10", "ether")), root); - - const maxDisagreement = new BN(wei("5", "ether")); - await sovryn.setMaxDisagreement(maxDisagreement); - - return sovryn; - } - - beforeEach(async () => { - await loadFixture(protocolDeploymentFixture); - }); - - describe("FeeSharingProxy", () => { - it("Check owner & implementation", async () => { - const proxyOwner = await feeSharingProxyObj.getProxyOwner(); - const implementation = await feeSharingProxyObj.getImplementation(); - - expect(implementation).to.be.equal(feeSharingLogic.address); - expect(proxyOwner).to.be.equal(root); - }); - - it("Set new implementation", async () => { - const newFeeSharingLogic = await FeeSharingLogic.new(); - await feeSharingProxyObj.setImplementation(newFeeSharingLogic.address); - const newImplementation = await feeSharingProxyObj.getImplementation(); - - expect(newImplementation).to.be.equal(newFeeSharingLogic.address); - }); - }); - - describe("withdrawFees", () => { - it("Shouldn't be able to use zero token address", async () => { - await protocolDeploymentFixture(); - await expectRevert(feeSharingProxy.withdrawFees([ZERO_ADDRESS]), "FeeSharingProxy::withdrawFees: token is not a contract"); - }); - - it("Shouldn't be able to withdraw if wRBTC loan pool does not exist", async () => { - await protocolDeploymentFixture(); - // Unset the loanPool for wRBTC - await sovryn.setLoanPool([loanTokenWrbtc.address], [ZERO_ADDRESS]); - - //mock data - let lendingFeeTokensHeld = new BN(wei("1", "ether")); - let tradingFeeTokensHeld = new BN(wei("2", "ether")); - let borrowingFeeTokensHeld = new BN(wei("3", "ether")); - let totalFeeTokensHeld = lendingFeeTokensHeld.add(tradingFeeTokensHeld).add(borrowingFeeTokensHeld); - let feeAmount = await setFeeTokensHeld(lendingFeeTokensHeld, tradingFeeTokensHeld, borrowingFeeTokensHeld, true); - - await expectRevert(feeSharingProxy.withdrawFees([WRBTC.address]), "FeeSharingProxy::withdrawFees: loan wRBTC not found"); - }); - - it("Shouldn't be able to withdraw zero amount", async () => { - await protocolDeploymentFixture(); - const tx = await feeSharingProxy.withdrawFees([SUSD.address]); - expectEvent(tx, "FeeWithdrawn", { - sender: root, - token: loanTokenWrbtc.address, - amount: new BN(0), - }); - }); - - it("ProtocolSettings.withdrawFees", async () => { - /// @dev This test requires redeploying the protocol - const protocol = await protocolDeploymentFixture(); - - // stake - getPriorTotalVotingPower - let totalStake = 1000; - await stake(totalStake, root); - - // mock data - let lendingFeeTokensHeld = new BN(wei("1", "ether")); - let tradingFeeTokensHeld = new BN(wei("2", "ether")); - let borrowingFeeTokensHeld = new BN(wei("3", "ether")); - let totalFeeTokensHeld = lendingFeeTokensHeld.add(tradingFeeTokensHeld).add(borrowingFeeTokensHeld); - - let feeAmount = await setFeeTokensHeld(lendingFeeTokensHeld, tradingFeeTokensHeld, borrowingFeeTokensHeld); - let previousProtocolWrbtcBalance = await WRBTC.balanceOf(protocol.address); - // let feeAmount = await setFeeTokensHeld(new BN(100), new BN(200), new BN(300)); - await protocol.setFeesController(root); - let tx = await protocol.withdrawFees([SUSD.address], root); - let latestProtocolWrbtcBalance = await WRBTC.balanceOf(protocol.address); - - await checkWithdrawFee(); - - //check wrbtc balance (wrbt balance = (totalFeeTokensHeld * mockPrice) - swapFee) - let userBalance = await WRBTC.balanceOf.call(root); - expect(userBalance.toString()).to.be.equal(feeAmount.toString()); - - // wrbtc balance should remain the same - expect(previousProtocolWrbtcBalance.toString()).to.equal(latestProtocolWrbtcBalance.toString()); - - expectEvent(tx, "WithdrawFees", { - sender: root, - token: SUSD.address, - receiver: root, - lendingAmount: lendingFeeTokensHeld, - tradingAmount: tradingFeeTokensHeld, - borrowingAmount: borrowingFeeTokensHeld, - // amountConvertedToWRBTC - }); - }); - - it("ProtocolSettings.withdrawFees (WRBTC token)", async () => { - /// @dev This test requires redeploying the protocol - await protocolDeploymentFixture(); - - //stake - getPriorTotalVotingPower - let totalStake = 1000; - await stake(totalStake, root); - - //mock data - let lendingFeeTokensHeld = new BN(wei("1", "ether")); - let tradingFeeTokensHeld = new BN(wei("2", "ether")); - let borrowingFeeTokensHeld = new BN(wei("3", "ether")); - let totalFeeTokensHeld = lendingFeeTokensHeld.add(tradingFeeTokensHeld).add(borrowingFeeTokensHeld); - - let feeAmount = await setFeeTokensHeld(lendingFeeTokensHeld, tradingFeeTokensHeld, borrowingFeeTokensHeld, true); - // let feeAmount = await setFeeTokensHeld(new BN(100), new BN(200), new BN(300)); - await sovryn.setFeesController(root); - let tx = await sovryn.withdrawFees([WRBTC.address], account1); - - await checkWithdrawFee(true, true, false); - - //check WRBTC balance (wrbt balance = (totalFeeTokensHeld * mockPrice) - swapFee) - let userBalance = await WRBTC.balanceOf.call(account1); - expect(userBalance.toString()).to.be.equal(feeAmount.toString()); - - expectEvent(tx, "WithdrawFees", { - sender: root, - token: WRBTC.address, - receiver: account1, - lendingAmount: lendingFeeTokensHeld, - tradingAmount: tradingFeeTokensHeld, - borrowingAmount: borrowingFeeTokensHeld, - wRBTCConverted: new BN(feeAmount), - }); - }); - - /// @dev Test coverage - it("ProtocolSettings.withdrawFees: Revert withdrawing by no feesController", async () => { - /// @dev This test requires redeploying the protocol - await protocolDeploymentFixture(); - - // stake - getPriorTotalVotingPower - let totalStake = 1000; - await stake(totalStake, root); - - // mock data - let feeAmount = await setFeeTokensHeld(new BN(100), new BN(200), new BN(300)); - - await sovryn.setFeesController(root); - - await expectRevert(sovryn.withdrawFees([SUSD.address], account1, { from: account1 }), "unauthorized"); - }); - - it("Should be able to withdraw fees", async () => { - /// @dev This test requires redeploying the protocol - const protocol = await protocolDeploymentFixture(); - - // stake - getPriorTotalVotingPower - let totalStake = 1000; - await stake(totalStake, root); - - // mock data - let lendingFeeTokensHeld = new BN(wei("1", "ether")); - let tradingFeeTokensHeld = new BN(wei("2", "ether")); - let borrowingFeeTokensHeld = new BN(wei("3", "ether")); - let totalFeeTokensHeld = lendingFeeTokensHeld.add(tradingFeeTokensHeld).add(borrowingFeeTokensHeld); - let feeAmount = await setFeeTokensHeld(lendingFeeTokensHeld, tradingFeeTokensHeld, borrowingFeeTokensHeld); - let previousProtocolWrbtcBalance = await WRBTC.balanceOf(protocol.address); - - tx = await feeSharingProxy.withdrawFees([SUSD.address]); - - await checkWithdrawFee(); - - //check irbtc balance (wrbt balance = (totalFeeTokensHeld * mockPrice) - swapFee) - let feeSharingProxyBalance = await loanTokenWrbtc.balanceOf.call(feeSharingProxy.address); - expect(feeSharingProxyBalance.toString()).to.be.equal(feeAmount.toString()); - - // make sure wrbtc balance is 0 after withdrawal - let feeSharingProxyWRBTCBalance = await WRBTC.balanceOf.call(feeSharingProxy.address); - expect(feeSharingProxyWRBTCBalance.toString()).to.be.equal(new BN(0).toString()); - - // wrbtc balance should remain the same - let latestProtocolWrbtcBalance = await WRBTC.balanceOf(protocol.address); - expect(previousProtocolWrbtcBalance.toString()).to.equal(latestProtocolWrbtcBalance.toString()); - - //checkpoints - let numTokenCheckpoints = await feeSharingProxy.numTokenCheckpoints.call(loanTokenWrbtc.address); - expect(numTokenCheckpoints.toNumber()).to.be.equal(1); - let checkpoint = await feeSharingProxy.tokenCheckpoints.call(loanTokenWrbtc.address, 0); - expect(checkpoint.blockNumber.toNumber()).to.be.equal(tx.receipt.blockNumber); - expect(checkpoint.totalWeightedStake.toNumber()).to.be.equal(totalStake * MAX_VOTING_WEIGHT); - expect(checkpoint.numTokens.toString()).to.be.equal(feeAmount.toString()); - - // check lastFeeWithdrawalTime - let lastFeeWithdrawalTime = await feeSharingProxy.lastFeeWithdrawalTime.call(loanTokenWrbtc.address); - let block = await web3.eth.getBlock(tx.receipt.blockNumber); - expect(lastFeeWithdrawalTime.toString()).to.be.equal(block.timestamp.toString()); - - expectEvent(tx, "FeeWithdrawn", { - sender: root, - token: loanTokenWrbtc.address, - amount: feeAmount, - }); - }); - - it("Should be able to withdraw fees (WRBTC token)", async () => { - /// @dev This test requires redeploying the protocol - await protocolDeploymentFixture(); - - //stake - getPriorTotalVotingPower - let totalStake = 1000; - await stake(totalStake, root); - - //mock data - let lendingFeeTokensHeld = new BN(wei("1", "ether")); - let tradingFeeTokensHeld = new BN(wei("2", "ether")); - let borrowingFeeTokensHeld = new BN(wei("3", "ether")); - let totalFeeTokensHeld = lendingFeeTokensHeld.add(tradingFeeTokensHeld).add(borrowingFeeTokensHeld); - let feeAmount = await setFeeTokensHeld(lendingFeeTokensHeld, tradingFeeTokensHeld, borrowingFeeTokensHeld, true); - - tx = await feeSharingProxy.withdrawFees([WRBTC.address]); - - await checkWithdrawFee(); - - //check irbtc balance (wrbt balance = (totalFeeTokensHeld * mockPrice) - swapFee) - let feeSharingProxyBalance = await loanTokenWrbtc.balanceOf.call(feeSharingProxy.address); - expect(feeSharingProxyBalance.toString()).to.be.equal(feeAmount.toString()); - - // make sure wrbtc balance is 0 after withdrawal - let feeSharingProxyWRBTCBalance = await WRBTC.balanceOf.call(feeSharingProxy.address); - expect(feeSharingProxyWRBTCBalance.toString()).to.be.equal(new BN(0).toString()); - - //checkpoints - let numTokenCheckpoints = await feeSharingProxy.numTokenCheckpoints.call(loanTokenWrbtc.address); - expect(numTokenCheckpoints.toNumber()).to.be.equal(1); - let checkpoint = await feeSharingProxy.tokenCheckpoints.call(loanTokenWrbtc.address, 0); - expect(checkpoint.blockNumber.toNumber()).to.be.equal(tx.receipt.blockNumber); - expect(checkpoint.totalWeightedStake.toNumber()).to.be.equal(totalStake * MAX_VOTING_WEIGHT); - expect(checkpoint.numTokens.toString()).to.be.equal(feeAmount.toString()); - - //check lastFeeWithdrawalTime - let lastFeeWithdrawalTime = await feeSharingProxy.lastFeeWithdrawalTime.call(loanTokenWrbtc.address); - let block = await web3.eth.getBlock(tx.receipt.blockNumber); - expect(lastFeeWithdrawalTime.toString()).to.be.equal(block.timestamp.toString()); - - expectEvent(tx, "FeeWithdrawn", { - sender: root, - token: loanTokenWrbtc.address, - amount: feeAmount, - }); - }); - - it("Should be able to withdraw fees (sov token)", async () => { - /// @dev This test requires redeploying the protocol - await protocolDeploymentFixture(); - - //stake - getPriorTotalVotingPower - let totalStake = 1000; - await stake(totalStake, root); - - //mock data - let lendingFeeTokensHeld = new BN(wei("1", "ether")); - let tradingFeeTokensHeld = new BN(wei("2", "ether")); - let borrowingFeeTokensHeld = new BN(wei("3", "ether")); - let totalFeeTokensHeld = lendingFeeTokensHeld.add(tradingFeeTokensHeld).add(borrowingFeeTokensHeld); - let feeAmount = await setFeeTokensHeld(lendingFeeTokensHeld, tradingFeeTokensHeld, borrowingFeeTokensHeld, false, true); - tx = await feeSharingProxy.withdrawFees([SOVToken.address]); - - await checkWithdrawFee(false, false, true); - - //check WRBTC balance (wrbt balance = (totalFeeTokensHeld * mockPrice) - swapFee) - let feeSharingProxyBalance = await SOVToken.balanceOf.call(feeSharingProxy.address); - expect(feeSharingProxyBalance.toString()).to.be.equal(feeAmount.toString()); - - // make sure wrbtc balance is 0 after withdrawal - let feeSharingProxyWRBTCBalance = await WRBTC.balanceOf.call(feeSharingProxy.address); - expect(feeSharingProxyWRBTCBalance.toString()).to.be.equal(new BN(0).toString()); - - //checkpoints - let numTokenCheckpoints = await feeSharingProxy.numTokenCheckpoints.call(SOVToken.address); - expect(numTokenCheckpoints.toNumber()).to.be.equal(1); - let checkpoint = await feeSharingProxy.tokenCheckpoints.call(SOVToken.address, 0); - expect(checkpoint.blockNumber.toNumber()).to.be.equal(tx.receipt.blockNumber); - expect(checkpoint.totalWeightedStake.toNumber()).to.be.equal(totalStake * MAX_VOTING_WEIGHT); - expect(checkpoint.numTokens.toString()).to.be.equal(feeAmount.toString()); - - //check lastFeeWithdrawalTime - let lastFeeWithdrawalTime = await feeSharingProxy.lastFeeWithdrawalTime.call(SOVToken.address); - let block = await web3.eth.getBlock(tx.receipt.blockNumber); - expect(lastFeeWithdrawalTime.toString()).to.be.equal(block.timestamp.toString()); - - expectEvent(tx, "TokensTransferred", { - sender: sovryn.address, - token: SOVToken.address, - amount: feeAmount, - }); - }); - - it("Should be able to withdraw fees 3 times", async () => { - /// @dev This test requires redeploying the protocol - await protocolDeploymentFixture(); - - // stake - getPriorTotalVotingPower - let totalStake = 1000; - await stake(1000, root); - - // [FIRST] - // mock data - let mockAmountLendingFeeTokensHeld = 0; - let mockAmountTradingFeeTokensHeld = 1; - let mockAmountBorrowingFeeTokensHeld = 2; - let totalMockAmount1 = mockAmountLendingFeeTokensHeld + mockAmountTradingFeeTokensHeld + mockAmountBorrowingFeeTokensHeld; - let lendingFeeTokensHeld = new BN(mockAmountLendingFeeTokensHeld); - let tradingFeeTokensHeld = new BN(wei(mockAmountTradingFeeTokensHeld.toString(), "ether")); - let borrowingFeeTokensHeld = new BN(wei(mockAmountBorrowingFeeTokensHeld.toString(), "ether")); - let totalFeeTokensHeld = lendingFeeTokensHeld.add(tradingFeeTokensHeld).add(borrowingFeeTokensHeld); - let feeAmount = await setFeeTokensHeld(lendingFeeTokensHeld, tradingFeeTokensHeld, borrowingFeeTokensHeld); - let totalFeeAmount = feeAmount; - - let tx = await feeSharingProxy.withdrawFees([SUSD.address]); - - await checkWithdrawFee(); - - // check WRBTC balance (wrbt balance = (totalFeeTokensHeld * mockPrice) - swapFee) - let feeSharingProxyBalance = await loanTokenWrbtc.balanceOf.call(feeSharingProxy.address); - expect(feeSharingProxyBalance.toString()).to.be.equal(feeAmount.toString()); - - // checkpoints - let numTokenCheckpoints = await feeSharingProxy.numTokenCheckpoints.call(loanTokenWrbtc.address); - expect(numTokenCheckpoints.toNumber()).to.be.equal(1); - let checkpoint = await feeSharingProxy.tokenCheckpoints.call(loanTokenWrbtc.address, 0); - expect(checkpoint.blockNumber.toNumber()).to.be.equal(tx.receipt.blockNumber); - expect(checkpoint.totalWeightedStake.toNumber()).to.be.equal(totalStake * MAX_VOTING_WEIGHT); - expect(checkpoint.numTokens.toString()).to.be.equal(feeAmount.toString()); - - // check lastFeeWithdrawalTime - let lastFeeWithdrawalTime = await feeSharingProxy.lastFeeWithdrawalTime.call(loanTokenWrbtc.address); - let block = await web3.eth.getBlock(tx.receipt.blockNumber); - expect(lastFeeWithdrawalTime.toString()).to.be.equal(block.timestamp.toString()); - - // [SECOND] - // mock data - let mockAmountLendingFeeTokensHeld2 = 1; - let mockAmountTradingFeeTokensHeld2 = 0; - let mockAmountBorrowingFeeTokensHeld2 = 0; - let totalMockAmount2 = mockAmountTradingFeeTokensHeld2 + mockAmountBorrowingFeeTokensHeld2 + mockAmountLendingFeeTokensHeld2; - lendingFeeTokensHeld = new BN(wei(mockAmountLendingFeeTokensHeld2.toString(), "ether")); - tradingFeeTokensHeld = new BN(mockAmountTradingFeeTokensHeld2); - borrowingFeeTokensHeld = new BN(mockAmountBorrowingFeeTokensHeld2); - totalFeeTokensHeld = lendingFeeTokensHeld.add(tradingFeeTokensHeld).add(borrowingFeeTokensHeld); - feeAmount = await setFeeTokensHeld(lendingFeeTokensHeld, tradingFeeTokensHeld, borrowingFeeTokensHeld); - let unprocessedAmount = feeAmount; - totalFeeAmount = totalFeeAmount.add(feeAmount); - - tx = await feeSharingProxy.withdrawFees([SUSD.address]); - - // Need to checkwithdrawfee manually - await checkWithdrawFee(); - - // check WRBTC balance (wrbt balance = (totalFeeTokensHeld * mockPrice) - swapFee) - feeSharingProxyBalance = await loanTokenWrbtc.balanceOf.call(feeSharingProxy.address); - expect(feeSharingProxyBalance.toString()).to.be.equal(totalFeeAmount.toString()); - - // [THIRD] - // mock data - let mockAmountLendingFeeTokensHeld3 = 0; - let mockAmountTradingFeeTokensHeld3 = 0.5; - let mockAmountBorrowingFeeTokensHeld3 = 0.5; - let totalMockAmount3 = mockAmountTradingFeeTokensHeld3 + mockAmountBorrowingFeeTokensHeld3 + mockAmountLendingFeeTokensHeld3; - lendingFeeTokensHeld = new BN(mockAmountLendingFeeTokensHeld3); - tradingFeeTokensHeld = new BN(wei(mockAmountTradingFeeTokensHeld3.toString(), "ether")); - borrowingFeeTokensHeld = new BN(wei(mockAmountBorrowingFeeTokensHeld3.toString(), "ether")); - totalFeeTokensHeld = lendingFeeTokensHeld.add(tradingFeeTokensHeld).add(borrowingFeeTokensHeld); - feeAmount = await setFeeTokensHeld(lendingFeeTokensHeld, tradingFeeTokensHeld, borrowingFeeTokensHeld); - totalFeeAmount = totalFeeAmount.add(feeAmount); - - await increaseTime(FEE_WITHDRAWAL_INTERVAL); - tx = await feeSharingProxy.withdrawFees([SUSD.address]); - // In this state the price of SUSD/WRBTC already adjusted because of previous swap, so we need to consider this in the next swapFee calculation - await checkWithdrawFee(); - - // check WRBTC balance (wrbt balance = (totalFeeTokensHeld * mockPrice) - swapFee) - feeSharingProxyBalance = await loanTokenWrbtc.balanceOf.call(feeSharingProxy.address); - expect(feeSharingProxyBalance.toString()).to.be.equal(totalFeeAmount.toString()); - - // checkpoints - numTokenCheckpoints = await feeSharingProxy.numTokenCheckpoints.call(loanTokenWrbtc.address); - expect(numTokenCheckpoints.toNumber()).to.be.equal(2); - checkpoint = await feeSharingProxy.tokenCheckpoints.call(loanTokenWrbtc.address, 1); - expect(checkpoint.blockNumber.toNumber()).to.be.equal(tx.receipt.blockNumber); - expect(checkpoint.totalWeightedStake.toNumber()).to.be.equal(totalStake * MAX_VOTING_WEIGHT); - expect(checkpoint.numTokens.toString()).to.be.equal(feeAmount.add(unprocessedAmount).toString()); - - // check lastFeeWithdrawalTime - lastFeeWithdrawalTime = await feeSharingProxy.lastFeeWithdrawalTime.call(loanTokenWrbtc.address); - block = await web3.eth.getBlock(tx.receipt.blockNumber); - expect(lastFeeWithdrawalTime.toString()).to.be.equal(block.timestamp.toString()); - - // make sure wrbtc balance is 0 after withdrawal - let feeSharingProxyWRBTCBalance = await WRBTC.balanceOf.call(feeSharingProxy.address); - expect(feeSharingProxyWRBTCBalance.toString()).to.be.equal(new BN(0).toString()); - }); - }); - - describe("transferTokens", () => { - it("Shouldn't be able to use zero token address", async () => { - await protocolDeploymentFixture(); - await expectRevert(feeSharingProxy.transferTokens(ZERO_ADDRESS, 1000), "FeeSharingProxy::transferTokens: invalid address"); - }); - - it("Shouldn't be able to transfer zero amount", async () => { - await protocolDeploymentFixture(); - await expectRevert(feeSharingProxy.transferTokens(SOVToken.address, 0), "FeeSharingProxy::transferTokens: invalid amount"); - }); - - it("Shouldn't be able to withdraw zero amount", async () => { - await protocolDeploymentFixture(); - await expectRevert(feeSharingProxy.transferTokens(SOVToken.address, 1000), "invalid transfer"); - }); - - it("Should be able to transfer tokens", async () => { - await protocolDeploymentFixture(); - // stake - getPriorTotalVotingPower - let totalStake = 1000; - await stake(totalStake, root); - - let amount = 1000; - await SOVToken.approve(feeSharingProxy.address, amount * 7); - - let tx = await feeSharingProxy.transferTokens(SOVToken.address, amount); - - expect(await feeSharingProxy.unprocessedAmount.call(SOVToken.address)).to.be.bignumber.equal(new BN(0)); - - expectEvent(tx, "TokensTransferred", { - sender: root, - token: SOVToken.address, - amount: new BN(amount), - }); - - // checkpoints - let numTokenCheckpoints = await feeSharingProxy.numTokenCheckpoints.call(SOVToken.address); - expect(numTokenCheckpoints.toNumber()).to.be.equal(1); - let checkpoint = await feeSharingProxy.tokenCheckpoints.call(SOVToken.address, 0); - expect(checkpoint.blockNumber.toNumber()).to.be.equal(tx.receipt.blockNumber); - expect(checkpoint.totalWeightedStake.toNumber()).to.be.equal(totalStake * MAX_VOTING_WEIGHT); - expect(checkpoint.numTokens.toString()).to.be.equal(amount.toString()); - - // check lastFeeWithdrawalTime - let lastFeeWithdrawalTime = await feeSharingProxy.lastFeeWithdrawalTime.call(SOVToken.address); - let block = await web3.eth.getBlock(tx.receipt.blockNumber); - expect(lastFeeWithdrawalTime.toString()).to.be.equal(block.timestamp.toString()); - - expectEvent(tx, "CheckpointAdded", { - sender: root, - token: SOVToken.address, - amount: new BN(amount), - }); - - // second time - tx = await feeSharingProxy.transferTokens(SOVToken.address, amount * 2); - - expect(await feeSharingProxy.unprocessedAmount.call(SOVToken.address)).to.be.bignumber.equal(new BN(amount * 2)); - - expectEvent(tx, "TokensTransferred", { - sender: root, - token: SOVToken.address, - amount: new BN(amount * 2), - }); - - await increaseTime(FEE_WITHDRAWAL_INTERVAL); - // third time - tx = await feeSharingProxy.transferTokens(SOVToken.address, amount * 4); - - expect(await feeSharingProxy.unprocessedAmount.call(SOVToken.address)).to.be.bignumber.equal(new BN(0)); - - // checkpoints - numTokenCheckpoints = await feeSharingProxy.numTokenCheckpoints.call(SOVToken.address); - expect(numTokenCheckpoints.toNumber()).to.be.equal(2); - checkpoint = await feeSharingProxy.tokenCheckpoints.call(SOVToken.address, 1); - expect(checkpoint.blockNumber.toNumber()).to.be.equal(tx.receipt.blockNumber); - expect(checkpoint.totalWeightedStake.toNumber()).to.be.equal(totalStake * MAX_VOTING_WEIGHT); - expect(checkpoint.numTokens.toNumber()).to.be.equal(amount * 6); - - // check lastFeeWithdrawalTime - lastFeeWithdrawalTime = await feeSharingProxy.lastFeeWithdrawalTime.call(SOVToken.address); - block = await web3.eth.getBlock(tx.receipt.blockNumber); - expect(lastFeeWithdrawalTime.toString()).to.be.equal(block.timestamp.toString()); - }); - }); - - describe("withdraw", () => { - it("Shouldn't be able to withdraw without checkpoints (for token pool)", async () => { - await protocolDeploymentFixture(); - await expectRevert( - feeSharingProxy.withdraw(loanToken.address, 0, account2, { from: account1 }), - "FeeSharingProxy::withdraw: _maxCheckpoints should be positive" - ); - }); - - it("Shouldn't be able to withdraw without checkpoints (for wRBTC pool)", async () => { - await protocolDeploymentFixture(); - await expectRevert( - feeSharingProxy.withdraw(loanTokenWrbtc.address, 0, account2, { from: account1 }), - "FeeSharingProxy::withdraw: _maxCheckpoints should be positive" - ); - }); - - it("Shouldn't be able to withdraw zero amount (for token pool)", async () => { - await protocolDeploymentFixture(); - let fees = await feeSharingProxy.getAccumulatedFees(account1, loanToken.address); - expect(fees).to.be.bignumber.equal("0"); - - await expectRevert( - feeSharingProxy.withdraw(loanToken.address, 10, ZERO_ADDRESS, { from: account1 }), - "FeeSharingProxy::withdrawFees: no tokens for a withdrawal" - ); - }); - - it("Shouldn't be able to withdraw zero amount (for wRBTC pool)", async () => { - await protocolDeploymentFixture(); - let fees = await feeSharingProxy.getAccumulatedFees(account1, loanTokenWrbtc.address); - expect(fees).to.be.bignumber.equal("0"); - - await expectRevert( - feeSharingProxy.withdraw(loanTokenWrbtc.address, 10, ZERO_ADDRESS, { from: account1 }), - "FeeSharingProxy::withdrawFees: no tokens for a withdrawal" - ); - }); - - it("Should be able to withdraw to another account", async () => { - await protocolDeploymentFixture(); - // stake - getPriorTotalVotingPower - let rootStake = 700; - await stake(rootStake, root); - - let userStake = 300; - if (MOCK_PRIOR_WEIGHTED_STAKE) { - await staking.MOCK_priorWeightedStake(userStake * 10); - } - await SOVToken.transfer(account1, userStake); - await stake(userStake, account1); - - // mock data - let lendingFeeTokensHeld = new BN(wei("1", "ether")); - let tradingFeeTokensHeld = new BN(wei("2", "ether")); - let borrowingFeeTokensHeld = new BN(wei("3", "ether")); - let totalFeeTokensHeld = lendingFeeTokensHeld.add(tradingFeeTokensHeld).add(borrowingFeeTokensHeld); - let feeAmount = await setFeeTokensHeld(lendingFeeTokensHeld, tradingFeeTokensHeld, borrowingFeeTokensHeld); - - await feeSharingProxy.withdrawFees([SUSD.address]); - - let fees = await feeSharingProxy.getAccumulatedFees(account1, loanTokenWrbtc.address); - expect(fees).to.be.bignumber.equal(new BN(feeAmount).mul(new BN(3)).div(new BN(10))); - - let tx = await feeSharingProxy.withdraw(loanTokenWrbtc.address, 1000, account2, { from: account1 }); - - // processedCheckpoints - let processedCheckpoints = await feeSharingProxy.processedCheckpoints.call(account1, loanTokenWrbtc.address); - expect(processedCheckpoints.toNumber()).to.be.equal(1); - - expectEvent(tx, "UserFeeWithdrawn", { - sender: account1, - receiver: account2, - token: loanTokenWrbtc.address, - amount: new BN(feeAmount).mul(new BN(3)).div(new BN(10)), - }); - }); - - it("Should be able to withdraw (token pool)", async () => { - await protocolDeploymentFixture(); - // FeeSharingProxy - feeSharingProxy = await FeeSharingProxyMockup.new(sovryn.address, staking.address); - await sovryn.setFeesController(feeSharingProxy.address); - - // stake - getPriorTotalVotingPower - let rootStake = 700; - await stake(rootStake, root); - - let userStake = 300; - if (MOCK_PRIOR_WEIGHTED_STAKE) { - await staking.MOCK_priorWeightedStake(userStake * 10); - } - await SOVToken.transfer(account1, userStake); - await stake(userStake, account1); - - // Mock (transfer loanToken to FeeSharingProxy contract) - const loanPoolTokenAddress = await sovryn.underlyingToLoanPool(SUSD.address); - const amountLend = new BN(wei("500", "ether")); - await SUSD.approve(loanPoolTokenAddress, amountLend); - await loanToken.mint(feeSharingProxy.address, amountLend); - - // Check ISUSD Balance for feeSharingProxy - const feeSharingProxyLoanBalanceToken = await loanToken.balanceOf(feeSharingProxy.address); - expect(feeSharingProxyLoanBalanceToken.toString()).to.be.equal(amountLend.toString()); - - // Withdraw ISUSD from feeSharingProxy - // const initial - await feeSharingProxy.addCheckPoint(loanPoolTokenAddress, amountLend.toString()); - let tx = await feeSharingProxy.trueWithdraw(loanToken.address, 10, ZERO_ADDRESS, { from: account1 }); - const updatedFeeSharingProxyLoanBalanceToken = await loanToken.balanceOf(feeSharingProxy.address); - const updatedAccount1LoanBalanceToken = await loanToken.balanceOf(account1); - console.log("\nwithdraw(checkpoints = 1).gasUsed: " + tx.receipt.gasUsed); - - expect(updatedFeeSharingProxyLoanBalanceToken.toString()).to.be.equal(((amountLend * 7) / 10).toString()); - expect(updatedAccount1LoanBalanceToken.toString()).to.be.equal(((amountLend * 3) / 10).toString()); - - expectEvent(tx, "UserFeeWithdrawn", { - sender: account1, - receiver: account1, - token: loanToken.address, - amount: amountLend.mul(new BN(3)).div(new BN(10)), - }); - }); - - it("Should be able to withdraw (WRBTC pool)", async () => { - /// @dev This test requires redeploying the protocol - await protocolDeploymentFixture(); - - // stake - getPriorTotalVotingPower - let rootStake = 700; - await stake(rootStake, root); - - let userStake = 300; - if (MOCK_PRIOR_WEIGHTED_STAKE) { - await staking.MOCK_priorWeightedStake(userStake * 10); - } - await SOVToken.transfer(account1, userStake); - await stake(userStake, account1); - - // mock data - let lendingFeeTokensHeld = new BN(wei("1", "gwei")); - let tradingFeeTokensHeld = new BN(wei("2", "gwei")); - let borrowingFeeTokensHeld = new BN(wei("3", "gwei")); - let totalFeeTokensHeld = lendingFeeTokensHeld.add(tradingFeeTokensHeld).add(borrowingFeeTokensHeld); - let feeAmount = await setFeeTokensHeld(lendingFeeTokensHeld, tradingFeeTokensHeld, borrowingFeeTokensHeld); - - await feeSharingProxy.withdrawFees([SUSD.address]); - - let fees = await feeSharingProxy.getAccumulatedFees(account1, loanTokenWrbtc.address); - expect(fees).to.be.bignumber.equal(feeAmount.mul(new BN(3)).div(new BN(10))); - - let userInitialBtcBalance = new BN(await web3.eth.getBalance(account1)); - let tx = await feeSharingProxy.withdraw(loanTokenWrbtc.address, 10, ZERO_ADDRESS, { from: account1 }); - - /// @dev To anticipate gas consumption it is required to split hardhat - /// behaviour into two different scenarios: coverage and regular testing. - /// On coverage gasPrice = 1, on regular tests gasPrice = 8000000000 - // - // On coverage: - // Fees: 1800000000 - // Balance: 10000000000000000000000 - // Balance: 10000000000001799398877 - // withdraw().gasUsed: 601123 - // txFee: 601123 - // - // On regular test: - // Fees: 1800000000 - // Balance: 10000000000000000000000 - // Balance: 9999996433281800000000 - // withdraw().gasUsed: 445840 - // txFee: 3566720000000000 - let userLatestBTCBalance = new BN(await web3.eth.getBalance(account1)); - let gasPrice; - /// @dev A balance decrease (negative difference) corresponds to regular test case - if (userLatestBTCBalance.sub(userInitialBtcBalance).toString()[0] == "-") { - gasPrice = new BN(8000000000); - } // regular test - else { - gasPrice = new BN(1); - } // coverage - - console.log("\nwithdraw(checkpoints = 1).gasUsed: " + tx.receipt.gasUsed); - let txFee = new BN(tx.receipt.gasUsed).mul(gasPrice); - - userInitialBtcBalance = userInitialBtcBalance.sub(new BN(txFee)); - // processedCheckpoints - let processedCheckpoints = await feeSharingProxy.processedCheckpoints.call(account1, loanTokenWrbtc.address); - expect(processedCheckpoints.toNumber()).to.be.equal(1); - - // check balances - let feeSharingProxyBalance = await loanTokenWrbtc.balanceOf.call(feeSharingProxy.address); - expect(feeSharingProxyBalance.toNumber()).to.be.equal((feeAmount * 7) / 10); - let userLoanTokenBalance = await loanTokenWrbtc.balanceOf.call(account1); - expect(userLoanTokenBalance.toNumber()).to.be.equal(0); - let userExpectedBtcBalance = userInitialBtcBalance.add(feeAmount.mul(new BN(3)).div(new BN(10))); - expect(userLatestBTCBalance.toString()).to.be.equal(userExpectedBtcBalance.toString()); - - expectEvent(tx, "UserFeeWithdrawn", { - sender: account1, - receiver: account1, - token: loanTokenWrbtc.address, - amount: feeAmount.mul(new BN(3)).div(new BN(10)), - }); - }); - - it("Should be able to withdraw (sov pool)", async () => { - /// @dev This test requires redeploying the protocol - await protocolDeploymentFixture(); - - //stake - getPriorTotalVotingPower - let rootStake = 700; - await stake(rootStake, root); - - let userStake = 300; - if (MOCK_PRIOR_WEIGHTED_STAKE) { - await staking.MOCK_priorWeightedStake(userStake * 10); - } - await SOVToken.transfer(account1, userStake); - await stake(userStake, account1); - - //mock data - let lendingFeeTokensHeld = new BN(wei("1", "gwei")); - let tradingFeeTokensHeld = new BN(wei("2", "gwei")); - let borrowingFeeTokensHeld = new BN(wei("3", "gwei")); - let totalFeeTokensHeld = lendingFeeTokensHeld.add(tradingFeeTokensHeld).add(borrowingFeeTokensHeld); - let feeAmount = await setFeeTokensHeld(lendingFeeTokensHeld, tradingFeeTokensHeld, borrowingFeeTokensHeld, false, true); - - await feeSharingProxy.withdrawFees([SOVToken.address]); - - let fees = await feeSharingProxy.getAccumulatedFees(account1, SOVToken.address); - expect(fees).to.be.bignumber.equal(feeAmount.mul(new BN(3)).div(new BN(10))); - - let userInitialISOVBalance = await SOVToken.balanceOf(account1); - let tx = await feeSharingProxy.withdraw(SOVToken.address, 10, ZERO_ADDRESS, { from: account1 }); - - //processedCheckpoints - let processedCheckpoints = await feeSharingProxy.processedCheckpoints.call(account1, SOVToken.address); - expect(processedCheckpoints.toNumber()).to.be.equal(1); - - //check balances - let feeSharingProxyBalance = await SOVToken.balanceOf.call(feeSharingProxy.address); - expect(feeSharingProxyBalance.toNumber()).to.be.equal((feeAmount * 7) / 10); - let userBalance = await SOVToken.balanceOf.call(account1); - expect(userBalance.sub(userInitialISOVBalance).toNumber()).to.be.equal((feeAmount * 3) / 10); - - expectEvent(tx, "UserFeeWithdrawn", { - sender: account1, - receiver: account1, - token: SOVToken.address, - amount: new BN(feeAmount).mul(new BN(3)).div(new BN(10)), - }); - }); - - it("Should be able to withdraw using 3 checkpoints", async () => { - /// @dev This test requires redeploying the protocol - await protocolDeploymentFixture(); - - // stake - getPriorTotalVotingPower - let rootStake = 900; - await stake(rootStake, root); - - let userStake = 100; - if (MOCK_PRIOR_WEIGHTED_STAKE) { - await staking.MOCK_priorWeightedStake(userStake * 10); - } - await SOVToken.transfer(account1, userStake); - await stake(userStake, account1); - - // [FIRST] - // mock data - let lendingFeeTokensHeld = new BN(wei("1", "gwei")); - let tradingFeeTokensHeld = new BN(wei("2", "gwei")); - let borrowingFeeTokensHeld = new BN(wei("3", "gwei")); - let totalFeeTokensHeld = lendingFeeTokensHeld.add(tradingFeeTokensHeld).add(borrowingFeeTokensHeld); - let feeAmount = await setFeeTokensHeld(lendingFeeTokensHeld, tradingFeeTokensHeld, borrowingFeeTokensHeld); - let totalFeeAmount = feeAmount; - await feeSharingProxy.withdrawFees([SUSD.address]); - - let userInitialBtcBalance = new BN(await web3.eth.getBalance(account1)); - let tx = await feeSharingProxy.withdraw(loanTokenWrbtc.address, 1, ZERO_ADDRESS, { from: account1 }); - - /// @dev Same as above gas consumption is different on regular tests than on coverge - let userLatestBTCBalance = new BN(await web3.eth.getBalance(account1)); - let gasPrice; - /// @dev A balance decrease (negative difference) corresponds to regular test case - if (userLatestBTCBalance.sub(userInitialBtcBalance).toString()[0] == "-") { - gasPrice = new BN(8000000000); - } // regular test - else { - gasPrice = new BN(1); - } // coverage - - console.log("\nwithdraw(checkpoints = 1).gasUsed: " + tx.receipt.gasUsed); - let txFee = new BN(tx.receipt.gasUsed).mul(gasPrice); - - userInitialBtcBalance = userInitialBtcBalance.sub(new BN(txFee)); - // processedCheckpoints - let processedCheckpoints = await feeSharingProxy.processedCheckpoints.call(account1, loanTokenWrbtc.address); - expect(processedCheckpoints.toNumber()).to.be.equal(1); - - // check balances - let feeSharingProxyBalance = await loanTokenWrbtc.balanceOf.call(feeSharingProxy.address); - expect(feeSharingProxyBalance.toNumber()).to.be.equal((totalFeeAmount * 9) / 10); - let userBalance = await loanTokenWrbtc.balanceOf.call(account1); - expect(userBalance.toNumber()).to.be.equal(0); - - expect(userLatestBTCBalance.toString()).to.be.equal( - userInitialBtcBalance.add(totalFeeAmount.mul(new BN(1)).div(new BN(10))).toString() - ); - - // [SECOND] - // mock data - let lendingFeeTokensHeld2 = new BN(wei("1", "gwei")); - let tradingFeeTokensHeld2 = new BN(wei("2", "gwei")); - let borrowingFeeTokensHeld2 = new BN(wei("3", "gwei")); - totalFeeTokensHeld = lendingFeeTokensHeld2.add(tradingFeeTokensHeld2).add(borrowingFeeTokensHeld2); - feeAmount = await setFeeTokensHeld(lendingFeeTokensHeld2, tradingFeeTokensHeld2, borrowingFeeTokensHeld2); - totalFeeAmount = totalFeeAmount.add(feeAmount); - let totalLoanTokenWRBTCBalanceShouldBeAccount1 = feeAmount; - await increaseTime(FEE_WITHDRAWAL_INTERVAL); - await feeSharingProxy.withdrawFees([SUSD.address]); - - // [THIRD] - // mock data - let lendingFeeTokensHeld3 = new BN(wei("1", "gwei")); - let tradingFeeTokensHeld3 = new BN(wei("2", "gwei")); - let borrowingFeeTokensHeld3 = new BN(wei("3", "gwei")); - totalFeeTokensHeld = lendingFeeTokensHeld3.add(tradingFeeTokensHeld3).add(borrowingFeeTokensHeld3); - feeAmount = await setFeeTokensHeld(lendingFeeTokensHeld3, tradingFeeTokensHeld3, borrowingFeeTokensHeld3); - totalFeeAmount = totalFeeAmount.add(feeAmount); - totalLoanTokenWRBTCBalanceShouldBeAccount1 = totalLoanTokenWRBTCBalanceShouldBeAccount1.add(feeAmount); - await increaseTime(FEE_WITHDRAWAL_INTERVAL); - await feeSharingProxy.withdrawFees([SUSD.address]); - - // [SECOND] - [THIRD] - userInitialBtcBalance = new BN(await web3.eth.getBalance(account1)); - tx = await feeSharingProxy.withdraw(loanTokenWrbtc.address, 2, ZERO_ADDRESS, { from: account1 }); - console.log("\nwithdraw(checkpoints = 2).gasUsed: " + tx.receipt.gasUsed); - txFee = new BN(tx.receipt.gasUsed).mul(gasPrice); - - userInitialBtcBalance = userInitialBtcBalance.sub(new BN(txFee)); - - // processedCheckpoints - processedCheckpoints = await feeSharingProxy.processedCheckpoints.call(account1, loanTokenWrbtc.address); - expect(processedCheckpoints.toNumber()).to.be.equal(3); - - // check balances - feeSharingProxyBalance = await loanTokenWrbtc.balanceOf.call(feeSharingProxy.address); - expect(feeSharingProxyBalance.toNumber()).to.be.equal(parseInt((totalFeeAmount * 9) / 10)); - userBalance = await loanTokenWrbtc.balanceOf.call(account1); - expect(userBalance.toNumber()).to.be.equal(0); - - userLatestBTCBalance = new BN(await web3.eth.getBalance(account1)); - - expect(userLatestBTCBalance.toString()).to.be.equal( - userInitialBtcBalance.add(totalLoanTokenWRBTCBalanceShouldBeAccount1.mul(new BN(1)).div(new BN(10))).toString() - ); - }); - - it("Should be able to process 10 checkpoints", async () => { - /// @dev This test requires redeploying the protocol - await protocolDeploymentFixture(); - - // stake - getPriorTotalVotingPower - await stake(900, root); - let userStake = 100; - if (MOCK_PRIOR_WEIGHTED_STAKE) { - await staking.MOCK_priorWeightedStake(userStake * 10); - } - await SOVToken.transfer(account1, userStake); - await stake(userStake, account1); - - // mock data - await createCheckpoints(10); - - let tx = await feeSharingProxy.withdraw(loanTokenWrbtc.address, 1000, ZERO_ADDRESS, { from: account1 }); - console.log("\nwithdraw(checkpoints = 10).gasUsed: " + tx.receipt.gasUsed); - // processedCheckpoints - let processedCheckpoints = await feeSharingProxy.processedCheckpoints.call(account1, loanTokenWrbtc.address); - expect(processedCheckpoints.toNumber()).to.be.equal(10); - }); - - it("Should be able to process 10 checkpoints and 3 withdrawals", async () => { - /// @dev This test requires redeploying the protocol - await protocolDeploymentFixture(); - - // stake - getPriorTotalVotingPower - await stake(900, root); - let userStake = 100; - if (MOCK_PRIOR_WEIGHTED_STAKE) { - await staking.MOCK_priorWeightedStake(userStake * 10); - } - await SOVToken.transfer(account1, userStake); - await stake(userStake, account1); - - // mock data - await createCheckpoints(10); - - let tx = await feeSharingProxy.withdraw(loanTokenWrbtc.address, 5, ZERO_ADDRESS, { from: account1 }); - console.log("\nwithdraw(checkpoints = 5).gasUsed: " + tx.receipt.gasUsed); - // processedCheckpoints - let processedCheckpoints = await feeSharingProxy.processedCheckpoints.call(account1, loanTokenWrbtc.address); - expect(processedCheckpoints.toNumber()).to.be.equal(5); - - tx = await feeSharingProxy.withdraw(loanTokenWrbtc.address, 3, ZERO_ADDRESS, { from: account1 }); - console.log("\nwithdraw(checkpoints = 3).gasUsed: " + tx.receipt.gasUsed); - // processedCheckpoints - processedCheckpoints = await feeSharingProxy.processedCheckpoints.call(account1, loanTokenWrbtc.address); - expect(processedCheckpoints.toNumber()).to.be.equal(8); - - tx = await feeSharingProxy.withdraw(loanTokenWrbtc.address, 1000, ZERO_ADDRESS, { from: account1 }); - console.log("\nwithdraw(checkpoints = 2).gasUsed: " + tx.receipt.gasUsed); - // processedCheckpoints - processedCheckpoints = await feeSharingProxy.processedCheckpoints.call(account1, loanTokenWrbtc.address); - expect(processedCheckpoints.toNumber()).to.be.equal(10); - }); - - // // use for gas usage tests - // it("Should be able to process 30 checkpoints", async () => { - // // stake - getPriorTotalVotingPower - // await stake(900, root); - // let userStake = 100; - // if (MOCK_PRIOR_WEIGHTED_STAKE) { - // await staking.MOCK_priorWeightedStake(userStake * 10); - // } - // await SOVToken.transfer(account1, userStake); - // await stake(userStake, account1); - // - // // mock data - // await createCheckpoints(30); - // - // let tx = await feeSharingProxy.withdraw(loanToken.address, 1000, ZERO_ADDRESS, {from: account1}); - // console.log("\nwithdraw(checkpoints = 30).gasUsed: " + tx.receipt.gasUsed); - // // processedCheckpoints - // let processedCheckpoints = await feeSharingProxy.processedCheckpoints.call(account1, loanToken.address); - // expect(processedCheckpoints.toNumber()).to.be.equal(30); - // }); - // - // // use for gas usage tests - // it("Should be able to process 100 checkpoints", async () => { - // // stake - getPriorTotalVotingPower - // await stake(900, root); - // let userStake = 100; - // if (MOCK_PRIOR_WEIGHTED_STAKE) { - // await staking.MOCK_priorWeightedStake(userStake * 10); - // } - // await SOVToken.transfer(account1, userStake); - // await stake(userStake, account1); - // - // // mock data - // await createCheckpoints(100); - // - // let tx = await feeSharingProxy.withdraw(loanToken.address, 1000, ZERO_ADDRESS, {from: account1}); - // console.log("\nwithdraw(checkpoints = 500).gasUsed: " + tx.receipt.gasUsed); - // // processedCheckpoints - // let processedCheckpoints = await feeSharingProxy.processedCheckpoints.call(account1, loanToken.address); - // expect(processedCheckpoints.toNumber()).to.be.equal(100); - // }); - // - // // use for gas usage tests - // it("Should be able to withdraw when staking contains a lot of checkpoints", async () => { - // let checkpointCount = 1000; - // await stake(1000, root, checkpointCount); - // let afterBlock = await blockNumber(); - // console.log(afterBlock); - // - // let kickoffTS = await staking.kickoffTS.call(); - // let stakingDate = kickoffTS.add(new BN(MAX_DURATION)); - // - // let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints.call(root, stakingDate); - // let firstCheckpoint = await staking.userStakingCheckpoints.call(root, stakingDate, 0); - // let lastCheckpoint = await staking.userStakingCheckpoints.call(root, stakingDate, numUserStakingCheckpoints - 1); - // let block1 = firstCheckpoint.fromBlock.toNumber() + 1; - // let block2 = lastCheckpoint.fromBlock; - // - // console.log("numUserStakingCheckpoints = " + numUserStakingCheckpoints.toString()); - // console.log("first = " + firstCheckpoint.fromBlock.toString()); - // console.log("last = " + lastCheckpoint.fromBlock.toString()); - // - // let tx = await staking.calculatePriorWeightedStake(root, block1, stakingDate); - // console.log("\ncalculatePriorWeightedStake(checkpoints = " + checkpointCount + ").gasUsed: " + tx.receipt.gasUsed); - // tx = await staking.calculatePriorWeightedStake(root, block2, stakingDate); - // console.log("\ncalculatePriorWeightedStake(checkpoints = " + checkpointCount + ").gasUsed: " + tx.receipt.gasUsed); - // }); - - it("Should be able to withdraw with staking for 78 dates", async () => { - /// @dev This test requires redeploying the protocol - await protocolDeploymentFixture(); - - // stake - getPriorTotalVotingPower - let rootStake = 700; - await stake(rootStake, root); - - let userStake = 300; - if (MOCK_PRIOR_WEIGHTED_STAKE) { - await staking.MOCK_priorWeightedStake(userStake * 10); - } - await SOVToken.transfer(account1, userStake); - await stake(userStake, account1); - - let kickoffTS = await staking.kickoffTS.call(); - await SOVToken.approve(staking.address, userStake * 1000); - for (let i = 0; i < 77; i++) { - let stakingDate = kickoffTS.add(new BN(TWO_WEEKS * (i + 1))); - await staking.stake(userStake, stakingDate, account1, account1); - } - - // mock data - await setFeeTokensHeld(new BN(100), new BN(200), new BN(300)); - - await feeSharingProxy.withdrawFees([SUSD.address]); - - let tx = await feeSharingProxy.withdraw(loanTokenWrbtc.address, 10, ZERO_ADDRESS, { from: account1 }); - console.log("\nwithdraw(checkpoints = 1).gasUsed: " + tx.receipt.gasUsed); - }); - - it("should compute the weighted stake and show gas usage", async () => { - /// @dev This test requires redeploying the protocol - await protocolDeploymentFixture(); - - await stake(100, root); - let kickoffTS = await staking.kickoffTS.call(); - let stakingDate = kickoffTS.add(new BN(MAX_DURATION)); - await SOVToken.approve(staking.address, 100); - let result = await staking.stake("100", stakingDate, root, root); - await mineBlock(); - - let tx = await staking.calculatePriorWeightedStake(root, result.receipt.blockNumber, stakingDate); - console.log("\ngasUsed: " + tx.receipt.gasUsed); - }); - }); - - describe("withdraw with or considering vesting contracts", () => { - it("getAccumulatedFees should return 0 for vesting contracts", async () => { - /// @dev This test requires redeploying the protocol - await protocolDeploymentFixture(); - - let { vestingInstance } = await createVestingContractWithSingleDate(new BN(MAX_DURATION), 1000, root); - await setFeeTokensHeld(new BN(100), new BN(200), new BN(300)); - let fees = await feeSharingProxy.getAccumulatedFees(vestingInstance.address, loanToken.address); - expect(fees).to.be.bignumber.equal("0"); - }); - - it("vesting contract should not be able to withdraw fees", async () => { - /// @dev This test requires redeploying the protocol - await protocolDeploymentFixture(); - - let { vestingInstance } = await createVestingContractWithSingleDate(new BN(MAX_DURATION), 1000, root); - await setFeeTokensHeld(new BN(100), new BN(200), new BN(300)); - await expectRevert( - vestingInstance.collectDividends(loanToken.address, 5, root), - "FeeSharingProxy::withdrawFees: no tokens for a withdrawal" - ); - }); - - it("vested stakes should be deducted from total weighted stake on share distribution", async () => { - /// @dev This test requires redeploying the protocol - await protocolDeploymentFixture(); - - // 50% vested 50% voluntary stakes - await createVestingContractWithSingleDate(new BN(MAX_DURATION), 1000, root); - let userStake = 1000; - if (MOCK_PRIOR_WEIGHTED_STAKE) { - await staking.MOCK_priorWeightedStake(userStake * 10); - } - await SOVToken.transfer(account1, userStake); - await stake(userStake, account1); - - await setFeeTokensHeld(new BN(100), new BN(200), new BN(300)); - let tx = await feeSharingProxy.withdrawFees([SUSD.address]); - let feesWithdrawn = tx.logs[1].args.amount; - let userFees = await feeSharingProxy.getAccumulatedFees(account1, loanTokenWrbtc.address); - - // 100% of the fees should go to the user -> vesting contract not considered - expect(feesWithdrawn).to.be.bignumber.equal(userFees); - }); - }); - - describe("withdraw AMM Fees", async () => { - it("Whitelist converter", async () => { - /// @dev This test requires redeploying the protocol - await protocolDeploymentFixture(); - - await expectRevert(feeSharingProxy.addWhitelistedConverterAddress(account1), "Non contract address given"); - await expectRevert(feeSharingProxy.addWhitelistedConverterAddress(ZERO_ADDRESS), "Non contract address given"); - - const liquidityPoolV1Converter = await LiquidityPoolV1Converter.new(SOVToken.address, SUSD.address); - await feeSharingProxy.addWhitelistedConverterAddress(liquidityPoolV1Converter.address); - let whitelistedConverterList = await feeSharingProxy.getWhitelistedConverterList(); - expect(whitelistedConverterList.length).to.equal(1); - expect(whitelistedConverterList[0]).to.equal(liquidityPoolV1Converter.address); - await feeSharingProxy.addWhitelistedConverterAddress(liquidityPoolV1Converter.address); - whitelistedConverterList = await feeSharingProxy.getWhitelistedConverterList(); - expect(whitelistedConverterList.length).to.equal(1); - expect(whitelistedConverterList[0]).to.equal(liquidityPoolV1Converter.address); - }); - - it("Remove converter from whitelist", async () => { - /// @dev This test requires redeploying the protocol - await protocolDeploymentFixture(); - - const liquidityPoolV1Converter = await LiquidityPoolV1Converter.new(SOVToken.address, SUSD.address); - let whitelistedConverterList = await feeSharingProxy.getWhitelistedConverterList(); - expect(whitelistedConverterList.length).to.equal(0); - - await feeSharingProxy.removeWhitelistedConverterAddress(liquidityPoolV1Converter.address); - whitelistedConverterList = await feeSharingProxy.getWhitelistedConverterList(); - expect(whitelistedConverterList.length).to.equal(0); - - await feeSharingProxy.addWhitelistedConverterAddress(liquidityPoolV1Converter.address); - whitelistedConverterList = await feeSharingProxy.getWhitelistedConverterList(); - expect(whitelistedConverterList.length).to.equal(1); - expect(whitelistedConverterList[0]).to.equal(liquidityPoolV1Converter.address); - - await feeSharingProxy.removeWhitelistedConverterAddress(liquidityPoolV1Converter.address); - whitelistedConverterList = await feeSharingProxy.getWhitelistedConverterList(); - expect(whitelistedConverterList.length).to.equal(0); - }); - - it("should not be able to withdraw fees if converters address is not a contract address", async () => { - /// @dev This test requires redeploying the protocol - await protocolDeploymentFixture(); - - await expectRevert(feeSharingProxy.withdrawFeesAMM([accounts[0]]), "Invalid Converter"); - }); - - it("Should not be able to withdraw AMM Fees after whitelist removal", async () => { - /// @dev This test requires redeploying the protocol - await protocolDeploymentFixture(); - - //stake - getPriorTotalVotingPower - let totalStake = 1000; - await stake(totalStake, root); - - //mock data - // AMM Converter - liquidityPoolV1Converter = await LiquidityPoolV1Converter.new(SOVToken.address, SUSD.address); - const feeAmount = new BN(wei("1", "ether")); - await liquidityPoolV1Converter.setTotalFeeMockupValue(feeAmount.toString()); - - await expectRevert(feeSharingProxy.withdrawFeesAMM([liquidityPoolV1Converter.address]), "Invalid Converter"); - await feeSharingProxy.addWhitelistedConverterAddress(liquidityPoolV1Converter.address); - await feeSharingProxy.removeWhitelistedConverterAddress(liquidityPoolV1Converter.address); - await expectRevert(feeSharingProxy.withdrawFeesAMM([liquidityPoolV1Converter.address]), "Invalid Converter"); - await feeSharingProxy.addWhitelistedConverterAddress(liquidityPoolV1Converter.address); - - await expectRevert(feeSharingProxy.withdrawFeesAMM([liquidityPoolV1Converter.address]), "unauthorized"); - await liquidityPoolV1Converter.setFeesController(feeSharingProxy.address); - await liquidityPoolV1Converter.setWrbtcToken(WRBTC.address); - await WRBTC.mint(liquidityPoolV1Converter.address, wei("2", "ether")); - - tx = await feeSharingProxy.withdrawFeesAMM([liquidityPoolV1Converter.address]); - - //check WRBTC balance (wrbt balance = (totalFeeTokensHeld * mockPrice) - swapFee) - let feeSharingProxyBalance = await loanTokenWrbtc.balanceOf.call(feeSharingProxy.address); - expect(feeSharingProxyBalance.toString()).to.be.equal(feeAmount.toString()); - - // make sure wrbtc balance is 0 after withdrawal - let feeSharingProxyWRBTCBalance = await WRBTC.balanceOf.call(feeSharingProxy.address); - expect(feeSharingProxyWRBTCBalance.toString()).to.be.equal(new BN(0).toString()); - - //checkpoints - let numTokenCheckpoints = await feeSharingProxy.numTokenCheckpoints.call(loanTokenWrbtc.address); - expect(numTokenCheckpoints.toNumber()).to.be.equal(1); - let checkpoint = await feeSharingProxy.tokenCheckpoints.call(loanTokenWrbtc.address, 0); - expect(checkpoint.blockNumber.toNumber()).to.be.equal(tx.receipt.blockNumber); - expect(checkpoint.totalWeightedStake.toNumber()).to.be.equal(totalStake * MAX_VOTING_WEIGHT); - expect(checkpoint.numTokens.toString()).to.be.equal(feeAmount.toString()); - - //check lastFeeWithdrawalTime - let lastFeeWithdrawalTime = await feeSharingProxy.lastFeeWithdrawalTime.call(loanTokenWrbtc.address); - let block = await web3.eth.getBlock(tx.receipt.blockNumber); - expect(lastFeeWithdrawalTime.toString()).to.be.equal(block.timestamp.toString()); - - expectEvent(tx, "FeeAMMWithdrawn", { - sender: root, - converter: liquidityPoolV1Converter.address, - amount: feeAmount, - }); - }); - - it("Should be able to withdraw AMM Fees", async () => { - /// @dev This test requires redeploying the protocol - await protocolDeploymentFixture(); - - //stake - getPriorTotalVotingPower - let totalStake = 1000; - await stake(totalStake, root); - - //mock data - // AMM Converter - liquidityPoolV1Converter = await LiquidityPoolV1Converter.new(SOVToken.address, SUSD.address); - const feeAmount = new BN(wei("1", "ether")); - await liquidityPoolV1Converter.setTotalFeeMockupValue(feeAmount.toString()); - - await expectRevert(feeSharingProxy.withdrawFeesAMM([liquidityPoolV1Converter.address]), "Invalid Converter"); - await feeSharingProxy.addWhitelistedConverterAddress(liquidityPoolV1Converter.address); - await expectRevert(feeSharingProxy.withdrawFeesAMM([liquidityPoolV1Converter.address]), "unauthorized"); - await liquidityPoolV1Converter.setFeesController(feeSharingProxy.address); - await liquidityPoolV1Converter.setWrbtcToken(WRBTC.address); - await WRBTC.mint(liquidityPoolV1Converter.address, wei("2", "ether")); - - tx = await feeSharingProxy.withdrawFeesAMM([liquidityPoolV1Converter.address]); - - //check WRBTC balance (wrbt balance = (totalFeeTokensHeld * mockPrice) - swapFee) - let feeSharingProxyBalance = await loanTokenWrbtc.balanceOf.call(feeSharingProxy.address); - expect(feeSharingProxyBalance.toString()).to.be.equal(feeAmount.toString()); - - // make sure wrbtc balance is 0 after withdrawal - let feeSharingProxyWRBTCBalance = await WRBTC.balanceOf.call(feeSharingProxy.address); - expect(feeSharingProxyWRBTCBalance.toString()).to.be.equal(new BN(0).toString()); - - //checkpoints - let numTokenCheckpoints = await feeSharingProxy.numTokenCheckpoints.call(loanTokenWrbtc.address); - expect(numTokenCheckpoints.toNumber()).to.be.equal(1); - let checkpoint = await feeSharingProxy.tokenCheckpoints.call(loanTokenWrbtc.address, 0); - expect(checkpoint.blockNumber.toNumber()).to.be.equal(tx.receipt.blockNumber); - expect(checkpoint.totalWeightedStake.toNumber()).to.be.equal(totalStake * MAX_VOTING_WEIGHT); - expect(checkpoint.numTokens.toString()).to.be.equal(feeAmount.toString()); - - //check lastFeeWithdrawalTime - let lastFeeWithdrawalTime = await feeSharingProxy.lastFeeWithdrawalTime.call(loanTokenWrbtc.address); - let block = await web3.eth.getBlock(tx.receipt.blockNumber); - expect(lastFeeWithdrawalTime.toString()).to.be.equal(block.timestamp.toString()); - - expectEvent(tx, "FeeAMMWithdrawn", { - sender: root, - converter: liquidityPoolV1Converter.address, - amount: feeAmount, - }); - }); - - it("Should be able to withdraw with 0 AMM Fees", async () => { - /// @dev This test requires redeploying the protocol - await protocolDeploymentFixture(); - - //stake - getPriorTotalVotingPower - let totalStake = 1000; - await stake(totalStake, root); - - //mock data - // AMM Converter - liquidityPoolV1Converter = await LiquidityPoolV1Converter.new(SOVToken.address, SUSD.address); - const feeAmount = new BN(wei("0", "ether")); - await liquidityPoolV1Converter.setTotalFeeMockupValue(feeAmount.toString()); - await expectRevert(feeSharingProxy.withdrawFeesAMM([liquidityPoolV1Converter.address]), "Invalid Converter"); - await feeSharingProxy.addWhitelistedConverterAddress(liquidityPoolV1Converter.address); - await expectRevert(feeSharingProxy.withdrawFeesAMM([liquidityPoolV1Converter.address]), "unauthorized"); - await liquidityPoolV1Converter.setFeesController(feeSharingProxy.address); - await liquidityPoolV1Converter.setWrbtcToken(WRBTC.address); - await WRBTC.mint(liquidityPoolV1Converter.address, wei("2", "ether")); - - tx = await feeSharingProxy.withdrawFeesAMM([liquidityPoolV1Converter.address]); - - //check WRBTC balance (wrbt balance = (totalFeeTokensHeld * mockPrice) - swapFee) - let feeSharingProxyBalance = await loanTokenWrbtc.balanceOf.call(feeSharingProxy.address); - expect(feeSharingProxyBalance.toString()).to.be.equal(feeAmount.toString()); - - // make sure wrbtc balance is 0 after withdrawal - let feeSharingProxyWRBTCBalance = await WRBTC.balanceOf.call(feeSharingProxy.address); - expect(feeSharingProxyWRBTCBalance.toString()).to.be.equal(new BN(0).toString()); - - //checkpoints - let numTokenCheckpoints = await feeSharingProxy.numTokenCheckpoints.call(loanTokenWrbtc.address); - expect(numTokenCheckpoints.toNumber()).to.be.equal(0); - let checkpoint = await feeSharingProxy.tokenCheckpoints.call(loanTokenWrbtc.address, 0); - expect(checkpoint.blockNumber.toNumber()).to.be.equal(0); - expect(checkpoint.totalWeightedStake.toNumber()).to.be.equal(0); - expect(checkpoint.numTokens.toString()).to.be.equal("0"); - - //check lastFeeWithdrawalTime - let lastFeeWithdrawalTime = await feeSharingProxy.lastFeeWithdrawalTime.call(loanTokenWrbtc.address); - expect(lastFeeWithdrawalTime.toString()).to.be.equal("0"); - }); - }); - - describe("withdraw wrbtc", async () => { - it("Withdraw wrbtc from non owner should revert", async () => { - await protocolDeploymentFixture(); - const receiver = accounts[1]; - const previousBalanceReceiver = await WRBTC.balanceOf(receiver); - await expectRevert(feeSharingProxy.withdrawWRBTC(receiver, 0, { from: accounts[1] }), "unauthorized"); - }); - - it("Withdraw 0 wrbtc", async () => { - await protocolDeploymentFixture(); - const receiver = accounts[1]; - const previousBalanceReceiver = await WRBTC.balanceOf(receiver); - await feeSharingProxy.withdrawWRBTC(receiver, 0); - const latestBalanceReceiver = await WRBTC.balanceOf(receiver); - const latestBalanceFeeSharingProxy = await WRBTC.balanceOf(feeSharingProxy.address); - - expect(new BN(latestBalanceReceiver).sub(new BN(previousBalanceReceiver)).toString()).to.equal("0"); - expect(latestBalanceFeeSharingProxy.toString()).to.equal("0"); - }); - - it("Withdraw wrbtc more than the balance of feeSharingProxy should revert", async () => { - await protocolDeploymentFixture(); - await WRBTC.mint(root, wei("500", "ether")); - await WRBTC.transfer(feeSharingProxy.address, wei("1", "ether")); - - const receiver = accounts[1]; - const previousBalanceReceiver = await WRBTC.balanceOf(receiver); - const feeSharingProxyBalance = await WRBTC.balanceOf(feeSharingProxy.address); - const amount = feeSharingProxyBalance.add(new BN(100)); - const previousBalanceFeeSharingProxy = await WRBTC.balanceOf(feeSharingProxy.address); - - await expectRevert(feeSharingProxy.withdrawWRBTC(receiver, amount.toString()), "Insufficient balance"); - - const latestBalanceReceiver = await WRBTC.balanceOf(receiver); - const latestBalanceFeeSharingProxy = await WRBTC.balanceOf(feeSharingProxy.address); - - expect(new BN(latestBalanceReceiver).sub(new BN(previousBalanceReceiver)).toString()).to.equal("0"); - expect(latestBalanceFeeSharingProxy.toString()).to.equal(previousBalanceFeeSharingProxy.toString()); - }); - - it("Fully Withdraw wrbtc", async () => { - await protocolDeploymentFixture(); - await WRBTC.mint(root, wei("500", "ether")); - await WRBTC.transfer(feeSharingProxy.address, wei("1", "ether")); - - const receiver = accounts[1]; - const previousBalanceReceiver = await WRBTC.balanceOf(receiver); - const feeSharingProxyBalance = await WRBTC.balanceOf(feeSharingProxy.address); - - const tx = await feeSharingProxy.withdrawWRBTC(receiver, feeSharingProxyBalance.toString()); - await expectEvent.inTransaction(tx.receipt.rawLogs[0].transactionHash, WRBTC, "Transfer", { - src: feeSharingProxy.address, - dst: receiver, - wad: feeSharingProxyBalance.toString(), - }); - - const latestBalanceReceiver = await WRBTC.balanceOf(receiver); - const latestBalanceFeeSharingProxy = await WRBTC.balanceOf(feeSharingProxy.address); - - expect(new BN(latestBalanceReceiver).sub(new BN(previousBalanceReceiver)).toString()).to.equal( - feeSharingProxyBalance.toString() - ); - expect(latestBalanceFeeSharingProxy.toString()).to.equal("0"); - }); - - it("Partially Withdraw wrbtc", async () => { - await protocolDeploymentFixture(); - await WRBTC.mint(root, wei("500", "ether")); - await WRBTC.transfer(feeSharingProxy.address, wei("1", "ether")); - - const receiver = accounts[1]; - const restAmount = new BN("100"); // 100 wei - const previousBalanceReceiver = await WRBTC.balanceOf(receiver); - const feeSharingProxyBalance = await WRBTC.balanceOf(feeSharingProxy.address); - const amount = feeSharingProxyBalance.sub(restAmount); - const previousBalanceFeeSharingProxy = await WRBTC.balanceOf(feeSharingProxy.address); - expect(previousBalanceFeeSharingProxy.toString()).to.equal(wei("1", "ether")); - - const tx = await feeSharingProxy.withdrawWRBTC(receiver, amount.toString()); - await expectEvent.inTransaction(tx.receipt.rawLogs[0].transactionHash, WRBTC, "Transfer", { - src: feeSharingProxy.address, - dst: receiver, - wad: amount, - }); - - const latestBalanceReceiver = await WRBTC.balanceOf(receiver); - const latestBalanceFeeSharingProxy = await WRBTC.balanceOf(feeSharingProxy.address); - - expect(new BN(latestBalanceReceiver).sub(new BN(previousBalanceReceiver)).toString()).to.equal(amount.toString()); - expect(latestBalanceFeeSharingProxy.toString()).to.equal(restAmount.toString()); - - // try to withdraw the rest - const tx2 = await feeSharingProxy.withdrawWRBTC(receiver, latestBalanceFeeSharingProxy.toString()); - const finalBalanceFeeSharingProxy = await WRBTC.balanceOf(feeSharingProxy.address); - const finalBalanceReceiver = await WRBTC.balanceOf(receiver); - expect(new BN(finalBalanceReceiver).toString()).to.equal(previousBalanceFeeSharingProxy.toString()); - expect(finalBalanceFeeSharingProxy.toString()).to.equal("0"); - - await expectEvent.inTransaction(tx2.receipt.rawLogs[0].transactionHash, WRBTC, "Transfer", { - src: feeSharingProxy.address, - dst: receiver, - wad: latestBalanceFeeSharingProxy.toString(), - }); - }); - }); - - async function stake(amount, user, checkpointCount) { - await SOVToken.approve(staking.address, amount); - let kickoffTS = await staking.kickoffTS.call(); - let stakingDate = kickoffTS.add(new BN(MAX_DURATION)); - let tx = await staking.stake(amount, stakingDate, user, user); - await mineBlock(); - - if (checkpointCount > 0) { - await increaseStake(amount, user, stakingDate, checkpointCount - 1); - } - - return tx; - } - - async function increaseStake(amount, user, stakingDate, checkpointCount) { - for (let i = 0; i < checkpointCount; i++) { - await SOVToken.approve(staking.address, amount); - await staking.increaseStake(amount, user, stakingDate); - } - } - - async function setFeeTokensHeld(lendingFee, tradingFee, borrowingFee, wrbtcTokenFee = false, sovTokenFee = false) { - let totalFeeAmount = lendingFee.add(tradingFee).add(borrowingFee); - let tokenFee; - if (wrbtcTokenFee) { - tokenFee = WRBTC; - } else { - tokenFee = SUSD; - await tokenFee.transfer(sovryn.address, totalFeeAmount); - } - await sovryn.setLendingFeeTokensHeld(tokenFee.address, lendingFee); - await sovryn.setTradingFeeTokensHeld(tokenFee.address, tradingFee); - await sovryn.setBorrowingFeeTokensHeld(tokenFee.address, borrowingFee); - - if (sovTokenFee) { - await SOVToken.transfer(sovryn.address, totalFeeAmount); - await sovryn.setLendingFeeTokensHeld(SOVToken.address, lendingFee); - await sovryn.setTradingFeeTokensHeld(SOVToken.address, tradingFee); - await sovryn.setBorrowingFeeTokensHeld(SOVToken.address, borrowingFee); - } - return totalFeeAmount; - } - - async function checkWithdrawFee(checkSUSD = true, checkWRBTC = false, checkSOV = false) { - if (checkSUSD) { - let protocolBalance = await SUSD.balanceOf(sovryn.address); - expect(protocolBalance.toString()).to.be.equal(new BN(0).toString()); - let lendingFeeTokensHeld = await sovryn.lendingFeeTokensHeld.call(SUSD.address); - expect(lendingFeeTokensHeld.toString()).to.be.equal(new BN(0).toString()); - let tradingFeeTokensHeld = await sovryn.tradingFeeTokensHeld.call(SUSD.address); - expect(tradingFeeTokensHeld.toString()).to.be.equal(new BN(0).toString()); - let borrowingFeeTokensHeld = await sovryn.borrowingFeeTokensHeld.call(SUSD.address); - expect(borrowingFeeTokensHeld.toString()).to.be.equal(new BN(0).toString()); - } - - if (checkWRBTC) { - lendingFeeTokensHeld = await sovryn.lendingFeeTokensHeld.call(WRBTC.address); - expect(lendingFeeTokensHeld.toString()).to.be.equal(new BN(0).toString()); - tradingFeeTokensHeld = await sovryn.tradingFeeTokensHeld.call(WRBTC.address); - expect(tradingFeeTokensHeld.toString()).to.be.equal(new BN(0).toString()); - borrowingFeeTokensHeld = await sovryn.borrowingFeeTokensHeld.call(WRBTC.address); - expect(borrowingFeeTokensHeld.toString()).to.be.equal(new BN(0).toString()); - } - - if (checkSOV) { - protocolBalance = await SOVToken.balanceOf(sovryn.address); - expect(protocolBalance.toString()).to.be.equal(new BN(0).toString()); - lendingFeeTokensHeld = await sovryn.lendingFeeTokensHeld.call(SOVToken.address); - expect(lendingFeeTokensHeld.toString()).to.be.equal(new BN(0).toString()); - tradingFeeTokensHeld = await sovryn.tradingFeeTokensHeld.call(SOVToken.address); - expect(tradingFeeTokensHeld.toString()).to.be.equal(new BN(0).toString()); - borrowingFeeTokensHeld = await sovryn.borrowingFeeTokensHeld.call(SOVToken.address); - expect(borrowingFeeTokensHeld.toString()).to.be.equal(new BN(0).toString()); - } - } - - async function createCheckpoints(number) { - for (let i = 0; i < number; i++) { - await setFeeTokensHeld(new BN(100), new BN(200), new BN(300)); - await increaseTime(FEE_WITHDRAWAL_INTERVAL); - await feeSharingProxy.withdrawFees([SUSD.address]); - } - } - - async function createVestingContractWithSingleDate(cliff, amount, tokenOwner) { - vestingLogic = await VestingLogic.new(); - let vestingInstance = await Vesting.new( - vestingLogic.address, - SOVToken.address, - staking.address, - tokenOwner, - cliff, - cliff, - feeSharingProxy.address - ); - vestingInstance = await VestingLogic.at(vestingInstance.address); - // important, so it's recognized as vesting contract - await staking.addContractCodeHash(vestingInstance.address); - - await SOVToken.approve(vestingInstance.address, amount); - let result = await vestingInstance.stakeTokens(amount); - return { vestingInstance: vestingInstance, blockNumber: result.receipt.blockNumber }; - } + const name = "Test SOVToken"; + const symbol = "TST"; + + let root, account1, account2, account3, account4; + let SOVToken, SUSD, WRBTC, sovryn, staking; + let loanTokenSettings, loanTokenLogic, loanToken; + let feeSharingProxyObj; + let feeSharingProxy; + let feeSharingLogic; + let loanTokenWrbtc; + let tradingFeePercent; + let mockPrice; + let liquidityPoolV1Converter; + + before(async () => { + [root, account1, account2, account3, account4, ...accounts] = accounts; + }); + + async function protocolDeploymentFixture(_wallets, _provider) { + // Token + SOVToken = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); + + // Staking + let stakingLogic = await StakingLogic.new(SOVToken.address); + staking = await StakingProxy.new(SOVToken.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); + + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + + // Deploying sovrynProtocol w/ generic function from initializer.js + /// @dev Tried but no success so far. When using the getSovryn function + /// , contracts revert w/ "target not active" error. + /// The weird thing is that deployment code below is exactly the same as + /// the code from getSovryn function at initializer.js. + /// Inline code works ok, but when calling the function it does not. + // sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + // await sovryn.setSovrynProtocolAddress(sovryn.address); + + const sovrynproxy = await Protocol.new(); + sovryn = await ISovryn.at(sovrynproxy.address); + + await sovryn.replaceContract((await ProtocolSettings.new()).address); + await sovryn.replaceContract((await LoanSettings.new()).address); + await sovryn.replaceContract((await LoanMaintenance.new()).address); + await sovryn.replaceContract((await SwapsExternal.new()).address); + + await sovryn.setWrbtcToken(WRBTC.address); + + await sovryn.replaceContract((await LoanClosingsWith.new()).address); + await sovryn.replaceContract((await LoanClosingsLiquidation.new()).address); + await sovryn.replaceContract((await LoanClosingsRollover.new()).address); + + await sovryn.replaceContract((await Affiliates.new()).address); + + sovryn = await ProtocolSettings.at(sovryn.address); + + // Loan token + const initLoanTokenLogic = await getLoanTokenLogic(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] + loanTokenLogic = initLoanTokenLogic[0]; + loanTokenLogicBeacon = initLoanTokenLogic[1]; + + loanToken = await LoanToken.new( + root, + loanTokenLogic.address, + sovryn.address, + WRBTC.address + ); + await loanToken.initialize(SUSD.address, "iSUSD", "iSUSD"); + + /** Initialize the loan token logic proxy */ + loanToken = await ILoanTokenLogicProxy.at(loanToken.address); + await loanToken.setBeaconAddress(loanTokenLogicBeacon.address); + + /** Use interface of LoanTokenModules */ + loanToken = await ILoanTokenModules.at(loanToken.address); + + await loanToken.setAdmin(root); + await sovryn.setLoanPool([loanToken.address], [SUSD.address]); + + // FeeSharingProxy + feeSharingLogic = await FeeSharingLogic.new(); + feeSharingProxyObj = await FeeSharingProxy.new(sovryn.address, staking.address); + await feeSharingProxyObj.setImplementation(feeSharingLogic.address); + feeSharingProxy = await FeeSharingLogic.at(feeSharingProxyObj.address); + await sovryn.setFeesController(feeSharingProxy.address); + + // Set loan pool for wRBTC -- because our fee sharing proxy required the loanPool of wRBTC + loanTokenLogicWrbtc = await LoanTokenLogicWrbtc.new(); + loanTokenWrbtc = await LoanToken.new( + root, + loanTokenLogicWrbtc.address, + sovryn.address, + WRBTC.address + ); + await loanTokenWrbtc.initialize(WRBTC.address, "iWRBTC", "iWRBTC"); + + loanTokenWrbtc = await LoanTokenLogicWrbtc.at(loanTokenWrbtc.address); + const loanTokenAddressWrbtc = await loanTokenWrbtc.loanTokenAddress(); + await sovryn.setLoanPool([loanTokenWrbtc.address], [loanTokenAddressWrbtc]); + + await WRBTC.mint(sovryn.address, wei("500", "ether")); + + await sovryn.setWrbtcToken(WRBTC.address); + await sovryn.setSOVTokenAddress(SOVToken.address); + await sovryn.setSovrynProtocolAddress(sovryn.address); + + // Creating the Vesting Instance. + vestingLogic = await VestingLogic.new(); + vestingFactory = await VestingFactory.new(vestingLogic.address); + vestingRegistry = await VestingRegistry.new( + vestingFactory.address, + SOVToken.address, + staking.address, + feeSharingProxy.address, + root // This should be Governance Timelock Contract. + ); + vestingFactory.transferOwnership(vestingRegistry.address); + + await sovryn.setLockedSOVAddress( + ( + await LockedSOV.new(SOVToken.address, vestingRegistry.address, cliff, duration, [ + root, + ]) + ).address + ); + + // Set PriceFeeds + feeds = await PriceFeedsLocal.new(WRBTC.address, sovryn.address); + mockPrice = "1"; + await feeds.setRates(SUSD.address, WRBTC.address, wei(mockPrice, "ether")); + const swaps = await SwapsImplSovrynSwap.new(); + const sovrynSwapSimulator = await TestSovrynSwap.new(feeds.address); + await sovryn.setSovrynSwapContractRegistryAddress(sovrynSwapSimulator.address); + await sovryn.setSupportedTokens([SUSD.address, WRBTC.address], [true, true]); + await sovryn.setPriceFeedContract( + feeds.address // priceFeeds + ); + await sovryn.setSwapsImplContract( + swaps.address // swapsImpl + ); + + tradingFeePercent = await sovryn.tradingFeePercent(); + await lend_btc_before_cashout(loanTokenWrbtc, new BN(wei("10", "ether")), root); + + const maxDisagreement = new BN(wei("5", "ether")); + await sovryn.setMaxDisagreement(maxDisagreement); + + return sovryn; + } + + beforeEach(async () => { + await loadFixture(protocolDeploymentFixture); + }); + + describe("FeeSharingProxy", () => { + it("Check owner & implementation", async () => { + const proxyOwner = await feeSharingProxyObj.getProxyOwner(); + const implementation = await feeSharingProxyObj.getImplementation(); + + expect(implementation).to.be.equal(feeSharingLogic.address); + expect(proxyOwner).to.be.equal(root); + }); + + it("Set new implementation", async () => { + const newFeeSharingLogic = await FeeSharingLogic.new(); + await feeSharingProxyObj.setImplementation(newFeeSharingLogic.address); + const newImplementation = await feeSharingProxyObj.getImplementation(); + + expect(newImplementation).to.be.equal(newFeeSharingLogic.address); + }); + }); + + describe("withdrawFees", () => { + it("Shouldn't be able to use zero token address", async () => { + await protocolDeploymentFixture(); + await expectRevert( + feeSharingProxy.withdrawFees([ZERO_ADDRESS]), + "FeeSharingProxy::withdrawFees: token is not a contract" + ); + }); + + it("Shouldn't be able to withdraw if wRBTC loan pool does not exist", async () => { + await protocolDeploymentFixture(); + // Unset the loanPool for wRBTC + await sovryn.setLoanPool([loanTokenWrbtc.address], [ZERO_ADDRESS]); + + //mock data + let lendingFeeTokensHeld = new BN(wei("1", "ether")); + let tradingFeeTokensHeld = new BN(wei("2", "ether")); + let borrowingFeeTokensHeld = new BN(wei("3", "ether")); + let totalFeeTokensHeld = lendingFeeTokensHeld + .add(tradingFeeTokensHeld) + .add(borrowingFeeTokensHeld); + let feeAmount = await setFeeTokensHeld( + lendingFeeTokensHeld, + tradingFeeTokensHeld, + borrowingFeeTokensHeld, + true + ); + + await expectRevert( + feeSharingProxy.withdrawFees([WRBTC.address]), + "FeeSharingProxy::withdrawFees: loan wRBTC not found" + ); + }); + + it("Shouldn't be able to withdraw zero amount", async () => { + await protocolDeploymentFixture(); + const tx = await feeSharingProxy.withdrawFees([SUSD.address]); + expectEvent(tx, "FeeWithdrawn", { + sender: root, + token: loanTokenWrbtc.address, + amount: new BN(0), + }); + }); + + it("ProtocolSettings.withdrawFees", async () => { + /// @dev This test requires redeploying the protocol + const protocol = await protocolDeploymentFixture(); + + // stake - getPriorTotalVotingPower + let totalStake = 1000; + await stake(totalStake, root); + + // mock data + let lendingFeeTokensHeld = new BN(wei("1", "ether")); + let tradingFeeTokensHeld = new BN(wei("2", "ether")); + let borrowingFeeTokensHeld = new BN(wei("3", "ether")); + let totalFeeTokensHeld = lendingFeeTokensHeld + .add(tradingFeeTokensHeld) + .add(borrowingFeeTokensHeld); + + let feeAmount = await setFeeTokensHeld( + lendingFeeTokensHeld, + tradingFeeTokensHeld, + borrowingFeeTokensHeld + ); + let previousProtocolWrbtcBalance = await WRBTC.balanceOf(protocol.address); + // let feeAmount = await setFeeTokensHeld(new BN(100), new BN(200), new BN(300)); + await protocol.setFeesController(root); + let tx = await protocol.withdrawFees([SUSD.address], root); + let latestProtocolWrbtcBalance = await WRBTC.balanceOf(protocol.address); + + await checkWithdrawFee(); + + //check wrbtc balance (wrbt balance = (totalFeeTokensHeld * mockPrice) - swapFee) + let userBalance = await WRBTC.balanceOf.call(root); + expect(userBalance.toString()).to.be.equal(feeAmount.toString()); + + // wrbtc balance should remain the same + expect(previousProtocolWrbtcBalance.toString()).to.equal( + latestProtocolWrbtcBalance.toString() + ); + + expectEvent(tx, "WithdrawFees", { + sender: root, + token: SUSD.address, + receiver: root, + lendingAmount: lendingFeeTokensHeld, + tradingAmount: tradingFeeTokensHeld, + borrowingAmount: borrowingFeeTokensHeld, + // amountConvertedToWRBTC + }); + }); + + it("ProtocolSettings.withdrawFees (WRBTC token)", async () => { + /// @dev This test requires redeploying the protocol + await protocolDeploymentFixture(); + + //stake - getPriorTotalVotingPower + let totalStake = 1000; + await stake(totalStake, root); + + //mock data + let lendingFeeTokensHeld = new BN(wei("1", "ether")); + let tradingFeeTokensHeld = new BN(wei("2", "ether")); + let borrowingFeeTokensHeld = new BN(wei("3", "ether")); + let totalFeeTokensHeld = lendingFeeTokensHeld + .add(tradingFeeTokensHeld) + .add(borrowingFeeTokensHeld); + + let feeAmount = await setFeeTokensHeld( + lendingFeeTokensHeld, + tradingFeeTokensHeld, + borrowingFeeTokensHeld, + true + ); + // let feeAmount = await setFeeTokensHeld(new BN(100), new BN(200), new BN(300)); + await sovryn.setFeesController(root); + let tx = await sovryn.withdrawFees([WRBTC.address], account1); + + await checkWithdrawFee(true, true, false); + + //check WRBTC balance (wrbt balance = (totalFeeTokensHeld * mockPrice) - swapFee) + let userBalance = await WRBTC.balanceOf.call(account1); + expect(userBalance.toString()).to.be.equal(feeAmount.toString()); + + expectEvent(tx, "WithdrawFees", { + sender: root, + token: WRBTC.address, + receiver: account1, + lendingAmount: lendingFeeTokensHeld, + tradingAmount: tradingFeeTokensHeld, + borrowingAmount: borrowingFeeTokensHeld, + wRBTCConverted: new BN(feeAmount), + }); + }); + + /// @dev Test coverage + it("ProtocolSettings.withdrawFees: Revert withdrawing by no feesController", async () => { + /// @dev This test requires redeploying the protocol + await protocolDeploymentFixture(); + + // stake - getPriorTotalVotingPower + let totalStake = 1000; + await stake(totalStake, root); + + // mock data + let feeAmount = await setFeeTokensHeld(new BN(100), new BN(200), new BN(300)); + + await sovryn.setFeesController(root); + + await expectRevert( + sovryn.withdrawFees([SUSD.address], account1, { from: account1 }), + "unauthorized" + ); + }); + + it("Should be able to withdraw fees", async () => { + /// @dev This test requires redeploying the protocol + const protocol = await protocolDeploymentFixture(); + + // stake - getPriorTotalVotingPower + let totalStake = 1000; + await stake(totalStake, root); + + // mock data + let lendingFeeTokensHeld = new BN(wei("1", "ether")); + let tradingFeeTokensHeld = new BN(wei("2", "ether")); + let borrowingFeeTokensHeld = new BN(wei("3", "ether")); + let totalFeeTokensHeld = lendingFeeTokensHeld + .add(tradingFeeTokensHeld) + .add(borrowingFeeTokensHeld); + let feeAmount = await setFeeTokensHeld( + lendingFeeTokensHeld, + tradingFeeTokensHeld, + borrowingFeeTokensHeld + ); + let previousProtocolWrbtcBalance = await WRBTC.balanceOf(protocol.address); + + tx = await feeSharingProxy.withdrawFees([SUSD.address]); + + await checkWithdrawFee(); + + //check irbtc balance (wrbt balance = (totalFeeTokensHeld * mockPrice) - swapFee) + let feeSharingProxyBalance = await loanTokenWrbtc.balanceOf.call( + feeSharingProxy.address + ); + expect(feeSharingProxyBalance.toString()).to.be.equal(feeAmount.toString()); + + // make sure wrbtc balance is 0 after withdrawal + let feeSharingProxyWRBTCBalance = await WRBTC.balanceOf.call(feeSharingProxy.address); + expect(feeSharingProxyWRBTCBalance.toString()).to.be.equal(new BN(0).toString()); + + // wrbtc balance should remain the same + let latestProtocolWrbtcBalance = await WRBTC.balanceOf(protocol.address); + expect(previousProtocolWrbtcBalance.toString()).to.equal( + latestProtocolWrbtcBalance.toString() + ); + + //checkpoints + let numTokenCheckpoints = await feeSharingProxy.numTokenCheckpoints.call( + loanTokenWrbtc.address + ); + expect(numTokenCheckpoints.toNumber()).to.be.equal(1); + let checkpoint = await feeSharingProxy.tokenCheckpoints.call( + loanTokenWrbtc.address, + 0 + ); + expect(checkpoint.blockNumber.toNumber()).to.be.equal(tx.receipt.blockNumber); + expect(checkpoint.totalWeightedStake.toNumber()).to.be.equal( + totalStake * MAX_VOTING_WEIGHT + ); + expect(checkpoint.numTokens.toString()).to.be.equal(feeAmount.toString()); + + // check lastFeeWithdrawalTime + let lastFeeWithdrawalTime = await feeSharingProxy.lastFeeWithdrawalTime.call( + loanTokenWrbtc.address + ); + let block = await web3.eth.getBlock(tx.receipt.blockNumber); + expect(lastFeeWithdrawalTime.toString()).to.be.equal(block.timestamp.toString()); + + expectEvent(tx, "FeeWithdrawn", { + sender: root, + token: loanTokenWrbtc.address, + amount: feeAmount, + }); + }); + + it("Should be able to withdraw fees (WRBTC token)", async () => { + /// @dev This test requires redeploying the protocol + await protocolDeploymentFixture(); + + //stake - getPriorTotalVotingPower + let totalStake = 1000; + await stake(totalStake, root); + + //mock data + let lendingFeeTokensHeld = new BN(wei("1", "ether")); + let tradingFeeTokensHeld = new BN(wei("2", "ether")); + let borrowingFeeTokensHeld = new BN(wei("3", "ether")); + let totalFeeTokensHeld = lendingFeeTokensHeld + .add(tradingFeeTokensHeld) + .add(borrowingFeeTokensHeld); + let feeAmount = await setFeeTokensHeld( + lendingFeeTokensHeld, + tradingFeeTokensHeld, + borrowingFeeTokensHeld, + true + ); + + tx = await feeSharingProxy.withdrawFees([WRBTC.address]); + + await checkWithdrawFee(); + + //check irbtc balance (wrbt balance = (totalFeeTokensHeld * mockPrice) - swapFee) + let feeSharingProxyBalance = await loanTokenWrbtc.balanceOf.call( + feeSharingProxy.address + ); + expect(feeSharingProxyBalance.toString()).to.be.equal(feeAmount.toString()); + + // make sure wrbtc balance is 0 after withdrawal + let feeSharingProxyWRBTCBalance = await WRBTC.balanceOf.call(feeSharingProxy.address); + expect(feeSharingProxyWRBTCBalance.toString()).to.be.equal(new BN(0).toString()); + + //checkpoints + let numTokenCheckpoints = await feeSharingProxy.numTokenCheckpoints.call( + loanTokenWrbtc.address + ); + expect(numTokenCheckpoints.toNumber()).to.be.equal(1); + let checkpoint = await feeSharingProxy.tokenCheckpoints.call( + loanTokenWrbtc.address, + 0 + ); + expect(checkpoint.blockNumber.toNumber()).to.be.equal(tx.receipt.blockNumber); + expect(checkpoint.totalWeightedStake.toNumber()).to.be.equal( + totalStake * MAX_VOTING_WEIGHT + ); + expect(checkpoint.numTokens.toString()).to.be.equal(feeAmount.toString()); + + //check lastFeeWithdrawalTime + let lastFeeWithdrawalTime = await feeSharingProxy.lastFeeWithdrawalTime.call( + loanTokenWrbtc.address + ); + let block = await web3.eth.getBlock(tx.receipt.blockNumber); + expect(lastFeeWithdrawalTime.toString()).to.be.equal(block.timestamp.toString()); + + expectEvent(tx, "FeeWithdrawn", { + sender: root, + token: loanTokenWrbtc.address, + amount: feeAmount, + }); + }); + + it("Should be able to withdraw fees (sov token)", async () => { + /// @dev This test requires redeploying the protocol + await protocolDeploymentFixture(); + + //stake - getPriorTotalVotingPower + let totalStake = 1000; + await stake(totalStake, root); + + //mock data + let lendingFeeTokensHeld = new BN(wei("1", "ether")); + let tradingFeeTokensHeld = new BN(wei("2", "ether")); + let borrowingFeeTokensHeld = new BN(wei("3", "ether")); + let totalFeeTokensHeld = lendingFeeTokensHeld + .add(tradingFeeTokensHeld) + .add(borrowingFeeTokensHeld); + let feeAmount = await setFeeTokensHeld( + lendingFeeTokensHeld, + tradingFeeTokensHeld, + borrowingFeeTokensHeld, + false, + true + ); + tx = await feeSharingProxy.withdrawFees([SOVToken.address]); + + await checkWithdrawFee(false, false, true); + + //check WRBTC balance (wrbt balance = (totalFeeTokensHeld * mockPrice) - swapFee) + let feeSharingProxyBalance = await SOVToken.balanceOf.call(feeSharingProxy.address); + expect(feeSharingProxyBalance.toString()).to.be.equal(feeAmount.toString()); + + // make sure wrbtc balance is 0 after withdrawal + let feeSharingProxyWRBTCBalance = await WRBTC.balanceOf.call(feeSharingProxy.address); + expect(feeSharingProxyWRBTCBalance.toString()).to.be.equal(new BN(0).toString()); + + //checkpoints + let numTokenCheckpoints = await feeSharingProxy.numTokenCheckpoints.call( + SOVToken.address + ); + expect(numTokenCheckpoints.toNumber()).to.be.equal(1); + let checkpoint = await feeSharingProxy.tokenCheckpoints.call(SOVToken.address, 0); + expect(checkpoint.blockNumber.toNumber()).to.be.equal(tx.receipt.blockNumber); + expect(checkpoint.totalWeightedStake.toNumber()).to.be.equal( + totalStake * MAX_VOTING_WEIGHT + ); + expect(checkpoint.numTokens.toString()).to.be.equal(feeAmount.toString()); + + //check lastFeeWithdrawalTime + let lastFeeWithdrawalTime = await feeSharingProxy.lastFeeWithdrawalTime.call( + SOVToken.address + ); + let block = await web3.eth.getBlock(tx.receipt.blockNumber); + expect(lastFeeWithdrawalTime.toString()).to.be.equal(block.timestamp.toString()); + + expectEvent(tx, "TokensTransferred", { + sender: sovryn.address, + token: SOVToken.address, + amount: feeAmount, + }); + }); + + it("Should be able to withdraw fees 3 times", async () => { + /// @dev This test requires redeploying the protocol + await protocolDeploymentFixture(); + + // stake - getPriorTotalVotingPower + let totalStake = 1000; + await stake(1000, root); + + // [FIRST] + // mock data + let mockAmountLendingFeeTokensHeld = 0; + let mockAmountTradingFeeTokensHeld = 1; + let mockAmountBorrowingFeeTokensHeld = 2; + let totalMockAmount1 = + mockAmountLendingFeeTokensHeld + + mockAmountTradingFeeTokensHeld + + mockAmountBorrowingFeeTokensHeld; + let lendingFeeTokensHeld = new BN(mockAmountLendingFeeTokensHeld); + let tradingFeeTokensHeld = new BN( + wei(mockAmountTradingFeeTokensHeld.toString(), "ether") + ); + let borrowingFeeTokensHeld = new BN( + wei(mockAmountBorrowingFeeTokensHeld.toString(), "ether") + ); + let totalFeeTokensHeld = lendingFeeTokensHeld + .add(tradingFeeTokensHeld) + .add(borrowingFeeTokensHeld); + let feeAmount = await setFeeTokensHeld( + lendingFeeTokensHeld, + tradingFeeTokensHeld, + borrowingFeeTokensHeld + ); + let totalFeeAmount = feeAmount; + + let tx = await feeSharingProxy.withdrawFees([SUSD.address]); + + await checkWithdrawFee(); + + // check WRBTC balance (wrbt balance = (totalFeeTokensHeld * mockPrice) - swapFee) + let feeSharingProxyBalance = await loanTokenWrbtc.balanceOf.call( + feeSharingProxy.address + ); + expect(feeSharingProxyBalance.toString()).to.be.equal(feeAmount.toString()); + + // checkpoints + let numTokenCheckpoints = await feeSharingProxy.numTokenCheckpoints.call( + loanTokenWrbtc.address + ); + expect(numTokenCheckpoints.toNumber()).to.be.equal(1); + let checkpoint = await feeSharingProxy.tokenCheckpoints.call( + loanTokenWrbtc.address, + 0 + ); + expect(checkpoint.blockNumber.toNumber()).to.be.equal(tx.receipt.blockNumber); + expect(checkpoint.totalWeightedStake.toNumber()).to.be.equal( + totalStake * MAX_VOTING_WEIGHT + ); + expect(checkpoint.numTokens.toString()).to.be.equal(feeAmount.toString()); + + // check lastFeeWithdrawalTime + let lastFeeWithdrawalTime = await feeSharingProxy.lastFeeWithdrawalTime.call( + loanTokenWrbtc.address + ); + let block = await web3.eth.getBlock(tx.receipt.blockNumber); + expect(lastFeeWithdrawalTime.toString()).to.be.equal(block.timestamp.toString()); + + // [SECOND] + // mock data + let mockAmountLendingFeeTokensHeld2 = 1; + let mockAmountTradingFeeTokensHeld2 = 0; + let mockAmountBorrowingFeeTokensHeld2 = 0; + let totalMockAmount2 = + mockAmountTradingFeeTokensHeld2 + + mockAmountBorrowingFeeTokensHeld2 + + mockAmountLendingFeeTokensHeld2; + lendingFeeTokensHeld = new BN( + wei(mockAmountLendingFeeTokensHeld2.toString(), "ether") + ); + tradingFeeTokensHeld = new BN(mockAmountTradingFeeTokensHeld2); + borrowingFeeTokensHeld = new BN(mockAmountBorrowingFeeTokensHeld2); + totalFeeTokensHeld = lendingFeeTokensHeld + .add(tradingFeeTokensHeld) + .add(borrowingFeeTokensHeld); + feeAmount = await setFeeTokensHeld( + lendingFeeTokensHeld, + tradingFeeTokensHeld, + borrowingFeeTokensHeld + ); + let unprocessedAmount = feeAmount; + totalFeeAmount = totalFeeAmount.add(feeAmount); + + tx = await feeSharingProxy.withdrawFees([SUSD.address]); + + // Need to checkwithdrawfee manually + await checkWithdrawFee(); + + // check WRBTC balance (wrbt balance = (totalFeeTokensHeld * mockPrice) - swapFee) + feeSharingProxyBalance = await loanTokenWrbtc.balanceOf.call(feeSharingProxy.address); + expect(feeSharingProxyBalance.toString()).to.be.equal(totalFeeAmount.toString()); + + // [THIRD] + // mock data + let mockAmountLendingFeeTokensHeld3 = 0; + let mockAmountTradingFeeTokensHeld3 = 0.5; + let mockAmountBorrowingFeeTokensHeld3 = 0.5; + let totalMockAmount3 = + mockAmountTradingFeeTokensHeld3 + + mockAmountBorrowingFeeTokensHeld3 + + mockAmountLendingFeeTokensHeld3; + lendingFeeTokensHeld = new BN(mockAmountLendingFeeTokensHeld3); + tradingFeeTokensHeld = new BN( + wei(mockAmountTradingFeeTokensHeld3.toString(), "ether") + ); + borrowingFeeTokensHeld = new BN( + wei(mockAmountBorrowingFeeTokensHeld3.toString(), "ether") + ); + totalFeeTokensHeld = lendingFeeTokensHeld + .add(tradingFeeTokensHeld) + .add(borrowingFeeTokensHeld); + feeAmount = await setFeeTokensHeld( + lendingFeeTokensHeld, + tradingFeeTokensHeld, + borrowingFeeTokensHeld + ); + totalFeeAmount = totalFeeAmount.add(feeAmount); + + await increaseTime(FEE_WITHDRAWAL_INTERVAL); + tx = await feeSharingProxy.withdrawFees([SUSD.address]); + // In this state the price of SUSD/WRBTC already adjusted because of previous swap, so we need to consider this in the next swapFee calculation + await checkWithdrawFee(); + + // check WRBTC balance (wrbt balance = (totalFeeTokensHeld * mockPrice) - swapFee) + feeSharingProxyBalance = await loanTokenWrbtc.balanceOf.call(feeSharingProxy.address); + expect(feeSharingProxyBalance.toString()).to.be.equal(totalFeeAmount.toString()); + + // checkpoints + numTokenCheckpoints = await feeSharingProxy.numTokenCheckpoints.call( + loanTokenWrbtc.address + ); + expect(numTokenCheckpoints.toNumber()).to.be.equal(2); + checkpoint = await feeSharingProxy.tokenCheckpoints.call(loanTokenWrbtc.address, 1); + expect(checkpoint.blockNumber.toNumber()).to.be.equal(tx.receipt.blockNumber); + expect(checkpoint.totalWeightedStake.toNumber()).to.be.equal( + totalStake * MAX_VOTING_WEIGHT + ); + expect(checkpoint.numTokens.toString()).to.be.equal( + feeAmount.add(unprocessedAmount).toString() + ); + + // check lastFeeWithdrawalTime + lastFeeWithdrawalTime = await feeSharingProxy.lastFeeWithdrawalTime.call( + loanTokenWrbtc.address + ); + block = await web3.eth.getBlock(tx.receipt.blockNumber); + expect(lastFeeWithdrawalTime.toString()).to.be.equal(block.timestamp.toString()); + + // make sure wrbtc balance is 0 after withdrawal + let feeSharingProxyWRBTCBalance = await WRBTC.balanceOf.call(feeSharingProxy.address); + expect(feeSharingProxyWRBTCBalance.toString()).to.be.equal(new BN(0).toString()); + }); + }); + + describe("transferTokens", () => { + it("Shouldn't be able to use zero token address", async () => { + await protocolDeploymentFixture(); + await expectRevert( + feeSharingProxy.transferTokens(ZERO_ADDRESS, 1000), + "FeeSharingProxy::transferTokens: invalid address" + ); + }); + + it("Shouldn't be able to transfer zero amount", async () => { + await protocolDeploymentFixture(); + await expectRevert( + feeSharingProxy.transferTokens(SOVToken.address, 0), + "FeeSharingProxy::transferTokens: invalid amount" + ); + }); + + it("Shouldn't be able to withdraw zero amount", async () => { + await protocolDeploymentFixture(); + await expectRevert( + feeSharingProxy.transferTokens(SOVToken.address, 1000), + "invalid transfer" + ); + }); + + it("Should be able to transfer tokens", async () => { + await protocolDeploymentFixture(); + // stake - getPriorTotalVotingPower + let totalStake = 1000; + await stake(totalStake, root); + + let amount = 1000; + await SOVToken.approve(feeSharingProxy.address, amount * 7); + + let tx = await feeSharingProxy.transferTokens(SOVToken.address, amount); + + expect( + await feeSharingProxy.unprocessedAmount.call(SOVToken.address) + ).to.be.bignumber.equal(new BN(0)); + + expectEvent(tx, "TokensTransferred", { + sender: root, + token: SOVToken.address, + amount: new BN(amount), + }); + + // checkpoints + let numTokenCheckpoints = await feeSharingProxy.numTokenCheckpoints.call( + SOVToken.address + ); + expect(numTokenCheckpoints.toNumber()).to.be.equal(1); + let checkpoint = await feeSharingProxy.tokenCheckpoints.call(SOVToken.address, 0); + expect(checkpoint.blockNumber.toNumber()).to.be.equal(tx.receipt.blockNumber); + expect(checkpoint.totalWeightedStake.toNumber()).to.be.equal( + totalStake * MAX_VOTING_WEIGHT + ); + expect(checkpoint.numTokens.toString()).to.be.equal(amount.toString()); + + // check lastFeeWithdrawalTime + let lastFeeWithdrawalTime = await feeSharingProxy.lastFeeWithdrawalTime.call( + SOVToken.address + ); + let block = await web3.eth.getBlock(tx.receipt.blockNumber); + expect(lastFeeWithdrawalTime.toString()).to.be.equal(block.timestamp.toString()); + + expectEvent(tx, "CheckpointAdded", { + sender: root, + token: SOVToken.address, + amount: new BN(amount), + }); + + // second time + tx = await feeSharingProxy.transferTokens(SOVToken.address, amount * 2); + + expect( + await feeSharingProxy.unprocessedAmount.call(SOVToken.address) + ).to.be.bignumber.equal(new BN(amount * 2)); + + expectEvent(tx, "TokensTransferred", { + sender: root, + token: SOVToken.address, + amount: new BN(amount * 2), + }); + + await increaseTime(FEE_WITHDRAWAL_INTERVAL); + // third time + tx = await feeSharingProxy.transferTokens(SOVToken.address, amount * 4); + + expect( + await feeSharingProxy.unprocessedAmount.call(SOVToken.address) + ).to.be.bignumber.equal(new BN(0)); + + // checkpoints + numTokenCheckpoints = await feeSharingProxy.numTokenCheckpoints.call(SOVToken.address); + expect(numTokenCheckpoints.toNumber()).to.be.equal(2); + checkpoint = await feeSharingProxy.tokenCheckpoints.call(SOVToken.address, 1); + expect(checkpoint.blockNumber.toNumber()).to.be.equal(tx.receipt.blockNumber); + expect(checkpoint.totalWeightedStake.toNumber()).to.be.equal( + totalStake * MAX_VOTING_WEIGHT + ); + expect(checkpoint.numTokens.toNumber()).to.be.equal(amount * 6); + + // check lastFeeWithdrawalTime + lastFeeWithdrawalTime = await feeSharingProxy.lastFeeWithdrawalTime.call( + SOVToken.address + ); + block = await web3.eth.getBlock(tx.receipt.blockNumber); + expect(lastFeeWithdrawalTime.toString()).to.be.equal(block.timestamp.toString()); + }); + }); + + describe("withdraw", () => { + it("Shouldn't be able to withdraw without checkpoints (for token pool)", async () => { + await protocolDeploymentFixture(); + await expectRevert( + feeSharingProxy.withdraw(loanToken.address, 0, account2, { from: account1 }), + "FeeSharingProxy::withdraw: _maxCheckpoints should be positive" + ); + }); + + it("Shouldn't be able to withdraw without checkpoints (for wRBTC pool)", async () => { + await protocolDeploymentFixture(); + await expectRevert( + feeSharingProxy.withdraw(loanTokenWrbtc.address, 0, account2, { from: account1 }), + "FeeSharingProxy::withdraw: _maxCheckpoints should be positive" + ); + }); + + it("Shouldn't be able to withdraw zero amount (for token pool)", async () => { + await protocolDeploymentFixture(); + let fees = await feeSharingProxy.getAccumulatedFees(account1, loanToken.address); + expect(fees).to.be.bignumber.equal("0"); + + await expectRevert( + feeSharingProxy.withdraw(loanToken.address, 10, ZERO_ADDRESS, { from: account1 }), + "FeeSharingProxy::withdrawFees: no tokens for a withdrawal" + ); + }); + + it("Shouldn't be able to withdraw zero amount (for wRBTC pool)", async () => { + await protocolDeploymentFixture(); + let fees = await feeSharingProxy.getAccumulatedFees(account1, loanTokenWrbtc.address); + expect(fees).to.be.bignumber.equal("0"); + + await expectRevert( + feeSharingProxy.withdraw(loanTokenWrbtc.address, 10, ZERO_ADDRESS, { + from: account1, + }), + "FeeSharingProxy::withdrawFees: no tokens for a withdrawal" + ); + }); + + it("Should be able to withdraw to another account", async () => { + await protocolDeploymentFixture(); + // stake - getPriorTotalVotingPower + let rootStake = 700; + await stake(rootStake, root); + + let userStake = 300; + if (MOCK_PRIOR_WEIGHTED_STAKE) { + await staking.MOCK_priorWeightedStake(userStake * 10); + } + await SOVToken.transfer(account1, userStake); + await stake(userStake, account1); + + // mock data + let lendingFeeTokensHeld = new BN(wei("1", "ether")); + let tradingFeeTokensHeld = new BN(wei("2", "ether")); + let borrowingFeeTokensHeld = new BN(wei("3", "ether")); + let totalFeeTokensHeld = lendingFeeTokensHeld + .add(tradingFeeTokensHeld) + .add(borrowingFeeTokensHeld); + let feeAmount = await setFeeTokensHeld( + lendingFeeTokensHeld, + tradingFeeTokensHeld, + borrowingFeeTokensHeld + ); + + await feeSharingProxy.withdrawFees([SUSD.address]); + + let fees = await feeSharingProxy.getAccumulatedFees(account1, loanTokenWrbtc.address); + expect(fees).to.be.bignumber.equal(new BN(feeAmount).mul(new BN(3)).div(new BN(10))); + + let tx = await feeSharingProxy.withdraw(loanTokenWrbtc.address, 1000, account2, { + from: account1, + }); + + // processedCheckpoints + let processedCheckpoints = await feeSharingProxy.processedCheckpoints.call( + account1, + loanTokenWrbtc.address + ); + expect(processedCheckpoints.toNumber()).to.be.equal(1); + + expectEvent(tx, "UserFeeWithdrawn", { + sender: account1, + receiver: account2, + token: loanTokenWrbtc.address, + amount: new BN(feeAmount).mul(new BN(3)).div(new BN(10)), + }); + }); + + it("Should be able to withdraw (token pool)", async () => { + await protocolDeploymentFixture(); + // FeeSharingProxy + feeSharingProxy = await FeeSharingProxyMockup.new(sovryn.address, staking.address); + await sovryn.setFeesController(feeSharingProxy.address); + + // stake - getPriorTotalVotingPower + let rootStake = 700; + await stake(rootStake, root); + + let userStake = 300; + if (MOCK_PRIOR_WEIGHTED_STAKE) { + await staking.MOCK_priorWeightedStake(userStake * 10); + } + await SOVToken.transfer(account1, userStake); + await stake(userStake, account1); + + // Mock (transfer loanToken to FeeSharingProxy contract) + const loanPoolTokenAddress = await sovryn.underlyingToLoanPool(SUSD.address); + const amountLend = new BN(wei("500", "ether")); + await SUSD.approve(loanPoolTokenAddress, amountLend); + await loanToken.mint(feeSharingProxy.address, amountLend); + + // Check ISUSD Balance for feeSharingProxy + const feeSharingProxyLoanBalanceToken = await loanToken.balanceOf( + feeSharingProxy.address + ); + expect(feeSharingProxyLoanBalanceToken.toString()).to.be.equal(amountLend.toString()); + + // Withdraw ISUSD from feeSharingProxy + // const initial + await feeSharingProxy.addCheckPoint(loanPoolTokenAddress, amountLend.toString()); + let tx = await feeSharingProxy.trueWithdraw(loanToken.address, 10, ZERO_ADDRESS, { + from: account1, + }); + const updatedFeeSharingProxyLoanBalanceToken = await loanToken.balanceOf( + feeSharingProxy.address + ); + const updatedAccount1LoanBalanceToken = await loanToken.balanceOf(account1); + console.log("\nwithdraw(checkpoints = 1).gasUsed: " + tx.receipt.gasUsed); + + expect(updatedFeeSharingProxyLoanBalanceToken.toString()).to.be.equal( + ((amountLend * 7) / 10).toString() + ); + expect(updatedAccount1LoanBalanceToken.toString()).to.be.equal( + ((amountLend * 3) / 10).toString() + ); + + expectEvent(tx, "UserFeeWithdrawn", { + sender: account1, + receiver: account1, + token: loanToken.address, + amount: amountLend.mul(new BN(3)).div(new BN(10)), + }); + }); + + it("Should be able to withdraw (WRBTC pool)", async () => { + /// @dev This test requires redeploying the protocol + await protocolDeploymentFixture(); + + // stake - getPriorTotalVotingPower + let rootStake = 700; + await stake(rootStake, root); + + let userStake = 300; + if (MOCK_PRIOR_WEIGHTED_STAKE) { + await staking.MOCK_priorWeightedStake(userStake * 10); + } + await SOVToken.transfer(account1, userStake); + await stake(userStake, account1); + + // mock data + let lendingFeeTokensHeld = new BN(wei("1", "gwei")); + let tradingFeeTokensHeld = new BN(wei("2", "gwei")); + let borrowingFeeTokensHeld = new BN(wei("3", "gwei")); + let totalFeeTokensHeld = lendingFeeTokensHeld + .add(tradingFeeTokensHeld) + .add(borrowingFeeTokensHeld); + let feeAmount = await setFeeTokensHeld( + lendingFeeTokensHeld, + tradingFeeTokensHeld, + borrowingFeeTokensHeld + ); + + await feeSharingProxy.withdrawFees([SUSD.address]); + + let fees = await feeSharingProxy.getAccumulatedFees(account1, loanTokenWrbtc.address); + expect(fees).to.be.bignumber.equal(feeAmount.mul(new BN(3)).div(new BN(10))); + + let userInitialBtcBalance = new BN(await web3.eth.getBalance(account1)); + let tx = await feeSharingProxy.withdraw(loanTokenWrbtc.address, 10, ZERO_ADDRESS, { + from: account1, + }); + + /// @dev To anticipate gas consumption it is required to split hardhat + /// behaviour into two different scenarios: coverage and regular testing. + /// On coverage gasPrice = 1, on regular tests gasPrice = 8000000000 + // + // On coverage: + // Fees: 1800000000 + // Balance: 10000000000000000000000 + // Balance: 10000000000001799398877 + // withdraw().gasUsed: 601123 + // txFee: 601123 + // + // On regular test: + // Fees: 1800000000 + // Balance: 10000000000000000000000 + // Balance: 9999996433281800000000 + // withdraw().gasUsed: 445840 + // txFee: 3566720000000000 + let userLatestBTCBalance = new BN(await web3.eth.getBalance(account1)); + let gasPrice; + /// @dev A balance decrease (negative difference) corresponds to regular test case + if (userLatestBTCBalance.sub(userInitialBtcBalance).toString()[0] == "-") { + gasPrice = new BN(8000000000); + } // regular test + else { + gasPrice = new BN(1); + } // coverage + + console.log("\nwithdraw(checkpoints = 1).gasUsed: " + tx.receipt.gasUsed); + let txFee = new BN(tx.receipt.gasUsed).mul(gasPrice); + + userInitialBtcBalance = userInitialBtcBalance.sub(new BN(txFee)); + // processedCheckpoints + let processedCheckpoints = await feeSharingProxy.processedCheckpoints.call( + account1, + loanTokenWrbtc.address + ); + expect(processedCheckpoints.toNumber()).to.be.equal(1); + + // check balances + let feeSharingProxyBalance = await loanTokenWrbtc.balanceOf.call( + feeSharingProxy.address + ); + expect(feeSharingProxyBalance.toNumber()).to.be.equal((feeAmount * 7) / 10); + let userLoanTokenBalance = await loanTokenWrbtc.balanceOf.call(account1); + expect(userLoanTokenBalance.toNumber()).to.be.equal(0); + let userExpectedBtcBalance = userInitialBtcBalance.add( + feeAmount.mul(new BN(3)).div(new BN(10)) + ); + expect(userLatestBTCBalance.toString()).to.be.equal(userExpectedBtcBalance.toString()); + + expectEvent(tx, "UserFeeWithdrawn", { + sender: account1, + receiver: account1, + token: loanTokenWrbtc.address, + amount: feeAmount.mul(new BN(3)).div(new BN(10)), + }); + }); + + it("Should be able to withdraw (sov pool)", async () => { + /// @dev This test requires redeploying the protocol + await protocolDeploymentFixture(); + + //stake - getPriorTotalVotingPower + let rootStake = 700; + await stake(rootStake, root); + + let userStake = 300; + if (MOCK_PRIOR_WEIGHTED_STAKE) { + await staking.MOCK_priorWeightedStake(userStake * 10); + } + await SOVToken.transfer(account1, userStake); + await stake(userStake, account1); + + //mock data + let lendingFeeTokensHeld = new BN(wei("1", "gwei")); + let tradingFeeTokensHeld = new BN(wei("2", "gwei")); + let borrowingFeeTokensHeld = new BN(wei("3", "gwei")); + let totalFeeTokensHeld = lendingFeeTokensHeld + .add(tradingFeeTokensHeld) + .add(borrowingFeeTokensHeld); + let feeAmount = await setFeeTokensHeld( + lendingFeeTokensHeld, + tradingFeeTokensHeld, + borrowingFeeTokensHeld, + false, + true + ); + + await feeSharingProxy.withdrawFees([SOVToken.address]); + + let fees = await feeSharingProxy.getAccumulatedFees(account1, SOVToken.address); + expect(fees).to.be.bignumber.equal(feeAmount.mul(new BN(3)).div(new BN(10))); + + let userInitialISOVBalance = await SOVToken.balanceOf(account1); + let tx = await feeSharingProxy.withdraw(SOVToken.address, 10, ZERO_ADDRESS, { + from: account1, + }); + + //processedCheckpoints + let processedCheckpoints = await feeSharingProxy.processedCheckpoints.call( + account1, + SOVToken.address + ); + expect(processedCheckpoints.toNumber()).to.be.equal(1); + + //check balances + let feeSharingProxyBalance = await SOVToken.balanceOf.call(feeSharingProxy.address); + expect(feeSharingProxyBalance.toNumber()).to.be.equal((feeAmount * 7) / 10); + let userBalance = await SOVToken.balanceOf.call(account1); + expect(userBalance.sub(userInitialISOVBalance).toNumber()).to.be.equal( + (feeAmount * 3) / 10 + ); + + expectEvent(tx, "UserFeeWithdrawn", { + sender: account1, + receiver: account1, + token: SOVToken.address, + amount: new BN(feeAmount).mul(new BN(3)).div(new BN(10)), + }); + }); + + it("Should be able to withdraw using 3 checkpoints", async () => { + /// @dev This test requires redeploying the protocol + await protocolDeploymentFixture(); + + // stake - getPriorTotalVotingPower + let rootStake = 900; + await stake(rootStake, root); + + let userStake = 100; + if (MOCK_PRIOR_WEIGHTED_STAKE) { + await staking.MOCK_priorWeightedStake(userStake * 10); + } + await SOVToken.transfer(account1, userStake); + await stake(userStake, account1); + + // [FIRST] + // mock data + let lendingFeeTokensHeld = new BN(wei("1", "gwei")); + let tradingFeeTokensHeld = new BN(wei("2", "gwei")); + let borrowingFeeTokensHeld = new BN(wei("3", "gwei")); + let totalFeeTokensHeld = lendingFeeTokensHeld + .add(tradingFeeTokensHeld) + .add(borrowingFeeTokensHeld); + let feeAmount = await setFeeTokensHeld( + lendingFeeTokensHeld, + tradingFeeTokensHeld, + borrowingFeeTokensHeld + ); + let totalFeeAmount = feeAmount; + await feeSharingProxy.withdrawFees([SUSD.address]); + + let userInitialBtcBalance = new BN(await web3.eth.getBalance(account1)); + let tx = await feeSharingProxy.withdraw(loanTokenWrbtc.address, 1, ZERO_ADDRESS, { + from: account1, + }); + + /// @dev Same as above gas consumption is different on regular tests than on coverge + let userLatestBTCBalance = new BN(await web3.eth.getBalance(account1)); + let gasPrice; + /// @dev A balance decrease (negative difference) corresponds to regular test case + if (userLatestBTCBalance.sub(userInitialBtcBalance).toString()[0] == "-") { + gasPrice = new BN(8000000000); + } // regular test + else { + gasPrice = new BN(1); + } // coverage + + console.log("\nwithdraw(checkpoints = 1).gasUsed: " + tx.receipt.gasUsed); + let txFee = new BN(tx.receipt.gasUsed).mul(gasPrice); + + userInitialBtcBalance = userInitialBtcBalance.sub(new BN(txFee)); + // processedCheckpoints + let processedCheckpoints = await feeSharingProxy.processedCheckpoints.call( + account1, + loanTokenWrbtc.address + ); + expect(processedCheckpoints.toNumber()).to.be.equal(1); + + // check balances + let feeSharingProxyBalance = await loanTokenWrbtc.balanceOf.call( + feeSharingProxy.address + ); + expect(feeSharingProxyBalance.toNumber()).to.be.equal((totalFeeAmount * 9) / 10); + let userBalance = await loanTokenWrbtc.balanceOf.call(account1); + expect(userBalance.toNumber()).to.be.equal(0); + + expect(userLatestBTCBalance.toString()).to.be.equal( + userInitialBtcBalance.add(totalFeeAmount.mul(new BN(1)).div(new BN(10))).toString() + ); + + // [SECOND] + // mock data + let lendingFeeTokensHeld2 = new BN(wei("1", "gwei")); + let tradingFeeTokensHeld2 = new BN(wei("2", "gwei")); + let borrowingFeeTokensHeld2 = new BN(wei("3", "gwei")); + totalFeeTokensHeld = lendingFeeTokensHeld2 + .add(tradingFeeTokensHeld2) + .add(borrowingFeeTokensHeld2); + feeAmount = await setFeeTokensHeld( + lendingFeeTokensHeld2, + tradingFeeTokensHeld2, + borrowingFeeTokensHeld2 + ); + totalFeeAmount = totalFeeAmount.add(feeAmount); + let totalLoanTokenWRBTCBalanceShouldBeAccount1 = feeAmount; + await increaseTime(FEE_WITHDRAWAL_INTERVAL); + await feeSharingProxy.withdrawFees([SUSD.address]); + + // [THIRD] + // mock data + let lendingFeeTokensHeld3 = new BN(wei("1", "gwei")); + let tradingFeeTokensHeld3 = new BN(wei("2", "gwei")); + let borrowingFeeTokensHeld3 = new BN(wei("3", "gwei")); + totalFeeTokensHeld = lendingFeeTokensHeld3 + .add(tradingFeeTokensHeld3) + .add(borrowingFeeTokensHeld3); + feeAmount = await setFeeTokensHeld( + lendingFeeTokensHeld3, + tradingFeeTokensHeld3, + borrowingFeeTokensHeld3 + ); + totalFeeAmount = totalFeeAmount.add(feeAmount); + totalLoanTokenWRBTCBalanceShouldBeAccount1 = + totalLoanTokenWRBTCBalanceShouldBeAccount1.add(feeAmount); + await increaseTime(FEE_WITHDRAWAL_INTERVAL); + await feeSharingProxy.withdrawFees([SUSD.address]); + + // [SECOND] - [THIRD] + userInitialBtcBalance = new BN(await web3.eth.getBalance(account1)); + tx = await feeSharingProxy.withdraw(loanTokenWrbtc.address, 2, ZERO_ADDRESS, { + from: account1, + }); + console.log("\nwithdraw(checkpoints = 2).gasUsed: " + tx.receipt.gasUsed); + txFee = new BN(tx.receipt.gasUsed).mul(gasPrice); + + userInitialBtcBalance = userInitialBtcBalance.sub(new BN(txFee)); + + // processedCheckpoints + processedCheckpoints = await feeSharingProxy.processedCheckpoints.call( + account1, + loanTokenWrbtc.address + ); + expect(processedCheckpoints.toNumber()).to.be.equal(3); + + // check balances + feeSharingProxyBalance = await loanTokenWrbtc.balanceOf.call(feeSharingProxy.address); + expect(feeSharingProxyBalance.toNumber()).to.be.equal( + parseInt((totalFeeAmount * 9) / 10) + ); + userBalance = await loanTokenWrbtc.balanceOf.call(account1); + expect(userBalance.toNumber()).to.be.equal(0); + + userLatestBTCBalance = new BN(await web3.eth.getBalance(account1)); + + expect(userLatestBTCBalance.toString()).to.be.equal( + userInitialBtcBalance + .add(totalLoanTokenWRBTCBalanceShouldBeAccount1.mul(new BN(1)).div(new BN(10))) + .toString() + ); + }); + + it("Should be able to process 10 checkpoints", async () => { + /// @dev This test requires redeploying the protocol + await protocolDeploymentFixture(); + + // stake - getPriorTotalVotingPower + await stake(900, root); + let userStake = 100; + if (MOCK_PRIOR_WEIGHTED_STAKE) { + await staking.MOCK_priorWeightedStake(userStake * 10); + } + await SOVToken.transfer(account1, userStake); + await stake(userStake, account1); + + // mock data + await createCheckpoints(10); + + let tx = await feeSharingProxy.withdraw(loanTokenWrbtc.address, 1000, ZERO_ADDRESS, { + from: account1, + }); + console.log("\nwithdraw(checkpoints = 10).gasUsed: " + tx.receipt.gasUsed); + // processedCheckpoints + let processedCheckpoints = await feeSharingProxy.processedCheckpoints.call( + account1, + loanTokenWrbtc.address + ); + expect(processedCheckpoints.toNumber()).to.be.equal(10); + }); + + it("Should be able to process 10 checkpoints and 3 withdrawals", async () => { + /// @dev This test requires redeploying the protocol + await protocolDeploymentFixture(); + + // stake - getPriorTotalVotingPower + await stake(900, root); + let userStake = 100; + if (MOCK_PRIOR_WEIGHTED_STAKE) { + await staking.MOCK_priorWeightedStake(userStake * 10); + } + await SOVToken.transfer(account1, userStake); + await stake(userStake, account1); + + // mock data + await createCheckpoints(10); + + let tx = await feeSharingProxy.withdraw(loanTokenWrbtc.address, 5, ZERO_ADDRESS, { + from: account1, + }); + console.log("\nwithdraw(checkpoints = 5).gasUsed: " + tx.receipt.gasUsed); + // processedCheckpoints + let processedCheckpoints = await feeSharingProxy.processedCheckpoints.call( + account1, + loanTokenWrbtc.address + ); + expect(processedCheckpoints.toNumber()).to.be.equal(5); + + tx = await feeSharingProxy.withdraw(loanTokenWrbtc.address, 3, ZERO_ADDRESS, { + from: account1, + }); + console.log("\nwithdraw(checkpoints = 3).gasUsed: " + tx.receipt.gasUsed); + // processedCheckpoints + processedCheckpoints = await feeSharingProxy.processedCheckpoints.call( + account1, + loanTokenWrbtc.address + ); + expect(processedCheckpoints.toNumber()).to.be.equal(8); + + tx = await feeSharingProxy.withdraw(loanTokenWrbtc.address, 1000, ZERO_ADDRESS, { + from: account1, + }); + console.log("\nwithdraw(checkpoints = 2).gasUsed: " + tx.receipt.gasUsed); + // processedCheckpoints + processedCheckpoints = await feeSharingProxy.processedCheckpoints.call( + account1, + loanTokenWrbtc.address + ); + expect(processedCheckpoints.toNumber()).to.be.equal(10); + }); + + // // use for gas usage tests + // it("Should be able to process 30 checkpoints", async () => { + // // stake - getPriorTotalVotingPower + // await stake(900, root); + // let userStake = 100; + // if (MOCK_PRIOR_WEIGHTED_STAKE) { + // await staking.MOCK_priorWeightedStake(userStake * 10); + // } + // await SOVToken.transfer(account1, userStake); + // await stake(userStake, account1); + // + // // mock data + // await createCheckpoints(30); + // + // let tx = await feeSharingProxy.withdraw(loanToken.address, 1000, ZERO_ADDRESS, {from: account1}); + // console.log("\nwithdraw(checkpoints = 30).gasUsed: " + tx.receipt.gasUsed); + // // processedCheckpoints + // let processedCheckpoints = await feeSharingProxy.processedCheckpoints.call(account1, loanToken.address); + // expect(processedCheckpoints.toNumber()).to.be.equal(30); + // }); + // + // // use for gas usage tests + // it("Should be able to process 100 checkpoints", async () => { + // // stake - getPriorTotalVotingPower + // await stake(900, root); + // let userStake = 100; + // if (MOCK_PRIOR_WEIGHTED_STAKE) { + // await staking.MOCK_priorWeightedStake(userStake * 10); + // } + // await SOVToken.transfer(account1, userStake); + // await stake(userStake, account1); + // + // // mock data + // await createCheckpoints(100); + // + // let tx = await feeSharingProxy.withdraw(loanToken.address, 1000, ZERO_ADDRESS, {from: account1}); + // console.log("\nwithdraw(checkpoints = 500).gasUsed: " + tx.receipt.gasUsed); + // // processedCheckpoints + // let processedCheckpoints = await feeSharingProxy.processedCheckpoints.call(account1, loanToken.address); + // expect(processedCheckpoints.toNumber()).to.be.equal(100); + // }); + // + // // use for gas usage tests + // it("Should be able to withdraw when staking contains a lot of checkpoints", async () => { + // let checkpointCount = 1000; + // await stake(1000, root, checkpointCount); + // let afterBlock = await blockNumber(); + // console.log(afterBlock); + // + // let kickoffTS = await staking.kickoffTS.call(); + // let stakingDate = kickoffTS.add(new BN(MAX_DURATION)); + // + // let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints.call(root, stakingDate); + // let firstCheckpoint = await staking.userStakingCheckpoints.call(root, stakingDate, 0); + // let lastCheckpoint = await staking.userStakingCheckpoints.call(root, stakingDate, numUserStakingCheckpoints - 1); + // let block1 = firstCheckpoint.fromBlock.toNumber() + 1; + // let block2 = lastCheckpoint.fromBlock; + // + // console.log("numUserStakingCheckpoints = " + numUserStakingCheckpoints.toString()); + // console.log("first = " + firstCheckpoint.fromBlock.toString()); + // console.log("last = " + lastCheckpoint.fromBlock.toString()); + // + // let tx = await staking.calculatePriorWeightedStake(root, block1, stakingDate); + // console.log("\ncalculatePriorWeightedStake(checkpoints = " + checkpointCount + ").gasUsed: " + tx.receipt.gasUsed); + // tx = await staking.calculatePriorWeightedStake(root, block2, stakingDate); + // console.log("\ncalculatePriorWeightedStake(checkpoints = " + checkpointCount + ").gasUsed: " + tx.receipt.gasUsed); + // }); + + it("Should be able to withdraw with staking for 78 dates", async () => { + /// @dev This test requires redeploying the protocol + await protocolDeploymentFixture(); + + // stake - getPriorTotalVotingPower + let rootStake = 700; + await stake(rootStake, root); + + let userStake = 300; + if (MOCK_PRIOR_WEIGHTED_STAKE) { + await staking.MOCK_priorWeightedStake(userStake * 10); + } + await SOVToken.transfer(account1, userStake); + await stake(userStake, account1); + + let kickoffTS = await staking.kickoffTS.call(); + await SOVToken.approve(staking.address, userStake * 1000); + for (let i = 0; i < 77; i++) { + let stakingDate = kickoffTS.add(new BN(TWO_WEEKS * (i + 1))); + await staking.stake(userStake, stakingDate, account1, account1); + } + + // mock data + await setFeeTokensHeld(new BN(100), new BN(200), new BN(300)); + + await feeSharingProxy.withdrawFees([SUSD.address]); + + let tx = await feeSharingProxy.withdraw(loanTokenWrbtc.address, 10, ZERO_ADDRESS, { + from: account1, + }); + console.log("\nwithdraw(checkpoints = 1).gasUsed: " + tx.receipt.gasUsed); + }); + + it("should compute the weighted stake and show gas usage", async () => { + /// @dev This test requires redeploying the protocol + await protocolDeploymentFixture(); + + await stake(100, root); + let kickoffTS = await staking.kickoffTS.call(); + let stakingDate = kickoffTS.add(new BN(MAX_DURATION)); + await SOVToken.approve(staking.address, 100); + let result = await staking.stake("100", stakingDate, root, root); + await mineBlock(); + + let tx = await staking.calculatePriorWeightedStake( + root, + result.receipt.blockNumber, + stakingDate + ); + console.log("\ngasUsed: " + tx.receipt.gasUsed); + }); + }); + + describe("withdraw with or considering vesting contracts", () => { + it("getAccumulatedFees should return 0 for vesting contracts", async () => { + /// @dev This test requires redeploying the protocol + await protocolDeploymentFixture(); + + let { vestingInstance } = await createVestingContractWithSingleDate( + new BN(MAX_DURATION), + 1000, + root + ); + await setFeeTokensHeld(new BN(100), new BN(200), new BN(300)); + let fees = await feeSharingProxy.getAccumulatedFees( + vestingInstance.address, + loanToken.address + ); + expect(fees).to.be.bignumber.equal("0"); + }); + + it("vesting contract should not be able to withdraw fees", async () => { + /// @dev This test requires redeploying the protocol + await protocolDeploymentFixture(); + + let { vestingInstance } = await createVestingContractWithSingleDate( + new BN(MAX_DURATION), + 1000, + root + ); + await setFeeTokensHeld(new BN(100), new BN(200), new BN(300)); + await expectRevert( + vestingInstance.collectDividends(loanToken.address, 5, root), + "FeeSharingProxy::withdrawFees: no tokens for a withdrawal" + ); + }); + + it("vested stakes should be deducted from total weighted stake on share distribution", async () => { + /// @dev This test requires redeploying the protocol + await protocolDeploymentFixture(); + + // 50% vested 50% voluntary stakes + await createVestingContractWithSingleDate(new BN(MAX_DURATION), 1000, root); + let userStake = 1000; + if (MOCK_PRIOR_WEIGHTED_STAKE) { + await staking.MOCK_priorWeightedStake(userStake * 10); + } + await SOVToken.transfer(account1, userStake); + await stake(userStake, account1); + + await setFeeTokensHeld(new BN(100), new BN(200), new BN(300)); + let tx = await feeSharingProxy.withdrawFees([SUSD.address]); + let feesWithdrawn = tx.logs[1].args.amount; + let userFees = await feeSharingProxy.getAccumulatedFees( + account1, + loanTokenWrbtc.address + ); + + // 100% of the fees should go to the user -> vesting contract not considered + expect(feesWithdrawn).to.be.bignumber.equal(userFees); + }); + }); + + describe("withdraw AMM Fees", async () => { + it("Whitelist converter", async () => { + /// @dev This test requires redeploying the protocol + await protocolDeploymentFixture(); + + await expectRevert( + feeSharingProxy.addWhitelistedConverterAddress(account1), + "Non contract address given" + ); + await expectRevert( + feeSharingProxy.addWhitelistedConverterAddress(ZERO_ADDRESS), + "Non contract address given" + ); + + const liquidityPoolV1Converter = await LiquidityPoolV1Converter.new( + SOVToken.address, + SUSD.address + ); + await feeSharingProxy.addWhitelistedConverterAddress(liquidityPoolV1Converter.address); + let whitelistedConverterList = await feeSharingProxy.getWhitelistedConverterList(); + expect(whitelistedConverterList.length).to.equal(1); + expect(whitelistedConverterList[0]).to.equal(liquidityPoolV1Converter.address); + await feeSharingProxy.addWhitelistedConverterAddress(liquidityPoolV1Converter.address); + whitelistedConverterList = await feeSharingProxy.getWhitelistedConverterList(); + expect(whitelistedConverterList.length).to.equal(1); + expect(whitelistedConverterList[0]).to.equal(liquidityPoolV1Converter.address); + }); + + it("Remove converter from whitelist", async () => { + /// @dev This test requires redeploying the protocol + await protocolDeploymentFixture(); + + const liquidityPoolV1Converter = await LiquidityPoolV1Converter.new( + SOVToken.address, + SUSD.address + ); + let whitelistedConverterList = await feeSharingProxy.getWhitelistedConverterList(); + expect(whitelistedConverterList.length).to.equal(0); + + await feeSharingProxy.removeWhitelistedConverterAddress( + liquidityPoolV1Converter.address + ); + whitelistedConverterList = await feeSharingProxy.getWhitelistedConverterList(); + expect(whitelistedConverterList.length).to.equal(0); + + await feeSharingProxy.addWhitelistedConverterAddress(liquidityPoolV1Converter.address); + whitelistedConverterList = await feeSharingProxy.getWhitelistedConverterList(); + expect(whitelistedConverterList.length).to.equal(1); + expect(whitelistedConverterList[0]).to.equal(liquidityPoolV1Converter.address); + + await feeSharingProxy.removeWhitelistedConverterAddress( + liquidityPoolV1Converter.address + ); + whitelistedConverterList = await feeSharingProxy.getWhitelistedConverterList(); + expect(whitelistedConverterList.length).to.equal(0); + }); + + it("should not be able to withdraw fees if converters address is not a contract address", async () => { + /// @dev This test requires redeploying the protocol + await protocolDeploymentFixture(); + + await expectRevert( + feeSharingProxy.withdrawFeesAMM([accounts[0]]), + "Invalid Converter" + ); + }); + + it("Should not be able to withdraw AMM Fees after whitelist removal", async () => { + /// @dev This test requires redeploying the protocol + await protocolDeploymentFixture(); + + //stake - getPriorTotalVotingPower + let totalStake = 1000; + await stake(totalStake, root); + + //mock data + // AMM Converter + liquidityPoolV1Converter = await LiquidityPoolV1Converter.new( + SOVToken.address, + SUSD.address + ); + const feeAmount = new BN(wei("1", "ether")); + await liquidityPoolV1Converter.setTotalFeeMockupValue(feeAmount.toString()); + + await expectRevert( + feeSharingProxy.withdrawFeesAMM([liquidityPoolV1Converter.address]), + "Invalid Converter" + ); + await feeSharingProxy.addWhitelistedConverterAddress(liquidityPoolV1Converter.address); + await feeSharingProxy.removeWhitelistedConverterAddress( + liquidityPoolV1Converter.address + ); + await expectRevert( + feeSharingProxy.withdrawFeesAMM([liquidityPoolV1Converter.address]), + "Invalid Converter" + ); + await feeSharingProxy.addWhitelistedConverterAddress(liquidityPoolV1Converter.address); + + await expectRevert( + feeSharingProxy.withdrawFeesAMM([liquidityPoolV1Converter.address]), + "unauthorized" + ); + await liquidityPoolV1Converter.setFeesController(feeSharingProxy.address); + await liquidityPoolV1Converter.setWrbtcToken(WRBTC.address); + await WRBTC.mint(liquidityPoolV1Converter.address, wei("2", "ether")); + + tx = await feeSharingProxy.withdrawFeesAMM([liquidityPoolV1Converter.address]); + + //check WRBTC balance (wrbt balance = (totalFeeTokensHeld * mockPrice) - swapFee) + let feeSharingProxyBalance = await loanTokenWrbtc.balanceOf.call( + feeSharingProxy.address + ); + expect(feeSharingProxyBalance.toString()).to.be.equal(feeAmount.toString()); + + // make sure wrbtc balance is 0 after withdrawal + let feeSharingProxyWRBTCBalance = await WRBTC.balanceOf.call(feeSharingProxy.address); + expect(feeSharingProxyWRBTCBalance.toString()).to.be.equal(new BN(0).toString()); + + //checkpoints + let numTokenCheckpoints = await feeSharingProxy.numTokenCheckpoints.call( + loanTokenWrbtc.address + ); + expect(numTokenCheckpoints.toNumber()).to.be.equal(1); + let checkpoint = await feeSharingProxy.tokenCheckpoints.call( + loanTokenWrbtc.address, + 0 + ); + expect(checkpoint.blockNumber.toNumber()).to.be.equal(tx.receipt.blockNumber); + expect(checkpoint.totalWeightedStake.toNumber()).to.be.equal( + totalStake * MAX_VOTING_WEIGHT + ); + expect(checkpoint.numTokens.toString()).to.be.equal(feeAmount.toString()); + + //check lastFeeWithdrawalTime + let lastFeeWithdrawalTime = await feeSharingProxy.lastFeeWithdrawalTime.call( + loanTokenWrbtc.address + ); + let block = await web3.eth.getBlock(tx.receipt.blockNumber); + expect(lastFeeWithdrawalTime.toString()).to.be.equal(block.timestamp.toString()); + + expectEvent(tx, "FeeAMMWithdrawn", { + sender: root, + converter: liquidityPoolV1Converter.address, + amount: feeAmount, + }); + }); + + it("Should be able to withdraw AMM Fees", async () => { + /// @dev This test requires redeploying the protocol + await protocolDeploymentFixture(); + + //stake - getPriorTotalVotingPower + let totalStake = 1000; + await stake(totalStake, root); + + //mock data + // AMM Converter + liquidityPoolV1Converter = await LiquidityPoolV1Converter.new( + SOVToken.address, + SUSD.address + ); + const feeAmount = new BN(wei("1", "ether")); + await liquidityPoolV1Converter.setTotalFeeMockupValue(feeAmount.toString()); + + await expectRevert( + feeSharingProxy.withdrawFeesAMM([liquidityPoolV1Converter.address]), + "Invalid Converter" + ); + await feeSharingProxy.addWhitelistedConverterAddress(liquidityPoolV1Converter.address); + await expectRevert( + feeSharingProxy.withdrawFeesAMM([liquidityPoolV1Converter.address]), + "unauthorized" + ); + await liquidityPoolV1Converter.setFeesController(feeSharingProxy.address); + await liquidityPoolV1Converter.setWrbtcToken(WRBTC.address); + await WRBTC.mint(liquidityPoolV1Converter.address, wei("2", "ether")); + + tx = await feeSharingProxy.withdrawFeesAMM([liquidityPoolV1Converter.address]); + + //check WRBTC balance (wrbt balance = (totalFeeTokensHeld * mockPrice) - swapFee) + let feeSharingProxyBalance = await loanTokenWrbtc.balanceOf.call( + feeSharingProxy.address + ); + expect(feeSharingProxyBalance.toString()).to.be.equal(feeAmount.toString()); + + // make sure wrbtc balance is 0 after withdrawal + let feeSharingProxyWRBTCBalance = await WRBTC.balanceOf.call(feeSharingProxy.address); + expect(feeSharingProxyWRBTCBalance.toString()).to.be.equal(new BN(0).toString()); + + //checkpoints + let numTokenCheckpoints = await feeSharingProxy.numTokenCheckpoints.call( + loanTokenWrbtc.address + ); + expect(numTokenCheckpoints.toNumber()).to.be.equal(1); + let checkpoint = await feeSharingProxy.tokenCheckpoints.call( + loanTokenWrbtc.address, + 0 + ); + expect(checkpoint.blockNumber.toNumber()).to.be.equal(tx.receipt.blockNumber); + expect(checkpoint.totalWeightedStake.toNumber()).to.be.equal( + totalStake * MAX_VOTING_WEIGHT + ); + expect(checkpoint.numTokens.toString()).to.be.equal(feeAmount.toString()); + + //check lastFeeWithdrawalTime + let lastFeeWithdrawalTime = await feeSharingProxy.lastFeeWithdrawalTime.call( + loanTokenWrbtc.address + ); + let block = await web3.eth.getBlock(tx.receipt.blockNumber); + expect(lastFeeWithdrawalTime.toString()).to.be.equal(block.timestamp.toString()); + + expectEvent(tx, "FeeAMMWithdrawn", { + sender: root, + converter: liquidityPoolV1Converter.address, + amount: feeAmount, + }); + }); + + it("Should be able to withdraw with 0 AMM Fees", async () => { + /// @dev This test requires redeploying the protocol + await protocolDeploymentFixture(); + + //stake - getPriorTotalVotingPower + let totalStake = 1000; + await stake(totalStake, root); + + //mock data + // AMM Converter + liquidityPoolV1Converter = await LiquidityPoolV1Converter.new( + SOVToken.address, + SUSD.address + ); + const feeAmount = new BN(wei("0", "ether")); + await liquidityPoolV1Converter.setTotalFeeMockupValue(feeAmount.toString()); + await expectRevert( + feeSharingProxy.withdrawFeesAMM([liquidityPoolV1Converter.address]), + "Invalid Converter" + ); + await feeSharingProxy.addWhitelistedConverterAddress(liquidityPoolV1Converter.address); + await expectRevert( + feeSharingProxy.withdrawFeesAMM([liquidityPoolV1Converter.address]), + "unauthorized" + ); + await liquidityPoolV1Converter.setFeesController(feeSharingProxy.address); + await liquidityPoolV1Converter.setWrbtcToken(WRBTC.address); + await WRBTC.mint(liquidityPoolV1Converter.address, wei("2", "ether")); + + tx = await feeSharingProxy.withdrawFeesAMM([liquidityPoolV1Converter.address]); + + //check WRBTC balance (wrbt balance = (totalFeeTokensHeld * mockPrice) - swapFee) + let feeSharingProxyBalance = await loanTokenWrbtc.balanceOf.call( + feeSharingProxy.address + ); + expect(feeSharingProxyBalance.toString()).to.be.equal(feeAmount.toString()); + + // make sure wrbtc balance is 0 after withdrawal + let feeSharingProxyWRBTCBalance = await WRBTC.balanceOf.call(feeSharingProxy.address); + expect(feeSharingProxyWRBTCBalance.toString()).to.be.equal(new BN(0).toString()); + + //checkpoints + let numTokenCheckpoints = await feeSharingProxy.numTokenCheckpoints.call( + loanTokenWrbtc.address + ); + expect(numTokenCheckpoints.toNumber()).to.be.equal(0); + let checkpoint = await feeSharingProxy.tokenCheckpoints.call( + loanTokenWrbtc.address, + 0 + ); + expect(checkpoint.blockNumber.toNumber()).to.be.equal(0); + expect(checkpoint.totalWeightedStake.toNumber()).to.be.equal(0); + expect(checkpoint.numTokens.toString()).to.be.equal("0"); + + //check lastFeeWithdrawalTime + let lastFeeWithdrawalTime = await feeSharingProxy.lastFeeWithdrawalTime.call( + loanTokenWrbtc.address + ); + expect(lastFeeWithdrawalTime.toString()).to.be.equal("0"); + }); + }); + + describe("withdraw wrbtc", async () => { + it("Withdraw wrbtc from non owner should revert", async () => { + await protocolDeploymentFixture(); + const receiver = accounts[1]; + const previousBalanceReceiver = await WRBTC.balanceOf(receiver); + await expectRevert( + feeSharingProxy.withdrawWRBTC(receiver, 0, { from: accounts[1] }), + "unauthorized" + ); + }); + + it("Withdraw 0 wrbtc", async () => { + await protocolDeploymentFixture(); + const receiver = accounts[1]; + const previousBalanceReceiver = await WRBTC.balanceOf(receiver); + await feeSharingProxy.withdrawWRBTC(receiver, 0); + const latestBalanceReceiver = await WRBTC.balanceOf(receiver); + const latestBalanceFeeSharingProxy = await WRBTC.balanceOf(feeSharingProxy.address); + + expect( + new BN(latestBalanceReceiver).sub(new BN(previousBalanceReceiver)).toString() + ).to.equal("0"); + expect(latestBalanceFeeSharingProxy.toString()).to.equal("0"); + }); + + it("Withdraw wrbtc more than the balance of feeSharingProxy should revert", async () => { + await protocolDeploymentFixture(); + await WRBTC.mint(root, wei("500", "ether")); + await WRBTC.transfer(feeSharingProxy.address, wei("1", "ether")); + + const receiver = accounts[1]; + const previousBalanceReceiver = await WRBTC.balanceOf(receiver); + const feeSharingProxyBalance = await WRBTC.balanceOf(feeSharingProxy.address); + const amount = feeSharingProxyBalance.add(new BN(100)); + const previousBalanceFeeSharingProxy = await WRBTC.balanceOf(feeSharingProxy.address); + + await expectRevert( + feeSharingProxy.withdrawWRBTC(receiver, amount.toString()), + "Insufficient balance" + ); + + const latestBalanceReceiver = await WRBTC.balanceOf(receiver); + const latestBalanceFeeSharingProxy = await WRBTC.balanceOf(feeSharingProxy.address); + + expect( + new BN(latestBalanceReceiver).sub(new BN(previousBalanceReceiver)).toString() + ).to.equal("0"); + expect(latestBalanceFeeSharingProxy.toString()).to.equal( + previousBalanceFeeSharingProxy.toString() + ); + }); + + it("Fully Withdraw wrbtc", async () => { + await protocolDeploymentFixture(); + await WRBTC.mint(root, wei("500", "ether")); + await WRBTC.transfer(feeSharingProxy.address, wei("1", "ether")); + + const receiver = accounts[1]; + const previousBalanceReceiver = await WRBTC.balanceOf(receiver); + const feeSharingProxyBalance = await WRBTC.balanceOf(feeSharingProxy.address); + + const tx = await feeSharingProxy.withdrawWRBTC( + receiver, + feeSharingProxyBalance.toString() + ); + await expectEvent.inTransaction( + tx.receipt.rawLogs[0].transactionHash, + WRBTC, + "Transfer", + { + src: feeSharingProxy.address, + dst: receiver, + wad: feeSharingProxyBalance.toString(), + } + ); + + const latestBalanceReceiver = await WRBTC.balanceOf(receiver); + const latestBalanceFeeSharingProxy = await WRBTC.balanceOf(feeSharingProxy.address); + + expect( + new BN(latestBalanceReceiver).sub(new BN(previousBalanceReceiver)).toString() + ).to.equal(feeSharingProxyBalance.toString()); + expect(latestBalanceFeeSharingProxy.toString()).to.equal("0"); + }); + + it("Partially Withdraw wrbtc", async () => { + await protocolDeploymentFixture(); + await WRBTC.mint(root, wei("500", "ether")); + await WRBTC.transfer(feeSharingProxy.address, wei("1", "ether")); + + const receiver = accounts[1]; + const restAmount = new BN("100"); // 100 wei + const previousBalanceReceiver = await WRBTC.balanceOf(receiver); + const feeSharingProxyBalance = await WRBTC.balanceOf(feeSharingProxy.address); + const amount = feeSharingProxyBalance.sub(restAmount); + const previousBalanceFeeSharingProxy = await WRBTC.balanceOf(feeSharingProxy.address); + expect(previousBalanceFeeSharingProxy.toString()).to.equal(wei("1", "ether")); + + const tx = await feeSharingProxy.withdrawWRBTC(receiver, amount.toString()); + await expectEvent.inTransaction( + tx.receipt.rawLogs[0].transactionHash, + WRBTC, + "Transfer", + { + src: feeSharingProxy.address, + dst: receiver, + wad: amount, + } + ); + + const latestBalanceReceiver = await WRBTC.balanceOf(receiver); + const latestBalanceFeeSharingProxy = await WRBTC.balanceOf(feeSharingProxy.address); + + expect( + new BN(latestBalanceReceiver).sub(new BN(previousBalanceReceiver)).toString() + ).to.equal(amount.toString()); + expect(latestBalanceFeeSharingProxy.toString()).to.equal(restAmount.toString()); + + // try to withdraw the rest + const tx2 = await feeSharingProxy.withdrawWRBTC( + receiver, + latestBalanceFeeSharingProxy.toString() + ); + const finalBalanceFeeSharingProxy = await WRBTC.balanceOf(feeSharingProxy.address); + const finalBalanceReceiver = await WRBTC.balanceOf(receiver); + expect(new BN(finalBalanceReceiver).toString()).to.equal( + previousBalanceFeeSharingProxy.toString() + ); + expect(finalBalanceFeeSharingProxy.toString()).to.equal("0"); + + await expectEvent.inTransaction( + tx2.receipt.rawLogs[0].transactionHash, + WRBTC, + "Transfer", + { + src: feeSharingProxy.address, + dst: receiver, + wad: latestBalanceFeeSharingProxy.toString(), + } + ); + }); + }); + + async function stake(amount, user, checkpointCount) { + await SOVToken.approve(staking.address, amount); + let kickoffTS = await staking.kickoffTS.call(); + let stakingDate = kickoffTS.add(new BN(MAX_DURATION)); + let tx = await staking.stake(amount, stakingDate, user, user); + await mineBlock(); + + if (checkpointCount > 0) { + await increaseStake(amount, user, stakingDate, checkpointCount - 1); + } + + return tx; + } + + async function increaseStake(amount, user, stakingDate, checkpointCount) { + for (let i = 0; i < checkpointCount; i++) { + await SOVToken.approve(staking.address, amount); + await staking.increaseStake(amount, user, stakingDate); + } + } + + async function setFeeTokensHeld( + lendingFee, + tradingFee, + borrowingFee, + wrbtcTokenFee = false, + sovTokenFee = false + ) { + let totalFeeAmount = lendingFee.add(tradingFee).add(borrowingFee); + let tokenFee; + if (wrbtcTokenFee) { + tokenFee = WRBTC; + } else { + tokenFee = SUSD; + await tokenFee.transfer(sovryn.address, totalFeeAmount); + } + await sovryn.setLendingFeeTokensHeld(tokenFee.address, lendingFee); + await sovryn.setTradingFeeTokensHeld(tokenFee.address, tradingFee); + await sovryn.setBorrowingFeeTokensHeld(tokenFee.address, borrowingFee); + + if (sovTokenFee) { + await SOVToken.transfer(sovryn.address, totalFeeAmount); + await sovryn.setLendingFeeTokensHeld(SOVToken.address, lendingFee); + await sovryn.setTradingFeeTokensHeld(SOVToken.address, tradingFee); + await sovryn.setBorrowingFeeTokensHeld(SOVToken.address, borrowingFee); + } + return totalFeeAmount; + } + + async function checkWithdrawFee(checkSUSD = true, checkWRBTC = false, checkSOV = false) { + if (checkSUSD) { + let protocolBalance = await SUSD.balanceOf(sovryn.address); + expect(protocolBalance.toString()).to.be.equal(new BN(0).toString()); + let lendingFeeTokensHeld = await sovryn.lendingFeeTokensHeld.call(SUSD.address); + expect(lendingFeeTokensHeld.toString()).to.be.equal(new BN(0).toString()); + let tradingFeeTokensHeld = await sovryn.tradingFeeTokensHeld.call(SUSD.address); + expect(tradingFeeTokensHeld.toString()).to.be.equal(new BN(0).toString()); + let borrowingFeeTokensHeld = await sovryn.borrowingFeeTokensHeld.call(SUSD.address); + expect(borrowingFeeTokensHeld.toString()).to.be.equal(new BN(0).toString()); + } + + if (checkWRBTC) { + lendingFeeTokensHeld = await sovryn.lendingFeeTokensHeld.call(WRBTC.address); + expect(lendingFeeTokensHeld.toString()).to.be.equal(new BN(0).toString()); + tradingFeeTokensHeld = await sovryn.tradingFeeTokensHeld.call(WRBTC.address); + expect(tradingFeeTokensHeld.toString()).to.be.equal(new BN(0).toString()); + borrowingFeeTokensHeld = await sovryn.borrowingFeeTokensHeld.call(WRBTC.address); + expect(borrowingFeeTokensHeld.toString()).to.be.equal(new BN(0).toString()); + } + + if (checkSOV) { + protocolBalance = await SOVToken.balanceOf(sovryn.address); + expect(protocolBalance.toString()).to.be.equal(new BN(0).toString()); + lendingFeeTokensHeld = await sovryn.lendingFeeTokensHeld.call(SOVToken.address); + expect(lendingFeeTokensHeld.toString()).to.be.equal(new BN(0).toString()); + tradingFeeTokensHeld = await sovryn.tradingFeeTokensHeld.call(SOVToken.address); + expect(tradingFeeTokensHeld.toString()).to.be.equal(new BN(0).toString()); + borrowingFeeTokensHeld = await sovryn.borrowingFeeTokensHeld.call(SOVToken.address); + expect(borrowingFeeTokensHeld.toString()).to.be.equal(new BN(0).toString()); + } + } + + async function createCheckpoints(number) { + for (let i = 0; i < number; i++) { + await setFeeTokensHeld(new BN(100), new BN(200), new BN(300)); + await increaseTime(FEE_WITHDRAWAL_INTERVAL); + await feeSharingProxy.withdrawFees([SUSD.address]); + } + } + + async function createVestingContractWithSingleDate(cliff, amount, tokenOwner) { + vestingLogic = await VestingLogic.new(); + let vestingInstance = await Vesting.new( + vestingLogic.address, + SOVToken.address, + staking.address, + tokenOwner, + cliff, + cliff, + feeSharingProxy.address + ); + vestingInstance = await VestingLogic.at(vestingInstance.address); + // important, so it's recognized as vesting contract + await staking.addContractCodeHash(vestingInstance.address); + + await SOVToken.approve(vestingInstance.address, amount); + let result = await vestingInstance.stakeTokens(amount); + return { vestingInstance: vestingInstance, blockNumber: result.receipt.blockNumber }; + } }); diff --git a/tests/Governance/GovernanceIntegrationTest.js b/tests/Governance/GovernanceIntegrationTest.js index 7b02b74c9..056700892 100644 --- a/tests/Governance/GovernanceIntegrationTest.js +++ b/tests/Governance/GovernanceIntegrationTest.js @@ -19,20 +19,20 @@ const { loadFixture } = waffle; const { expectRevert, expectEvent, constants, BN } = require("@openzeppelin/test-helpers"); const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getLoanTokenLogic, - getLoanToken, - getLoanTokenLogicWrbtc, - getLoanTokenWRBTC, - loan_pool_setup, - set_demand_curve, - getPriceFeeds, - getSovryn, - decodeLogs, - getSOV, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getLoanTokenLogic, + getLoanToken, + getLoanTokenLogicWrbtc, + getLoanTokenWRBTC, + loan_pool_setup, + set_demand_curve, + getPriceFeeds, + getSovryn, + decodeLogs, + getSOV, } = require("../Utils/initializer.js"); const { ZERO_ADDRESS } = constants; @@ -53,199 +53,207 @@ const TWO_DAYS = 86400 * 2; const MAX_DURATION = new BN(24 * 60 * 60).mul(new BN(1092)); contract("GovernanceIntegration", (accounts) => { - let root, account1, account2, account3, account4; - let SUSD, staking, gov, timelock; - let sovryn; - - async function deploymentAndInitFixture(_wallets, _provider) { - // Deploying sovrynProtocol w/ generic function from initializer.js - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - await sovryn.setSovrynProtocolAddress(sovryn.address); - - // Staking - let stakingLogic = await StakingLogic.new(SUSD.address); - staking = await StakingProxy.new(SUSD.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingLogic.at(staking.address); - - // Governor - timelock = await Timelock.new(root, TWO_DAYS); - gov = await GovernorAlpha.new(timelock.address, staking.address, root, 4, 0); - await timelock.harnessSetAdmin(gov.address); - - // Settings - loanTokenSettings = await LoanTokenSettings.new(); - loanToken = await LoanToken.new(root, loanTokenSettings.address, SUSD.address, SUSD.address); - loanToken = await LoanTokenSettings.at(loanToken.address); - await loanToken.transferOwnership(timelock.address); - - await sovryn.transferOwnership(timelock.address); - } - - before(async () => { - [root, account1, account2, account3, account4, ...accounts] = accounts; - }); - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - describe("change settings", () => { - it("Should be able to execute one action", async () => { - let lendingFeePercentOld = etherMantissa(10).toString(); - let lendingFeePercentNew = etherMantissa(7).toString(); - - let proposalData = { - targets: [sovryn.address], - values: [0], - signatures: ["setLendingFeePercent(uint256)"], - callDatas: [encodeParameters(["uint256"], [lendingFeePercentNew])], - description: "change settings", - }; - - // old value - let lendingFeePercent = await sovryn.lendingFeePercent.call(); - expect(lendingFeePercent.toString()).to.be.equal(lendingFeePercentOld); - - // make changes - await executeProposal(proposalData); - - // new value - lendingFeePercent = await sovryn.lendingFeePercent.call(); - expect(lendingFeePercent.toString()).to.be.equal(lendingFeePercentNew); - }); - - it("Should be able to execute one action with signature in the call data", async () => { - let lendingFeePercentOld = etherMantissa(10).toString(); - let lendingFeePercentNew = etherMantissa(7).toString(); - - let selector = web3.utils.keccak256("setLendingFeePercent(uint256)").substring(0, 10); - let callData = encodeParameters(["uint256"], [lendingFeePercentNew]).replace("0x", selector); - - let proposalData = { - targets: [sovryn.address], - values: [0], - signatures: [""], - callDatas: [callData], - description: "change settings", - }; - - // old value - let lendingFeePercent = await sovryn.lendingFeePercent.call(); - expect(lendingFeePercent.toString()).to.be.equal(lendingFeePercentOld); - - // make changes - await executeProposal(proposalData); - - // new value - lendingFeePercent = await sovryn.lendingFeePercent.call(); - expect(lendingFeePercent.toString()).to.be.equal(lendingFeePercentNew); - }); - - it("Should be able to execute three actions", async () => { - let tradingFeePercentOld = etherMantissa(15, 1e16).toString(); - let tradingFeePercentNew = etherMantissa(9, 1e16).toString(); - - let proposalData = { - targets: [sovryn.address, sovryn.address /*, loanToken.address*/], - values: [0, 0 /*, 0*/], - signatures: [ - "setTradingFeePercent(uint256)", - "setLoanPool(address[],address[])", - /*"setTransactionLimits(address[],uint256[])",*/ - ], - callDatas: [ - encodeParameters(["uint256"], [tradingFeePercentNew]), - encodeParameters( - ["address[]", "address[]"], - [ - [account1, account2], - [account3, account4], - ] - ), - /*encodeParameters( + let root, account1, account2, account3, account4; + let SUSD, staking, gov, timelock; + let sovryn; + + async function deploymentAndInitFixture(_wallets, _provider) { + // Deploying sovrynProtocol w/ generic function from initializer.js + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + await sovryn.setSovrynProtocolAddress(sovryn.address); + + // Staking + let stakingLogic = await StakingLogic.new(SUSD.address); + staking = await StakingProxy.new(SUSD.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); + + // Governor + timelock = await Timelock.new(root, TWO_DAYS); + gov = await GovernorAlpha.new(timelock.address, staking.address, root, 4, 0); + await timelock.harnessSetAdmin(gov.address); + + // Settings + loanTokenSettings = await LoanTokenSettings.new(); + loanToken = await LoanToken.new( + root, + loanTokenSettings.address, + SUSD.address, + SUSD.address + ); + loanToken = await LoanTokenSettings.at(loanToken.address); + await loanToken.transferOwnership(timelock.address); + + await sovryn.transferOwnership(timelock.address); + } + + before(async () => { + [root, account1, account2, account3, account4, ...accounts] = accounts; + }); + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + describe("change settings", () => { + it("Should be able to execute one action", async () => { + let lendingFeePercentOld = etherMantissa(10).toString(); + let lendingFeePercentNew = etherMantissa(7).toString(); + + let proposalData = { + targets: [sovryn.address], + values: [0], + signatures: ["setLendingFeePercent(uint256)"], + callDatas: [encodeParameters(["uint256"], [lendingFeePercentNew])], + description: "change settings", + }; + + // old value + let lendingFeePercent = await sovryn.lendingFeePercent.call(); + expect(lendingFeePercent.toString()).to.be.equal(lendingFeePercentOld); + + // make changes + await executeProposal(proposalData); + + // new value + lendingFeePercent = await sovryn.lendingFeePercent.call(); + expect(lendingFeePercent.toString()).to.be.equal(lendingFeePercentNew); + }); + + it("Should be able to execute one action with signature in the call data", async () => { + let lendingFeePercentOld = etherMantissa(10).toString(); + let lendingFeePercentNew = etherMantissa(7).toString(); + + let selector = web3.utils.keccak256("setLendingFeePercent(uint256)").substring(0, 10); + let callData = encodeParameters(["uint256"], [lendingFeePercentNew]).replace( + "0x", + selector + ); + + let proposalData = { + targets: [sovryn.address], + values: [0], + signatures: [""], + callDatas: [callData], + description: "change settings", + }; + + // old value + let lendingFeePercent = await sovryn.lendingFeePercent.call(); + expect(lendingFeePercent.toString()).to.be.equal(lendingFeePercentOld); + + // make changes + await executeProposal(proposalData); + + // new value + lendingFeePercent = await sovryn.lendingFeePercent.call(); + expect(lendingFeePercent.toString()).to.be.equal(lendingFeePercentNew); + }); + + it("Should be able to execute three actions", async () => { + let tradingFeePercentOld = etherMantissa(15, 1e16).toString(); + let tradingFeePercentNew = etherMantissa(9, 1e16).toString(); + + let proposalData = { + targets: [sovryn.address, sovryn.address /*, loanToken.address*/], + values: [0, 0 /*, 0*/], + signatures: [ + "setTradingFeePercent(uint256)", + "setLoanPool(address[],address[])", + /*"setTransactionLimits(address[],uint256[])",*/ + ], + callDatas: [ + encodeParameters(["uint256"], [tradingFeePercentNew]), + encodeParameters( + ["address[]", "address[]"], + [ + [account1, account2], + [account3, account4], + ] + ), + /*encodeParameters( ["address[]", "uint256[]"], [ [account1, account2], [1111, 2222], ] ),*/ - ], - description: "change settings", - }; - - // old values - let tradingFeePercent = await sovryn.tradingFeePercent.call(); - expect(tradingFeePercent.toString()).to.be.equal(tradingFeePercentOld); - - expect(await sovryn.loanPoolToUnderlying.call(account1)).to.be.equal(ZERO_ADDRESS); - expect(await sovryn.loanPoolToUnderlying.call(account2)).to.be.equal(ZERO_ADDRESS); - expect(await sovryn.underlyingToLoanPool.call(account3)).to.be.equal(ZERO_ADDRESS); - expect(await sovryn.underlyingToLoanPool.call(account4)).to.be.equal(ZERO_ADDRESS); - - // expect((await loanToken.transactionLimit.call(account1)).toNumber()).to.be.equal(0); - // expect((await loanToken.transactionLimit.call(account2)).toNumber()).to.be.equal(0); - - // make changes - await executeProposal(proposalData); - - // new values - tradingFeePercent = await sovryn.tradingFeePercent.call(); - expect(tradingFeePercent.toString()).to.be.equal(tradingFeePercentNew); - - expect(await sovryn.loanPoolToUnderlying.call(account1)).to.be.equal(account3); - expect(await sovryn.loanPoolToUnderlying.call(account2)).to.be.equal(account4); - expect(await sovryn.underlyingToLoanPool.call(account3)).to.be.equal(account1); - expect(await sovryn.underlyingToLoanPool.call(account4)).to.be.equal(account2); - - // expect((await loanToken.transactionLimit.call(account1)).toNumber()).to.be.equal(1111); - // expect((await loanToken.transactionLimit.call(account2)).toNumber()).to.be.equal(2222); - }); - - it("Shouldn't be able to execute proposal using Timelock directly", async () => { - await expectRevert( - timelock.executeTransaction(ZERO_ADDRESS, "0", "", "0x", "0"), - "Timelock::executeTransaction: Call must come from admin." - ); - }); - }); - - async function executeProposal(proposalData) { - await SUSD.approve(staking.address, QUORUM_VOTES); - let kickoffTS = await staking.kickoffTS.call(); - await staking.stake(QUORUM_VOTES, kickoffTS.add(MAX_DURATION), root, root); - - await gov.propose( - proposalData.targets, - proposalData.values, - proposalData.signatures, - proposalData.callDatas, - proposalData.description - ); - let proposalId = await gov.latestProposalIds.call(root); - - await mineBlock(); - await gov.castVote(proposalId, true); - - await advanceBlocks(10); - await gov.queue(proposalId); - - await increaseTime(TWO_DAYS); - let tx = await gov.execute(proposalId); - - expectEvent(tx, "ProposalExecuted", { - id: proposalId, - }); - } + ], + description: "change settings", + }; + + // old values + let tradingFeePercent = await sovryn.tradingFeePercent.call(); + expect(tradingFeePercent.toString()).to.be.equal(tradingFeePercentOld); + + expect(await sovryn.loanPoolToUnderlying.call(account1)).to.be.equal(ZERO_ADDRESS); + expect(await sovryn.loanPoolToUnderlying.call(account2)).to.be.equal(ZERO_ADDRESS); + expect(await sovryn.underlyingToLoanPool.call(account3)).to.be.equal(ZERO_ADDRESS); + expect(await sovryn.underlyingToLoanPool.call(account4)).to.be.equal(ZERO_ADDRESS); + + // expect((await loanToken.transactionLimit.call(account1)).toNumber()).to.be.equal(0); + // expect((await loanToken.transactionLimit.call(account2)).toNumber()).to.be.equal(0); + + // make changes + await executeProposal(proposalData); + + // new values + tradingFeePercent = await sovryn.tradingFeePercent.call(); + expect(tradingFeePercent.toString()).to.be.equal(tradingFeePercentNew); + + expect(await sovryn.loanPoolToUnderlying.call(account1)).to.be.equal(account3); + expect(await sovryn.loanPoolToUnderlying.call(account2)).to.be.equal(account4); + expect(await sovryn.underlyingToLoanPool.call(account3)).to.be.equal(account1); + expect(await sovryn.underlyingToLoanPool.call(account4)).to.be.equal(account2); + + // expect((await loanToken.transactionLimit.call(account1)).toNumber()).to.be.equal(1111); + // expect((await loanToken.transactionLimit.call(account2)).toNumber()).to.be.equal(2222); + }); + + it("Shouldn't be able to execute proposal using Timelock directly", async () => { + await expectRevert( + timelock.executeTransaction(ZERO_ADDRESS, "0", "", "0x", "0"), + "Timelock::executeTransaction: Call must come from admin." + ); + }); + }); + + async function executeProposal(proposalData) { + await SUSD.approve(staking.address, QUORUM_VOTES); + let kickoffTS = await staking.kickoffTS.call(); + await staking.stake(QUORUM_VOTES, kickoffTS.add(MAX_DURATION), root, root); + + await gov.propose( + proposalData.targets, + proposalData.values, + proposalData.signatures, + proposalData.callDatas, + proposalData.description + ); + let proposalId = await gov.latestProposalIds.call(root); + + await mineBlock(); + await gov.castVote(proposalId, true); + + await advanceBlocks(10); + await gov.queue(proposalId); + + await increaseTime(TWO_DAYS); + let tx = await gov.execute(proposalId); + + expectEvent(tx, "ProposalExecuted", { + id: proposalId, + }); + } }); async function advanceBlocks(number) { - for (let i = 0; i < number; i++) { - await mineBlock(); - } + for (let i = 0; i < number; i++) { + await mineBlock(); + } } diff --git a/tests/Governance/GovernorAlpha/CastVoteTest.js b/tests/Governance/GovernorAlpha/CastVoteTest.js index 082a18799..be5a1e438 100644 --- a/tests/Governance/GovernorAlpha/CastVoteTest.js +++ b/tests/Governance/GovernorAlpha/CastVoteTest.js @@ -17,7 +17,13 @@ const { expect } = require("chai"); const { expectRevert, BN } = require("@openzeppelin/test-helpers"); -const { address, etherMantissa, encodeParameters, mineBlock, setNextBlockTimestamp } = require("../../Utils/Ethereum"); +const { + address, + etherMantissa, + encodeParameters, + mineBlock, + setNextBlockTimestamp, +} = require("../../Utils/Ethereum"); const EIP712 = require("../../Utils/EIP712"); const BigNumber = require("bignumber.js"); @@ -35,212 +41,272 @@ const QUORUM_VOTES = etherMantissa(4000000); const TOTAL_SUPPLY = etherMantissa(100000000); async function enfranchise(token, comp, actor, amount) { - await token.transfer(actor, amount); - await token.approve(comp.address, amount, { from: actor }); - let kickoffTS = await comp.kickoffTS.call(); - await comp.stake(amount, kickoffTS.add(new BN(DELAY)), actor, actor, { from: actor }); + await token.transfer(actor, amount); + await token.approve(comp.address, amount, { from: actor }); + let kickoffTS = await comp.kickoffTS.call(); + await comp.stake(amount, kickoffTS.add(new BN(DELAY)), actor, actor, { from: actor }); } contract("governorAlpha#castVote/2", (accounts) => { - let token, staking, gov, root, a1; - let pkbA1, currentChainId; - let targets, values, signatures, callDatas, proposalId; - - before(async () => { - [root, a1, ...accounts] = accounts; - [pkbRoot, pkbA1, ...pkbAccounts] = getAccountsPrivateKeysBuffer(); - currentChainId = (await ethers.provider.getNetwork()).chainId; - // let blockTimestamp = etherUnsigned(100); - // await setTime(blockTimestamp.toNumber()); - const block = await ethers.provider.getBlock("latest"); - await setNextBlockTimestamp(block.timestamp + 100); - token = await TestToken.new("TestToken", "TST", 18, TOTAL_SUPPLY); - - let stakingLogic = await StakingLogic.new(token.address); - staking = await StakingProxy.new(token.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingLogic.at(staking.address); - - gov = await GovernorAlpha.new(address(0), staking.address, root, 4, 0); - - targets = [a1]; - values = ["0"]; - signatures = ["getBalanceOf(address)"]; - callDatas = [encodeParameters(["address"], [a1])]; - await enfranchise(token, staking, root, QUORUM_VOTES); - await gov.propose(targets, values, signatures, callDatas, "do nothing"); - proposalId = await gov.latestProposalIds.call(root); - }); - - describe("We must revert if:", () => { - it("There does not exist a proposal with matching proposal id where the current block number is between the proposal's start block (exclusive) and end block (inclusive)", async () => { - await expectRevert(gov.castVote.call(proposalId, true), "GovernorAlpha::_castVote: voting is closed"); - }); - - it("Such proposal already has an entry in its voters set matching the sender", async () => { - await mineBlock(); - await mineBlock(); - - await gov.castVote(proposalId, true, { from: accounts[4] }); - await expectRevert(gov.castVote.call(proposalId, true, { from: accounts[4] }), "GovernorAlpha::_castVote: voter already voted"); - }); - }); - - describe("Otherwise", () => { - it("we add the sender to the proposal's voters set", async () => { - await expect((await gov.getReceipt.call(proposalId, accounts[2])).hasVoted).to.be.equal(false); - await gov.castVote(proposalId, true, { from: accounts[2] }); - await expect((await gov.getReceipt.call(proposalId, accounts[2])).hasVoted).to.be.equal(true); - }); - - describe("and we take the balance returned by GetPriorVotes for the given sender and the proposal's start block, which may be zero,", () => { - let actor; // an account that will propose, receive tokens, delegate to self, and vote on own proposal - - it("and we add that ForVotes", async () => { - actor = accounts[1]; - await enfranchise(token, staking, actor, QUORUM_VOTES); - - await gov.propose(targets, values, signatures, callDatas, "do nothing", { from: actor }); - proposalId = await gov.latestProposalIds.call(actor); - - /// @dev Unneeded variable removed for optimization - // let beforeFors = (await gov.proposals.call(proposalId)).forVotes; - await mineBlock(); - await gov.castVote(proposalId, true, { from: actor }); - - let afterFors = (await gov.proposals.call(proposalId)).forVotes; - let proposal = await gov.proposals.call(proposalId); - let expectedVotes = await staking.getPriorVotes.call(actor, proposal.startBlock.toString(), proposal.startTime.toString()); - expect(new BigNumber(afterFors).toString()).to.be.equal(new BigNumber(expectedVotes.toString()).toString()); - }); - - it("or AgainstVotes corresponding to the caller's support flag.", async () => { - actor = accounts[3]; - - await gov.propose(targets, values, signatures, callDatas, "do nothing", { from: actor }); - proposalId = await gov.latestProposalIds.call(actor); - - /// @dev Unneeded variable removed for optimization - // let beforeAgainsts = (await gov.proposals.call(proposalId)).againstVotes; - await mineBlock(); - await gov.castVote(proposalId, false, { from: actor }); - - let afterAgainsts = (await gov.proposals.call(proposalId)).againstVotes; - let proposal = await gov.proposals.call(proposalId); - let expectedVotes = await staking.getPriorVotes.call(actor, proposal.startBlock.toString(), proposal.startTime.toString()); - expect(new BigNumber(afterAgainsts).toString()).to.be.equal(new BigNumber(expectedVotes.toString()).toString()); - }); - }); - - describe("castVoteBySig", () => { - const Domain = (gov) => ({ - name: "Sovryn Governor Alpha", - chainId: currentChainId, // 31337 - Hardhat, // 1 - Mainnet, // await web3.eth.net.getId(); See: https:// github.com/trufflesuite/ganache-core/issues/515 - verifyingContract: gov.address, - }); - const Types = { - Ballot: [ - { name: "proposalId", type: "uint256" }, - { name: "support", type: "bool" }, - ], - }; - - it("reverts if the signatory is invalid", async () => { - await expectRevert( - gov.castVoteBySig(proposalId, false, 0, "0xbad", "0xbad"), - "GovernorAlpha::castVoteBySig: invalid signature" - ); - }); - - it("casts vote on behalf of the signatory", async () => { - await enfranchise(token, staking, a1, QUORUM_VOTES); - await gov.propose(targets, values, signatures, callDatas, "do nothing", { from: a1 }); - proposalId = await gov.latestProposalIds.call(a1); - - const { v, r, s } = EIP712.sign( - Domain(gov), - "Ballot", - { - proposalId, - support: true, - }, - Types, - pkbA1 - // unlockedAccount(a1).secretKey - this doesn't work with Hardhat - ); - - /// @dev Unneeded variable removed for optimization - // let beforeFors = (await gov.proposals.call(proposalId)).forVotes; - await mineBlock(); - const tx = await gov.castVoteBySig(proposalId, true, v, r, s, { from: a1 }); - expect(tx.gasUsed < 80000); - - let proposal = await gov.proposals.call(proposalId); - let expectedVotes = await staking.getPriorVotes.call(a1, proposal.startBlock.toString(), proposal.startTime.toString()); - let afterFors = (await gov.proposals.call(proposalId)).forVotes; - expect(new BigNumber(afterFors).toString()).to.be.equal(new BigNumber(expectedVotes.toString()).toString()); - }); - }); - - it("receipt uses one load", async () => { - /// @dev optimization metric: 87ms - let actor = accounts[2]; - let actor2 = accounts[3]; - await enfranchise(token, staking, actor, QUORUM_VOTES); - await enfranchise(token, staking, actor2, QUORUM_VOTES.multipliedBy(2)); - - /// @dev optimization metric: 180ms - await gov.propose(targets, values, signatures, callDatas, "do nothing", { from: actor }); - proposalId = await gov.latestProposalIds.call(actor); - - /// @dev optimization metric: 159ms - await mineBlock(); - await mineBlock(); - await gov.castVote(proposalId, true, { from: actor }); - await gov.castVote(proposalId, false, { from: actor2 }); - - let trxReceipt = await gov.getReceipt.call(proposalId, actor); - let trxReceipt2 = await gov.getReceipt.call(proposalId, actor2); - - /// @dev optimization metric: 169ms - let proposal = await gov.proposals.call(proposalId); - let expectedVotes = await staking.getPriorVotes.call(actor, proposal.startBlock.toString(), proposal.startTime.toString()); - let expectedVotes2 = await staking.getPriorVotes.call(actor2, proposal.startBlock.toString(), proposal.startTime.toString()); - - expect(new BigNumber(trxReceipt.votes.toString()).toString()).to.be.equal(new BigNumber(expectedVotes.toString()).toString()); - expect(trxReceipt.hasVoted).to.be.equal(true); - expect(trxReceipt.support).to.be.equal(true); - - expect(new BigNumber(trxReceipt2.votes.toString()).toString()).to.be.equal(new BigNumber(expectedVotes2.toString()).toString()); - expect(trxReceipt2.hasVoted).to.be.equal(true); - expect(trxReceipt2.support).to.be.equal(false); - }); - }); - - describe("Check votes for a proposal creator:", () => { - it("compare votes", async () => { - let actor = accounts[4]; - let amount = etherMantissa(1000000); - await token.transfer(actor, amount); - await token.approve(staking.address, amount, { from: actor }); - let kickoffTS = await staking.kickoffTS.call(); - await staking.stake(amount, kickoffTS.add(new BN(TWO_WEEKS)), actor, actor, { from: actor }); - - await gov.propose(targets, values, signatures, callDatas, "do nothing", { from: actor }); - proposalId = await gov.latestProposalIds.call(actor); - - let proposal = await gov.proposals.call(proposalId); - expect(proposal.forVotes.toNumber()).to.be.equal(0); - - await mineBlock(); - await gov.castVote(proposalId, true, { from: actor }); - - proposal = await gov.proposals.call(proposalId); - let expectedVotes = await staking.getPriorVotes.call(actor, proposal.startBlock, proposal.startTime); - expect(proposal.forVotes.toString()).to.be.equal(expectedVotes.toString()); - let receipt = await gov.getReceipt.call(proposalId, actor); - expect(receipt.votes.toString()).to.be.equal(expectedVotes.toString()); - // console.log("\n" + proposal.forVotes.toString()); - }); - }); + let token, staking, gov, root, a1; + let pkbA1, currentChainId; + let targets, values, signatures, callDatas, proposalId; + + before(async () => { + [root, a1, ...accounts] = accounts; + [pkbRoot, pkbA1, ...pkbAccounts] = getAccountsPrivateKeysBuffer(); + currentChainId = (await ethers.provider.getNetwork()).chainId; + // let blockTimestamp = etherUnsigned(100); + // await setTime(blockTimestamp.toNumber()); + const block = await ethers.provider.getBlock("latest"); + await setNextBlockTimestamp(block.timestamp + 100); + token = await TestToken.new("TestToken", "TST", 18, TOTAL_SUPPLY); + + let stakingLogic = await StakingLogic.new(token.address); + staking = await StakingProxy.new(token.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); + + gov = await GovernorAlpha.new(address(0), staking.address, root, 4, 0); + + targets = [a1]; + values = ["0"]; + signatures = ["getBalanceOf(address)"]; + callDatas = [encodeParameters(["address"], [a1])]; + await enfranchise(token, staking, root, QUORUM_VOTES); + await gov.propose(targets, values, signatures, callDatas, "do nothing"); + proposalId = await gov.latestProposalIds.call(root); + }); + + describe("We must revert if:", () => { + it("There does not exist a proposal with matching proposal id where the current block number is between the proposal's start block (exclusive) and end block (inclusive)", async () => { + await expectRevert( + gov.castVote.call(proposalId, true), + "GovernorAlpha::_castVote: voting is closed" + ); + }); + + it("Such proposal already has an entry in its voters set matching the sender", async () => { + await mineBlock(); + await mineBlock(); + + await gov.castVote(proposalId, true, { from: accounts[4] }); + await expectRevert( + gov.castVote.call(proposalId, true, { from: accounts[4] }), + "GovernorAlpha::_castVote: voter already voted" + ); + }); + }); + + describe("Otherwise", () => { + it("we add the sender to the proposal's voters set", async () => { + await expect( + ( + await gov.getReceipt.call(proposalId, accounts[2]) + ).hasVoted + ).to.be.equal(false); + await gov.castVote(proposalId, true, { from: accounts[2] }); + await expect( + ( + await gov.getReceipt.call(proposalId, accounts[2]) + ).hasVoted + ).to.be.equal(true); + }); + + describe("and we take the balance returned by GetPriorVotes for the given sender and the proposal's start block, which may be zero,", () => { + let actor; // an account that will propose, receive tokens, delegate to self, and vote on own proposal + + it("and we add that ForVotes", async () => { + actor = accounts[1]; + await enfranchise(token, staking, actor, QUORUM_VOTES); + + await gov.propose(targets, values, signatures, callDatas, "do nothing", { + from: actor, + }); + proposalId = await gov.latestProposalIds.call(actor); + + /// @dev Unneeded variable removed for optimization + // let beforeFors = (await gov.proposals.call(proposalId)).forVotes; + await mineBlock(); + await gov.castVote(proposalId, true, { from: actor }); + + let afterFors = (await gov.proposals.call(proposalId)).forVotes; + let proposal = await gov.proposals.call(proposalId); + let expectedVotes = await staking.getPriorVotes.call( + actor, + proposal.startBlock.toString(), + proposal.startTime.toString() + ); + expect(new BigNumber(afterFors).toString()).to.be.equal( + new BigNumber(expectedVotes.toString()).toString() + ); + }); + + it("or AgainstVotes corresponding to the caller's support flag.", async () => { + actor = accounts[3]; + + await gov.propose(targets, values, signatures, callDatas, "do nothing", { + from: actor, + }); + proposalId = await gov.latestProposalIds.call(actor); + + /// @dev Unneeded variable removed for optimization + // let beforeAgainsts = (await gov.proposals.call(proposalId)).againstVotes; + await mineBlock(); + await gov.castVote(proposalId, false, { from: actor }); + + let afterAgainsts = (await gov.proposals.call(proposalId)).againstVotes; + let proposal = await gov.proposals.call(proposalId); + let expectedVotes = await staking.getPriorVotes.call( + actor, + proposal.startBlock.toString(), + proposal.startTime.toString() + ); + expect(new BigNumber(afterAgainsts).toString()).to.be.equal( + new BigNumber(expectedVotes.toString()).toString() + ); + }); + }); + + describe("castVoteBySig", () => { + const Domain = (gov) => ({ + name: "Sovryn Governor Alpha", + chainId: currentChainId, // 31337 - Hardhat, // 1 - Mainnet, // await web3.eth.net.getId(); See: https:// github.com/trufflesuite/ganache-core/issues/515 + verifyingContract: gov.address, + }); + const Types = { + Ballot: [ + { name: "proposalId", type: "uint256" }, + { name: "support", type: "bool" }, + ], + }; + + it("reverts if the signatory is invalid", async () => { + await expectRevert( + gov.castVoteBySig(proposalId, false, 0, "0xbad", "0xbad"), + "GovernorAlpha::castVoteBySig: invalid signature" + ); + }); + + it("casts vote on behalf of the signatory", async () => { + await enfranchise(token, staking, a1, QUORUM_VOTES); + await gov.propose(targets, values, signatures, callDatas, "do nothing", { + from: a1, + }); + proposalId = await gov.latestProposalIds.call(a1); + + const { v, r, s } = EIP712.sign( + Domain(gov), + "Ballot", + { + proposalId, + support: true, + }, + Types, + pkbA1 + // unlockedAccount(a1).secretKey - this doesn't work with Hardhat + ); + + /// @dev Unneeded variable removed for optimization + // let beforeFors = (await gov.proposals.call(proposalId)).forVotes; + await mineBlock(); + const tx = await gov.castVoteBySig(proposalId, true, v, r, s, { from: a1 }); + expect(tx.gasUsed < 80000); + + let proposal = await gov.proposals.call(proposalId); + let expectedVotes = await staking.getPriorVotes.call( + a1, + proposal.startBlock.toString(), + proposal.startTime.toString() + ); + let afterFors = (await gov.proposals.call(proposalId)).forVotes; + expect(new BigNumber(afterFors).toString()).to.be.equal( + new BigNumber(expectedVotes.toString()).toString() + ); + }); + }); + + it("receipt uses one load", async () => { + /// @dev optimization metric: 87ms + let actor = accounts[2]; + let actor2 = accounts[3]; + await enfranchise(token, staking, actor, QUORUM_VOTES); + await enfranchise(token, staking, actor2, QUORUM_VOTES.multipliedBy(2)); + + /// @dev optimization metric: 180ms + await gov.propose(targets, values, signatures, callDatas, "do nothing", { + from: actor, + }); + proposalId = await gov.latestProposalIds.call(actor); + + /// @dev optimization metric: 159ms + await mineBlock(); + await mineBlock(); + await gov.castVote(proposalId, true, { from: actor }); + await gov.castVote(proposalId, false, { from: actor2 }); + + let trxReceipt = await gov.getReceipt.call(proposalId, actor); + let trxReceipt2 = await gov.getReceipt.call(proposalId, actor2); + + /// @dev optimization metric: 169ms + let proposal = await gov.proposals.call(proposalId); + let expectedVotes = await staking.getPriorVotes.call( + actor, + proposal.startBlock.toString(), + proposal.startTime.toString() + ); + let expectedVotes2 = await staking.getPriorVotes.call( + actor2, + proposal.startBlock.toString(), + proposal.startTime.toString() + ); + + expect(new BigNumber(trxReceipt.votes.toString()).toString()).to.be.equal( + new BigNumber(expectedVotes.toString()).toString() + ); + expect(trxReceipt.hasVoted).to.be.equal(true); + expect(trxReceipt.support).to.be.equal(true); + + expect(new BigNumber(trxReceipt2.votes.toString()).toString()).to.be.equal( + new BigNumber(expectedVotes2.toString()).toString() + ); + expect(trxReceipt2.hasVoted).to.be.equal(true); + expect(trxReceipt2.support).to.be.equal(false); + }); + }); + + describe("Check votes for a proposal creator:", () => { + it("compare votes", async () => { + let actor = accounts[4]; + let amount = etherMantissa(1000000); + await token.transfer(actor, amount); + await token.approve(staking.address, amount, { from: actor }); + let kickoffTS = await staking.kickoffTS.call(); + await staking.stake(amount, kickoffTS.add(new BN(TWO_WEEKS)), actor, actor, { + from: actor, + }); + + await gov.propose(targets, values, signatures, callDatas, "do nothing", { + from: actor, + }); + proposalId = await gov.latestProposalIds.call(actor); + + let proposal = await gov.proposals.call(proposalId); + expect(proposal.forVotes.toNumber()).to.be.equal(0); + + await mineBlock(); + await gov.castVote(proposalId, true, { from: actor }); + + proposal = await gov.proposals.call(proposalId); + let expectedVotes = await staking.getPriorVotes.call( + actor, + proposal.startBlock, + proposal.startTime + ); + expect(proposal.forVotes.toString()).to.be.equal(expectedVotes.toString()); + let receipt = await gov.getReceipt.call(proposalId, actor); + expect(receipt.votes.toString()).to.be.equal(expectedVotes.toString()); + // console.log("\n" + proposal.forVotes.toString()); + }); + }); }); diff --git a/tests/Governance/GovernorAlpha/ProposeTest.js b/tests/Governance/GovernorAlpha/ProposeTest.js index 6352ee3e3..a3d8ca98e 100644 --- a/tests/Governance/GovernorAlpha/ProposeTest.js +++ b/tests/Governance/GovernorAlpha/ProposeTest.js @@ -28,181 +28,232 @@ const TOTAL_SUPPLY = etherMantissa(1000000000); const DELAY = 86400 * 14; contract("GovernorAlpha#propose/5", (accounts) => { - let token, staking, gov, root, acct; - let trivialProposal, targets, values, signatures, callDatas; - let proposalBlock; - - before(async () => { - [root, acct, ...accounts] = accounts; - token = await TestToken.new("TestToken", "TST", 18, TOTAL_SUPPLY); - - let stakingLogic = await StakingLogic.new(token.address); - staking = await StakingProxy.new(token.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingLogic.at(staking.address); - - gov = await GovernorAlpha.new(address(0), staking.address, address(0), 4, 0); - - // Upgradable Vesting Registry - vestingRegistryLogic = await VestingRegistryLogic.new(); - vesting = await VestingRegistryProxy.new(); - await vesting.setImplementation(vestingRegistryLogic.address); - vesting = await VestingRegistryLogic.at(vesting.address); - - await staking.setVestingRegistry(vesting.address); - - targets = [root]; - // values = ["0"]; - values = [new BN("0")]; - signatures = ["getBalanceOf(address)"]; - callDatas = [encodeParameters(["address"], [acct])]; - - await token.approve(staking.address, QUORUM_VOTES); - let kickoffTS = await staking.kickoffTS.call(); - let stakingDate = kickoffTS.add(new BN(DELAY)); - await staking.stake(QUORUM_VOTES, stakingDate, acct, acct); - await staking.delegate(root, stakingDate, { from: acct }); - - await gov.propose(targets, values, signatures, callDatas, "do nothing"); - - proposalBlock = +(await web3.eth.getBlockNumber()); - proposalId = await gov.latestProposalIds.call(root); - trivialProposal = await gov.proposals.call(proposalId); - }); - - describe("simple initialization", () => { - it("ID is set to a globally unique identifier", async () => { - expect(trivialProposal.id.toString()).to.be.equal(proposalId.toString()); - }); - - it("Proposer is set to the sender", async () => { - expect(trivialProposal.proposer).to.be.equal(root); - }); - - it("Start block is set to the current block number plus vote delay", async () => { - expect(trivialProposal.startBlock.toString()).to.be.equal(proposalBlock + 1 + ""); - }); - - it("End block is set to the current block number plus the sum of vote delay and vote period", async () => { - expect(trivialProposal.endBlock.toString()).to.be.equal(proposalBlock + 1 + 2880 + ""); - }); - - it("ForVotes and AgainstVotes are initialized to zero", async () => { - expect(trivialProposal.forVotes.toString()).to.be.equal("0"); - expect(trivialProposal.againstVotes.toString()).to.be.equal("0"); - }); - - it("Executed and Canceled flags are initialized to false", async () => { - expect(trivialProposal.canceled).to.be.equal(false); - expect(trivialProposal.executed).to.be.equal(false); - }); - - it("ETA is initialized to zero", async () => { - expect(trivialProposal.eta.toString()).to.be.equal("0"); - }); - - it("Targets, Values, Signatures, Calldatas are set according to parameters", async () => { - let dynamicFields = await gov.getActions.call(trivialProposal.id); - expect(dynamicFields.targets).to.have.all.members(targets); - expect(dynamicFields.values[0].toString()).to.be.equal(values[0].toString()); - expect(dynamicFields.signatures).to.have.all.members(signatures); - expect(dynamicFields.calldatas).to.have.all.members(callDatas); - }); - - describe("This function must revert if", () => { - it("the length of the values, signatures or calldatas arrays are not the same length,", async () => { - await expectRevert( - gov.propose.call(targets.concat(root), values, signatures, callDatas, "do nothing"), - "GovernorAlpha::propose: proposal function information arity mismatch" - ); - - await expectRevert( - gov.propose.call(targets, values.concat(values), signatures, callDatas, "do nothing"), - "GovernorAlpha::propose: proposal function information arity mismatch" - ); - - await expectRevert( - gov.propose.call(targets, values, signatures.concat(signatures), callDatas, "do nothing"), - "GovernorAlpha::propose: proposal function information arity mismatch" - ); - - await expectRevert( - gov.propose.call(targets, values, signatures, callDatas.concat(callDatas), "do nothing"), - "GovernorAlpha::propose: proposal function information arity mismatch" - ); - }); - - it("or if that length is zero or greater than Max Operations.", async () => { - await expectRevert(gov.propose.call([], [], [], [], "do nothing"), "GovernorAlpha::propose: must provide actions"); - }); - - describe("Additionally, if there exists a pending or active proposal from the same proposer, we must revert.", () => { - it("reverts with pending", async () => { - await token.transfer(accounts[4], QUORUM_VOTES); - await token.approve(staking.address, QUORUM_VOTES, { from: accounts[4] }); - let kickoffTS = await staking.kickoffTS.call(); - let stakingDate = kickoffTS.add(new BN(DELAY)); - await staking.stake(QUORUM_VOTES, stakingDate, accounts[4], accounts[4], { from: accounts[4] }); - await staking.delegate(accounts[4], stakingDate, { from: accounts[4] }); - - await gov.propose(targets, values, signatures, callDatas, "do nothing", { from: accounts[4] }); - await expectRevert( - gov.propose.call(targets, values, signatures, callDatas, "do nothing", { from: accounts[4] }), - "GovernorAlpha::propose: one live proposal per proposer, found an already pending proposal" - ); - }); - - it("reverts with active", async () => { - await mineBlock(); - await mineBlock(); - - await expectRevert( - gov.propose.call(targets, values, signatures, callDatas, "do nothing"), - "GovernorAlpha::propose: one live proposal per proposer, found an already active proposal" - ); - }); - }); - }); - - it("This function returns the id of the newly created proposal. # proposalId(n) = succ(proposalId(n-1))", async () => { - await token.transfer(accounts[2], QUORUM_VOTES); - await token.approve(staking.address, QUORUM_VOTES, { from: accounts[2] }); - let kickoffTS = await staking.kickoffTS.call(); - let stakingDate = kickoffTS.add(new BN(DELAY)); - await staking.stake(QUORUM_VOTES, stakingDate, accounts[2], accounts[2], { from: accounts[2] }); - await staking.delegate(accounts[2], stakingDate, { from: accounts[2] }); - - await mineBlock(); - let nextProposalId = await gov.propose.call(targets, values, signatures, callDatas, "yoot", { from: accounts[2] }); - // let nextProposalId = await call(gov, 'propose', [targets, values, signatures, callDatas, "second proposal"], { from: accounts[2] }); - - expect(+nextProposalId).to.be.equal(+trivialProposal.id + 1); - }); - - it("emits log with id and description", async () => { - await token.transfer(accounts[3], QUORUM_VOTES); - await token.approve(staking.address, QUORUM_VOTES, { from: accounts[3] }); - let kickoffTS = await staking.kickoffTS.call(); - let stakingDate = kickoffTS.add(new BN(DELAY)); - await staking.stake(QUORUM_VOTES, stakingDate, accounts[3], accounts[3], { from: accounts[3] }); - await staking.delegate(accounts[3], stakingDate, { from: accounts[3] }); - await mineBlock(); - - // await updateTime(comp); - let result = await gov.propose(targets, values, signatures, callDatas, "second proposal", { from: accounts[3] }); - let proposalId = await gov.latestProposalIds.call(accounts[3]); - let blockNumber = (await web3.eth.getBlockNumber()) + 1; - expectEvent(result, "ProposalCreated", { - id: proposalId, - targets: targets, - // values: [new BN("0")] - signatures: signatures, - calldatas: callDatas, - startBlock: new BN(blockNumber), - endBlock: new BN(2880 + blockNumber), - description: "second proposal", - proposer: accounts[3], - }); - }); - }); + let token, staking, gov, root, acct; + let trivialProposal, targets, values, signatures, callDatas; + let proposalBlock; + + before(async () => { + [root, acct, ...accounts] = accounts; + token = await TestToken.new("TestToken", "TST", 18, TOTAL_SUPPLY); + + let stakingLogic = await StakingLogic.new(token.address); + staking = await StakingProxy.new(token.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); + + gov = await GovernorAlpha.new(address(0), staking.address, address(0), 4, 0); + + // Upgradable Vesting Registry + vestingRegistryLogic = await VestingRegistryLogic.new(); + vesting = await VestingRegistryProxy.new(); + await vesting.setImplementation(vestingRegistryLogic.address); + vesting = await VestingRegistryLogic.at(vesting.address); + + await staking.setVestingRegistry(vesting.address); + + targets = [root]; + // values = ["0"]; + values = [new BN("0")]; + signatures = ["getBalanceOf(address)"]; + callDatas = [encodeParameters(["address"], [acct])]; + + await token.approve(staking.address, QUORUM_VOTES); + let kickoffTS = await staking.kickoffTS.call(); + let stakingDate = kickoffTS.add(new BN(DELAY)); + await staking.stake(QUORUM_VOTES, stakingDate, acct, acct); + await staking.delegate(root, stakingDate, { from: acct }); + + await gov.propose(targets, values, signatures, callDatas, "do nothing"); + + proposalBlock = +(await web3.eth.getBlockNumber()); + proposalId = await gov.latestProposalIds.call(root); + trivialProposal = await gov.proposals.call(proposalId); + }); + + describe("simple initialization", () => { + it("ID is set to a globally unique identifier", async () => { + expect(trivialProposal.id.toString()).to.be.equal(proposalId.toString()); + }); + + it("Proposer is set to the sender", async () => { + expect(trivialProposal.proposer).to.be.equal(root); + }); + + it("Start block is set to the current block number plus vote delay", async () => { + expect(trivialProposal.startBlock.toString()).to.be.equal(proposalBlock + 1 + ""); + }); + + it("End block is set to the current block number plus the sum of vote delay and vote period", async () => { + expect(trivialProposal.endBlock.toString()).to.be.equal(proposalBlock + 1 + 2880 + ""); + }); + + it("ForVotes and AgainstVotes are initialized to zero", async () => { + expect(trivialProposal.forVotes.toString()).to.be.equal("0"); + expect(trivialProposal.againstVotes.toString()).to.be.equal("0"); + }); + + it("Executed and Canceled flags are initialized to false", async () => { + expect(trivialProposal.canceled).to.be.equal(false); + expect(trivialProposal.executed).to.be.equal(false); + }); + + it("ETA is initialized to zero", async () => { + expect(trivialProposal.eta.toString()).to.be.equal("0"); + }); + + it("Targets, Values, Signatures, Calldatas are set according to parameters", async () => { + let dynamicFields = await gov.getActions.call(trivialProposal.id); + expect(dynamicFields.targets).to.have.all.members(targets); + expect(dynamicFields.values[0].toString()).to.be.equal(values[0].toString()); + expect(dynamicFields.signatures).to.have.all.members(signatures); + expect(dynamicFields.calldatas).to.have.all.members(callDatas); + }); + + describe("This function must revert if", () => { + it("the length of the values, signatures or calldatas arrays are not the same length,", async () => { + await expectRevert( + gov.propose.call( + targets.concat(root), + values, + signatures, + callDatas, + "do nothing" + ), + "GovernorAlpha::propose: proposal function information arity mismatch" + ); + + await expectRevert( + gov.propose.call( + targets, + values.concat(values), + signatures, + callDatas, + "do nothing" + ), + "GovernorAlpha::propose: proposal function information arity mismatch" + ); + + await expectRevert( + gov.propose.call( + targets, + values, + signatures.concat(signatures), + callDatas, + "do nothing" + ), + "GovernorAlpha::propose: proposal function information arity mismatch" + ); + + await expectRevert( + gov.propose.call( + targets, + values, + signatures, + callDatas.concat(callDatas), + "do nothing" + ), + "GovernorAlpha::propose: proposal function information arity mismatch" + ); + }); + + it("or if that length is zero or greater than Max Operations.", async () => { + await expectRevert( + gov.propose.call([], [], [], [], "do nothing"), + "GovernorAlpha::propose: must provide actions" + ); + }); + + describe("Additionally, if there exists a pending or active proposal from the same proposer, we must revert.", () => { + it("reverts with pending", async () => { + await token.transfer(accounts[4], QUORUM_VOTES); + await token.approve(staking.address, QUORUM_VOTES, { from: accounts[4] }); + let kickoffTS = await staking.kickoffTS.call(); + let stakingDate = kickoffTS.add(new BN(DELAY)); + await staking.stake(QUORUM_VOTES, stakingDate, accounts[4], accounts[4], { + from: accounts[4], + }); + await staking.delegate(accounts[4], stakingDate, { from: accounts[4] }); + + await gov.propose(targets, values, signatures, callDatas, "do nothing", { + from: accounts[4], + }); + await expectRevert( + gov.propose.call(targets, values, signatures, callDatas, "do nothing", { + from: accounts[4], + }), + "GovernorAlpha::propose: one live proposal per proposer, found an already pending proposal" + ); + }); + + it("reverts with active", async () => { + await mineBlock(); + await mineBlock(); + + await expectRevert( + gov.propose.call(targets, values, signatures, callDatas, "do nothing"), + "GovernorAlpha::propose: one live proposal per proposer, found an already active proposal" + ); + }); + }); + }); + + it("This function returns the id of the newly created proposal. # proposalId(n) = succ(proposalId(n-1))", async () => { + await token.transfer(accounts[2], QUORUM_VOTES); + await token.approve(staking.address, QUORUM_VOTES, { from: accounts[2] }); + let kickoffTS = await staking.kickoffTS.call(); + let stakingDate = kickoffTS.add(new BN(DELAY)); + await staking.stake(QUORUM_VOTES, stakingDate, accounts[2], accounts[2], { + from: accounts[2], + }); + await staking.delegate(accounts[2], stakingDate, { from: accounts[2] }); + + await mineBlock(); + let nextProposalId = await gov.propose.call( + targets, + values, + signatures, + callDatas, + "yoot", + { from: accounts[2] } + ); + // let nextProposalId = await call(gov, 'propose', [targets, values, signatures, callDatas, "second proposal"], { from: accounts[2] }); + + expect(+nextProposalId).to.be.equal(+trivialProposal.id + 1); + }); + + it("emits log with id and description", async () => { + await token.transfer(accounts[3], QUORUM_VOTES); + await token.approve(staking.address, QUORUM_VOTES, { from: accounts[3] }); + let kickoffTS = await staking.kickoffTS.call(); + let stakingDate = kickoffTS.add(new BN(DELAY)); + await staking.stake(QUORUM_VOTES, stakingDate, accounts[3], accounts[3], { + from: accounts[3], + }); + await staking.delegate(accounts[3], stakingDate, { from: accounts[3] }); + await mineBlock(); + + // await updateTime(comp); + let result = await gov.propose( + targets, + values, + signatures, + callDatas, + "second proposal", + { from: accounts[3] } + ); + let proposalId = await gov.latestProposalIds.call(accounts[3]); + let blockNumber = (await web3.eth.getBlockNumber()) + 1; + expectEvent(result, "ProposalCreated", { + id: proposalId, + targets: targets, + // values: [new BN("0")] + signatures: signatures, + calldatas: callDatas, + startBlock: new BN(blockNumber), + endBlock: new BN(2880 + blockNumber), + description: "second proposal", + proposer: accounts[3], + }); + }); + }); }); diff --git a/tests/Governance/GovernorAlpha/QueueTest.js b/tests/Governance/GovernorAlpha/QueueTest.js index b3a5fc13c..9c88f52ea 100644 --- a/tests/Governance/GovernorAlpha/QueueTest.js +++ b/tests/Governance/GovernorAlpha/QueueTest.js @@ -9,7 +9,12 @@ const { expectRevert, BN } = require("@openzeppelin/test-helpers"); -const { etherMantissa, encodeParameters, mineBlock, increaseTime } = require("../../Utils/Ethereum"); +const { + etherMantissa, + encodeParameters, + mineBlock, + increaseTime, +} = require("../../Utils/Ethereum"); const GovernorAlpha = artifacts.require("GovernorAlphaMockup"); const Timelock = artifacts.require("TimelockHarness"); @@ -26,95 +31,101 @@ const QUORUM_VOTES = etherMantissa(4000000); const TOTAL_SUPPLY = etherMantissa(10000000000000); /// @dev increased to share the same value in both tests. async function enfranchise(token, staking, actor, amount) { - await token.transfer(actor, amount); - await token.approve(staking.address, amount, { from: actor }); - let kickoffTS = await staking.kickoffTS.call(); - let stakingDate = kickoffTS.add(new BN(DELAY)); - //Upgradable Vesting Registry - vestingRegistryLogic = await VestingRegistryLogic.new(); - vesting = await VestingRegistryProxy.new(); - await vesting.setImplementation(vestingRegistryLogic.address); - vesting = await VestingRegistryLogic.at(vesting.address); - - await staking.setVestingRegistry(vesting.address); - await staking.stake(amount, stakingDate, actor, actor, { from: actor }); - - await staking.delegate(actor, stakingDate, { from: actor }); + await token.transfer(actor, amount); + await token.approve(staking.address, amount, { from: actor }); + let kickoffTS = await staking.kickoffTS.call(); + let stakingDate = kickoffTS.add(new BN(DELAY)); + //Upgradable Vesting Registry + vestingRegistryLogic = await VestingRegistryLogic.new(); + vesting = await VestingRegistryProxy.new(); + await vesting.setImplementation(vestingRegistryLogic.address); + vesting = await VestingRegistryLogic.at(vesting.address); + + await staking.setVestingRegistry(vesting.address); + await staking.stake(amount, stakingDate, actor, actor, { from: actor }); + + await staking.delegate(actor, stakingDate, { from: actor }); } contract("GovernorAlpha#queue/1", (accounts) => { - let root, a1, a2; - let token, staking, gov; - - before(async () => { - [root, a1, a2, ...accounts] = accounts; - - const timelock = await Timelock.new(root, DELAY); - token = await TestToken.new("TestToken", "TST", 18, TOTAL_SUPPLY); - - let stakingLogic = await StakingLogic.new(token.address); - staking = await StakingProxy.new(token.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingLogic.at(staking.address); - - gov = await GovernorAlpha.new(timelock.address, staking.address, root, 4, 0); - - await timelock.harnessSetAdmin(gov.address); - - await enfranchise(token, staking, a1, QUORUM_VOTES); - await mineBlock(); - }); - - describe("overlapping actions", () => { - it("reverts on queueing overlapping actions in same proposal", async () => { - const targets = [staking.address, staking.address]; - const values = ["0", "0"]; - const signatures = ["getBalanceOf(address)", "getBalanceOf(address)"]; - const calldatas = [encodeParameters(["address"], [root]), encodeParameters(["address"], [root])]; - - await gov.propose(targets, values, signatures, calldatas, "do nothing", { from: a1 }); - let proposalId1 = await gov.proposalCount.call(); - await mineBlock(); - - await gov.castVote(proposalId1, true, { from: a1 }); - await advanceBlocks(10); - await expectRevert(gov.queue(proposalId1), "GovernorAlpha::_queueOrRevert: proposal action already queued at eta"); - }); - - it("reverts on queueing overlapping actions in different proposals, works if waiting; using Ganache", async () => { - await enfranchise(token, staking, a2, QUORUM_VOTES); - await mineBlock(); - - const targets = [staking.address]; - const values = ["0"]; - const signatures = ["getBalanceOf(address)"]; - const calldatas = [encodeParameters(["address"], [root])]; - - await gov.propose(targets, values, signatures, calldatas, "do nothing", { from: a1 }); - let proposalId1 = await gov.proposalCount.call(); - - await gov.propose(targets, values, signatures, calldatas, "do nothing", { from: a2 }); - let proposalId2 = await gov.proposalCount.call(); - await mineBlock(); - - await gov.castVote(proposalId1, true, { from: a1 }); - await gov.castVote(proposalId2, true, { from: a2 }); - await advanceBlocks(30); - - await expectRevert( - gov.queueProposals([proposalId1, proposalId2]), - "GovernorAlpha::_queueOrRevert: proposal action already queued at eta" - ); - - await gov.queue(proposalId1); - await increaseTime(60); - await gov.queue(proposalId2); - }); - }); + let root, a1, a2; + let token, staking, gov; + + before(async () => { + [root, a1, a2, ...accounts] = accounts; + + const timelock = await Timelock.new(root, DELAY); + token = await TestToken.new("TestToken", "TST", 18, TOTAL_SUPPLY); + + let stakingLogic = await StakingLogic.new(token.address); + staking = await StakingProxy.new(token.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); + + gov = await GovernorAlpha.new(timelock.address, staking.address, root, 4, 0); + + await timelock.harnessSetAdmin(gov.address); + + await enfranchise(token, staking, a1, QUORUM_VOTES); + await mineBlock(); + }); + + describe("overlapping actions", () => { + it("reverts on queueing overlapping actions in same proposal", async () => { + const targets = [staking.address, staking.address]; + const values = ["0", "0"]; + const signatures = ["getBalanceOf(address)", "getBalanceOf(address)"]; + const calldatas = [ + encodeParameters(["address"], [root]), + encodeParameters(["address"], [root]), + ]; + + await gov.propose(targets, values, signatures, calldatas, "do nothing", { from: a1 }); + let proposalId1 = await gov.proposalCount.call(); + await mineBlock(); + + await gov.castVote(proposalId1, true, { from: a1 }); + await advanceBlocks(10); + await expectRevert( + gov.queue(proposalId1), + "GovernorAlpha::_queueOrRevert: proposal action already queued at eta" + ); + }); + + it("reverts on queueing overlapping actions in different proposals, works if waiting; using Ganache", async () => { + await enfranchise(token, staking, a2, QUORUM_VOTES); + await mineBlock(); + + const targets = [staking.address]; + const values = ["0"]; + const signatures = ["getBalanceOf(address)"]; + const calldatas = [encodeParameters(["address"], [root])]; + + await gov.propose(targets, values, signatures, calldatas, "do nothing", { from: a1 }); + let proposalId1 = await gov.proposalCount.call(); + + await gov.propose(targets, values, signatures, calldatas, "do nothing", { from: a2 }); + let proposalId2 = await gov.proposalCount.call(); + await mineBlock(); + + await gov.castVote(proposalId1, true, { from: a1 }); + await gov.castVote(proposalId2, true, { from: a2 }); + await advanceBlocks(30); + + await expectRevert( + gov.queueProposals([proposalId1, proposalId2]), + "GovernorAlpha::_queueOrRevert: proposal action already queued at eta" + ); + + await gov.queue(proposalId1); + await increaseTime(60); + await gov.queue(proposalId2); + }); + }); }); async function advanceBlocks(number) { - for (let i = 0; i < number; i++) { - await mineBlock(); - } + for (let i = 0; i < number; i++) { + await mineBlock(); + } } diff --git a/tests/Governance/GovernorAlpha/StateTest.js b/tests/Governance/GovernorAlpha/StateTest.js index 992ce67f1..160c37e86 100644 --- a/tests/Governance/GovernorAlpha/StateTest.js +++ b/tests/Governance/GovernorAlpha/StateTest.js @@ -15,7 +15,14 @@ const { waffle } = require("hardhat"); const { loadFixture } = waffle; const { expectRevert, BN } = require("@openzeppelin/test-helpers"); -const { etherUnsigned, encodeParameters, etherMantissa, mineBlock, increaseTime, setNextBlockTimestamp } = require("../../Utils/Ethereum"); +const { + etherUnsigned, + encodeParameters, + etherMantissa, + mineBlock, + increaseTime, + setNextBlockTimestamp, +} = require("../../Utils/Ethereum"); const path = require("path"); const solparse = require("solparse"); @@ -26,14 +33,22 @@ const StakingLogic = artifacts.require("StakingMockup"); const StakingProxy = artifacts.require("StakingProxy"); const TestToken = artifacts.require("TestToken"); -const governorAlphaPath = path.join(__dirname, "../../..", "contracts", "governance/GovernorAlpha.sol"); +const governorAlphaPath = path.join( + __dirname, + "../../..", + "contracts", + "governance/GovernorAlpha.sol" +); const statesInverted = solparse - .parseFile(governorAlphaPath) - .body.find((k) => k.type === "ContractStatement") - .body.find((k) => k.name === "ProposalState").members; + .parseFile(governorAlphaPath) + .body.find((k) => k.type === "ContractStatement") + .body.find((k) => k.name === "ProposalState").members; -const states = Object.entries(statesInverted).reduce((obj, [key, value]) => ({ ...obj, [value]: key }), {}); +const states = Object.entries(statesInverted).reduce( + (obj, [key, value]) => ({ ...obj, [value]: key }), + {} +); const TWO_PERCENTAGE_VOTES = etherMantissa(200000); const QUORUM_VOTES = etherMantissa(4000000); @@ -47,289 +62,302 @@ const QUORUM = 4; const MAJORITY = 50; contract("GovernorAlpha#state/1", (accounts) => { - let token, staking, gov, root, acct, acct2, delay, timelock; - let trivialProposal, targets, values, signatures, callDatas; - - async function deploymentAndInitFixture(_wallets, _provider) { - // await setTime(100); - block = await ethers.provider.getBlock("latest"); - setNextBlockTimestamp(block.timestamp + 100); - - token = await TestToken.new("TestToken", "TST", 18, TOTAL_SUPPLY); - - await deployGovernor(); - - await token.approve(staking.address, QUORUM_VOTES); - await staking.stake(QUORUM_VOTES, block.timestamp + MAX_DURATION, root, root); - - await token.transfer(acct, QUORUM_VOTES); - await token.approve(staking.address, QUORUM_VOTES, { from: acct }); - let kickoffTS = await staking.kickoffTS.call(); - let stakingDate = kickoffTS.add(new BN(MAX_DURATION)); - await staking.stake(QUORUM_VOTES, stakingDate, acct, acct, { from: acct }); - - await token.transfer(accounts[3], TWO_PERCENTAGE_VOTES); - await token.approve(staking.address, TWO_PERCENTAGE_VOTES, { from: accounts[3] }); - kickoffTS = await staking.kickoffTS.call(); - stakingDate = kickoffTS.add(new BN(MAX_DURATION)); - await staking.stake(TWO_PERCENTAGE_VOTES, stakingDate, accounts[3], accounts[3], { from: accounts[3] }); - - targets = [root]; - values = ["0"]; - signatures = ["getBalanceOf(address)"]; - callDatas = [encodeParameters(["address"], [acct])]; - - await mineBlock(); - await updateTime(staking); - await gov.propose(targets, values, signatures, callDatas, "do nothing"); - proposalId = await gov.latestProposalIds.call(root); - trivialProposal = await gov.proposals.call(proposalId); - } - - before(async () => { - [root, acct, acct2, ...accounts] = accounts; - }); - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - it("Invalid for proposal not found", async () => { - await expectRevert(gov.state.call("5"), "GovernorAlpha::state: invalid proposal id"); - }); - - it("Pending", async () => { - expect((await gov.state.call(trivialProposal.id)).toString()).to.be.equal(states["Pending"].toString()); - }); - - it("Active", async () => { - await mineBlock(); - await mineBlock(); - expect((await gov.state.call(trivialProposal.id)).toString()).to.be.equal(states["Active"].toString()); - }); - - it("Canceled", async () => { - await mineBlock(); - await updateTime(staking); - await gov.propose(targets, values, signatures, callDatas, "do nothing", { from: acct }); - let newProposalId = await gov.proposalCount.call(); - - await gov.cancel(newProposalId); - - expect((await gov.state.call(newProposalId)).toString()).to.be.equal(states["Canceled"]); - }); - - it("Defeated by time", async () => { - // travel to end block - await advanceBlocks(20); - - expect((await gov.state(trivialProposal.id)).toString()).to.be.equal(states["Defeated"]); - }); - - it("Defeated by quorum", async () => { - await mineBlock(); - await updateTime(staking); - - await gov.propose(targets, values, signatures, callDatas, "do nothing", { from: accounts[3] }); - let newProposalId = await gov.latestProposalIds.call(accounts[3]); - await mineBlock(); - - await updateTime(staking); - await gov.castVote(newProposalId, true, { from: accounts[3] }); - await advanceBlocks(20); - - // 2% of votes - let proposal = await gov.proposals.call(newProposalId); - let totalVotes = proposal.forVotes.add(proposal.againstVotes); - expect(totalVotes).to.be.bignumber.lessThan(proposal.quorum); - - expect((await gov.state.call(newProposalId)).toString()).to.be.equal(states["Defeated"]); - }); - - it("Defeated by minPercentageVotes", async () => { - let stakeAmount = QUORUM_VOTES_MUL_2; - await token.transfer(acct2, stakeAmount); - await token.approve(staking.address, stakeAmount, { from: acct2 }); - let kickoffTS = await staking.kickoffTS.call(); - let stakingDate = kickoffTS.add(new BN(MAX_DURATION)); - await staking.stake(stakeAmount, stakingDate, acct2, acct2, { from: acct2 }); - - await mineBlock(); - await updateTime(staking); - - await gov.propose(targets, values, signatures, callDatas, "do nothing", { from: acct }); - let newProposalId = await gov.latestProposalIds.call(acct); - await mineBlock(); - - await updateTime(staking); - await gov.castVote(newProposalId, true, { from: acct }); - await gov.castVote(newProposalId, false, { from: acct2 }); - await advanceBlocks(20); - - // 48% of votes - let proposal = await gov.proposals.call(newProposalId); - let totalVotes = proposal.forVotes.add(proposal.againstVotes); - expect(totalVotes).to.be.bignumber.greaterThan(proposal.quorum); - expect(proposal.forVotes).to.be.bignumber.lessThan(totalVotes.mul(new BN(MAJORITY)).div(new BN(100))); - - expect((await gov.state.call(newProposalId)).toString()).to.be.equal(states["Defeated"]); - }); - - it("Succeeded", async () => { - await mineBlock(); - await updateTime(staking); - - await gov.propose(targets, values, signatures, callDatas, "do nothing", { from: acct }); - let newProposalId = await gov.latestProposalIds.call(acct); - await mineBlock(); - - await updateTime(staking); - await gov.castVote(newProposalId, true); - await gov.castVote(newProposalId, true, { from: accounts[3] }); - await advanceBlocks(20); - - expect((await gov.state.call(newProposalId)).toString()).to.be.equal(states["Succeeded"]); - }); - - it("Queued", async () => { - await mineBlock(); - await updateTime(staking); - await gov.propose(targets, values, signatures, callDatas, "do nothing", { from: acct }); - let newProposalId = await gov.latestProposalIds.call(acct); - await mineBlock(); - - await updateTime(staking); - await gov.castVote(newProposalId, true); - await gov.castVote(newProposalId, true, { from: accounts[3] }); - await advanceBlocks(20); - - await gov.queue(newProposalId, { from: acct }); - expect((await gov.state.call(newProposalId)).toString()).to.be.equal(states["Queued"]); - }); - - it("Expired", async () => { - await mineBlock(); - await updateTime(staking); - await gov.propose(targets, values, signatures, callDatas, "do nothing", { from: acct }); - let newProposalId = await gov.latestProposalIds.call(acct); - await mineBlock(); + let token, staking, gov, root, acct, acct2, delay, timelock; + let trivialProposal, targets, values, signatures, callDatas; + + async function deploymentAndInitFixture(_wallets, _provider) { + // await setTime(100); + block = await ethers.provider.getBlock("latest"); + setNextBlockTimestamp(block.timestamp + 100); + + token = await TestToken.new("TestToken", "TST", 18, TOTAL_SUPPLY); + + await deployGovernor(); + + await token.approve(staking.address, QUORUM_VOTES); + await staking.stake(QUORUM_VOTES, block.timestamp + MAX_DURATION, root, root); + + await token.transfer(acct, QUORUM_VOTES); + await token.approve(staking.address, QUORUM_VOTES, { from: acct }); + let kickoffTS = await staking.kickoffTS.call(); + let stakingDate = kickoffTS.add(new BN(MAX_DURATION)); + await staking.stake(QUORUM_VOTES, stakingDate, acct, acct, { from: acct }); + + await token.transfer(accounts[3], TWO_PERCENTAGE_VOTES); + await token.approve(staking.address, TWO_PERCENTAGE_VOTES, { from: accounts[3] }); + kickoffTS = await staking.kickoffTS.call(); + stakingDate = kickoffTS.add(new BN(MAX_DURATION)); + await staking.stake(TWO_PERCENTAGE_VOTES, stakingDate, accounts[3], accounts[3], { + from: accounts[3], + }); + + targets = [root]; + values = ["0"]; + signatures = ["getBalanceOf(address)"]; + callDatas = [encodeParameters(["address"], [acct])]; + + await mineBlock(); + await updateTime(staking); + await gov.propose(targets, values, signatures, callDatas, "do nothing"); + proposalId = await gov.latestProposalIds.call(root); + trivialProposal = await gov.proposals.call(proposalId); + } + + before(async () => { + [root, acct, acct2, ...accounts] = accounts; + }); + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + it("Invalid for proposal not found", async () => { + await expectRevert(gov.state.call("5"), "GovernorAlpha::state: invalid proposal id"); + }); + + it("Pending", async () => { + expect((await gov.state.call(trivialProposal.id)).toString()).to.be.equal( + states["Pending"].toString() + ); + }); + + it("Active", async () => { + await mineBlock(); + await mineBlock(); + expect((await gov.state.call(trivialProposal.id)).toString()).to.be.equal( + states["Active"].toString() + ); + }); + + it("Canceled", async () => { + await mineBlock(); + await updateTime(staking); + await gov.propose(targets, values, signatures, callDatas, "do nothing", { from: acct }); + let newProposalId = await gov.proposalCount.call(); + + await gov.cancel(newProposalId); + + expect((await gov.state.call(newProposalId)).toString()).to.be.equal(states["Canceled"]); + }); + + it("Defeated by time", async () => { + // travel to end block + await advanceBlocks(20); + + expect((await gov.state(trivialProposal.id)).toString()).to.be.equal(states["Defeated"]); + }); + + it("Defeated by quorum", async () => { + await mineBlock(); + await updateTime(staking); + + await gov.propose(targets, values, signatures, callDatas, "do nothing", { + from: accounts[3], + }); + let newProposalId = await gov.latestProposalIds.call(accounts[3]); + await mineBlock(); + + await updateTime(staking); + await gov.castVote(newProposalId, true, { from: accounts[3] }); + await advanceBlocks(20); + + // 2% of votes + let proposal = await gov.proposals.call(newProposalId); + let totalVotes = proposal.forVotes.add(proposal.againstVotes); + expect(totalVotes).to.be.bignumber.lessThan(proposal.quorum); + + expect((await gov.state.call(newProposalId)).toString()).to.be.equal(states["Defeated"]); + }); + + it("Defeated by minPercentageVotes", async () => { + let stakeAmount = QUORUM_VOTES_MUL_2; + await token.transfer(acct2, stakeAmount); + await token.approve(staking.address, stakeAmount, { from: acct2 }); + let kickoffTS = await staking.kickoffTS.call(); + let stakingDate = kickoffTS.add(new BN(MAX_DURATION)); + await staking.stake(stakeAmount, stakingDate, acct2, acct2, { from: acct2 }); + + await mineBlock(); + await updateTime(staking); + + await gov.propose(targets, values, signatures, callDatas, "do nothing", { from: acct }); + let newProposalId = await gov.latestProposalIds.call(acct); + await mineBlock(); + + await updateTime(staking); + await gov.castVote(newProposalId, true, { from: acct }); + await gov.castVote(newProposalId, false, { from: acct2 }); + await advanceBlocks(20); + + // 48% of votes + let proposal = await gov.proposals.call(newProposalId); + let totalVotes = proposal.forVotes.add(proposal.againstVotes); + expect(totalVotes).to.be.bignumber.greaterThan(proposal.quorum); + expect(proposal.forVotes).to.be.bignumber.lessThan( + totalVotes.mul(new BN(MAJORITY)).div(new BN(100)) + ); + + expect((await gov.state.call(newProposalId)).toString()).to.be.equal(states["Defeated"]); + }); + + it("Succeeded", async () => { + await mineBlock(); + await updateTime(staking); + + await gov.propose(targets, values, signatures, callDatas, "do nothing", { from: acct }); + let newProposalId = await gov.latestProposalIds.call(acct); + await mineBlock(); + + await updateTime(staking); + await gov.castVote(newProposalId, true); + await gov.castVote(newProposalId, true, { from: accounts[3] }); + await advanceBlocks(20); + + expect((await gov.state.call(newProposalId)).toString()).to.be.equal(states["Succeeded"]); + }); + + it("Queued", async () => { + await mineBlock(); + await updateTime(staking); + await gov.propose(targets, values, signatures, callDatas, "do nothing", { from: acct }); + let newProposalId = await gov.latestProposalIds.call(acct); + await mineBlock(); + + await updateTime(staking); + await gov.castVote(newProposalId, true); + await gov.castVote(newProposalId, true, { from: accounts[3] }); + await advanceBlocks(20); + + await gov.queue(newProposalId, { from: acct }); + expect((await gov.state.call(newProposalId)).toString()).to.be.equal(states["Queued"]); + }); + + it("Expired", async () => { + await mineBlock(); + await updateTime(staking); + await gov.propose(targets, values, signatures, callDatas, "do nothing", { from: acct }); + let newProposalId = await gov.latestProposalIds.call(acct); + await mineBlock(); + + await updateTime(staking); + await gov.castVote(newProposalId, true); + await gov.castVote(newProposalId, true, { from: accounts[3] }); + await advanceBlocks(20); + + await increaseTime(1); + await gov.queue(newProposalId, { from: acct }); + + let gracePeriod = await timelock.GRACE_PERIOD.call(); + let p = await gov.proposals.call(newProposalId); + let eta = etherUnsigned(p.eta); + + // await setTime(eta.plus(gracePeriod).minus(1).toNumber()); + setNextBlockTimestamp(eta.plus(gracePeriod).minus(1).toNumber()); + + expect((await gov.state.call(newProposalId)).toString()).to.be.equal(states["Queued"]); + + await increaseTime(1 /*eta.plus(gracePeriod).toNumber()*/); + await mineBlock(); + + expect((await gov.state.call(newProposalId)).toString()).to.be.equal(states["Expired"]); + }); + + it("Executed", async () => { + await mineBlock(); + await updateTime(staking); + await gov.propose(targets, values, signatures, callDatas, "do nothing", { from: acct }); + let newProposalId = await gov.latestProposalIds.call(acct); + + await mineBlock(); + await updateTime(staking); + await gov.castVote(newProposalId, true); + await gov.castVote(newProposalId, true, { from: accounts[3] }); + await advanceBlocks(20); + + await gov.queue(newProposalId, { from: acct }); + + let gracePeriod = await timelock.GRACE_PERIOD.call(); + let p = await gov.proposals.call(newProposalId); + let eta = etherUnsigned(p.eta); + + // await setTime(eta.plus(gracePeriod).minus(1).toNumber()); + setNextBlockTimestamp(eta.plus(gracePeriod).minus(1).toNumber()); + + expect((await gov.state.call(newProposalId)).toString()).to.be.equal(states["Queued"]); + await gov.execute(newProposalId, { from: acct }); + + expect((await gov.state.call(newProposalId)).toString()).to.be.equal(states["Executed"]); + + // still executed even though would be expired + // await setTime(eta.plus(gracePeriod).toNumber()); + setNextBlockTimestamp(eta.plus(gracePeriod).toNumber()); + + expect((await gov.state.call(newProposalId)).toString()).to.be.equal(states["Executed"]); + }); + + it("Shouldn't be canceled", async () => { + await deployGovernor(); + await token.approve(staking.address, TOTAL_SUPPLY); + let kickoffTS = await staking.kickoffTS.call(); - await updateTime(staking); - await gov.castVote(newProposalId, true); - await gov.castVote(newProposalId, true, { from: accounts[3] }); - await advanceBlocks(20); - - await increaseTime(1); - await gov.queue(newProposalId, { from: acct }); - - let gracePeriod = await timelock.GRACE_PERIOD.call(); - let p = await gov.proposals.call(newProposalId); - let eta = etherUnsigned(p.eta); - - // await setTime(eta.plus(gracePeriod).minus(1).toNumber()); - setNextBlockTimestamp(eta.plus(gracePeriod).minus(1).toNumber()); - - expect((await gov.state.call(newProposalId)).toString()).to.be.equal(states["Queued"]); - - await increaseTime(1 /*eta.plus(gracePeriod).toNumber()*/); - await mineBlock(); - - expect((await gov.state.call(newProposalId)).toString()).to.be.equal(states["Expired"]); - }); - - it("Executed", async () => { - await mineBlock(); - await updateTime(staking); - await gov.propose(targets, values, signatures, callDatas, "do nothing", { from: acct }); - let newProposalId = await gov.latestProposalIds.call(acct); - - await mineBlock(); - await updateTime(staking); - await gov.castVote(newProposalId, true); - await gov.castVote(newProposalId, true, { from: accounts[3] }); - await advanceBlocks(20); - - await gov.queue(newProposalId, { from: acct }); - - let gracePeriod = await timelock.GRACE_PERIOD.call(); - let p = await gov.proposals.call(newProposalId); - let eta = etherUnsigned(p.eta); - - // await setTime(eta.plus(gracePeriod).minus(1).toNumber()); - setNextBlockTimestamp(eta.plus(gracePeriod).minus(1).toNumber()); - - expect((await gov.state.call(newProposalId)).toString()).to.be.equal(states["Queued"]); - await gov.execute(newProposalId, { from: acct }); + // stakes tokens for user 1, 99% of voting power + await staking.stake(99000, kickoffTS.add(new BN(DELAY)), root, root); + await mineBlock(); - expect((await gov.state.call(newProposalId)).toString()).to.be.equal(states["Executed"]); - - // still executed even though would be expired - // await setTime(eta.plus(gracePeriod).toNumber()); - setNextBlockTimestamp(eta.plus(gracePeriod).toNumber()); - - expect((await gov.state.call(newProposalId)).toString()).to.be.equal(states["Executed"]); - }); + // stakes tokens for user 2 (proposer), we need more than 1% of voting power + await staking.stake(1100, kickoffTS.add(new BN(DELAY)), acct, acct); + await mineBlock(); - it("Shouldn't be canceled", async () => { - await deployGovernor(); - await token.approve(staking.address, TOTAL_SUPPLY); - let kickoffTS = await staking.kickoffTS.call(); + // proposer creates proposal + await gov.propose(targets, values, signatures, callDatas, "do nothing", { from: acct }); + let proposalId = await gov.latestProposalIds.call(acct); + await mineBlock(); - // stakes tokens for user 1, 99% of voting power - await staking.stake(99000, kickoffTS.add(new BN(DELAY)), root, root); - await mineBlock(); + // voting + // await gov.castVote(proposalId, true); - // stakes tokens for user 2 (proposer), we need more than 1% of voting power - await staking.stake(1100, kickoffTS.add(new BN(DELAY)), acct, acct); - await mineBlock(); + // queue proposal + // await advanceBlocks(10); + // await gov.queue(proposalId); - // proposer creates proposal - await gov.propose(targets, values, signatures, callDatas, "do nothing", { from: acct }); - let proposalId = await gov.latestProposalIds.call(acct); - await mineBlock(); + // increase proposal threshold - stakes tokens for user 3 + // await staking.stake(10000, kickoffTS.add(new BN(DELAY)), accounts[3], accounts[3]); + // await mineBlock(); - // voting - // await gov.castVote(proposalId, true); + // cancel proposal + await expectRevert( + gov.cancel(proposalId, { from: acct }), + "GovernorAlpha::cancel: sender isn't a guardian" + ); + }); - // queue proposal - // await advanceBlocks(10); - // await gov.queue(proposalId); + async function deployGovernor() { + let stakingLogic = await StakingLogic.new(token.address); + staking = await StakingProxy.new(token.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); - // increase proposal threshold - stakes tokens for user 3 - // await staking.stake(10000, kickoffTS.add(new BN(DELAY)), accounts[3], accounts[3]); - // await mineBlock(); + delay = etherUnsigned(2 * 24 * 60 * 60).multipliedBy(2); + timelock = await Timelock.new(root, delay); + delay = etherUnsigned(10); + await timelock.setDelayWithoutChecking(delay); - // cancel proposal - await expectRevert(gov.cancel(proposalId, { from: acct }), "GovernorAlpha::cancel: sender isn't a guardian"); - }); + gov = await GovernorAlpha.new(timelock.address, staking.address, root, QUORUM, MAJORITY); - async function deployGovernor() { - let stakingLogic = await StakingLogic.new(token.address); - staking = await StakingProxy.new(token.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingLogic.at(staking.address); - - delay = etherUnsigned(2 * 24 * 60 * 60).multipliedBy(2); - timelock = await Timelock.new(root, delay); - delay = etherUnsigned(10); - await timelock.setDelayWithoutChecking(delay); - - gov = await GovernorAlpha.new(timelock.address, staking.address, root, QUORUM, MAJORITY); - - await timelock.harnessSetAdmin(gov.address); - } + await timelock.harnessSetAdmin(gov.address); + } }); async function advanceBlocks(number) { - for (let i = 0; i < number; i++) { - await mineBlock(); - } + for (let i = 0; i < number; i++) { + await mineBlock(); + } } async function updateTime(staking) { - let kickoffTS = await staking.kickoffTS.call(); - let newTime = kickoffTS.add(new BN(DELAY).mul(new BN(2))); - // await setTime(newTime); - let block = await ethers.provider.getBlock("latest"); - await setNextBlockTimestamp(Math.max(newTime.toNumber(), block.timestamp + 1)); + let kickoffTS = await staking.kickoffTS.call(); + let newTime = kickoffTS.add(new BN(DELAY).mul(new BN(2))); + // await setTime(newTime); + let block = await ethers.provider.getBlock("latest"); + await setNextBlockTimestamp(Math.max(newTime.toNumber(), block.timestamp + 1)); } diff --git a/tests/Governance/GovernorAlpha/anyone.test.js b/tests/Governance/GovernorAlpha/anyone.test.js index 191e682a4..bf9201cd3 100644 --- a/tests/Governance/GovernorAlpha/anyone.test.js +++ b/tests/Governance/GovernorAlpha/anyone.test.js @@ -34,11 +34,11 @@ const StakingProxy = artifacts.require("StakingProxy"); const SetGet = artifacts.require("setGet"); const { - time, // Convert different time units to seconds. Available helpers are: seconds, minutes, hours, days, weeks and years. - BN, // Big Number support. - constants, // Common constants, like the zero address and largest integers. - expectEvent, // Assertions for emitted events. - expectRevert, // Assertions for transactions that should fail. + time, // Convert different time units to seconds. Available helpers are: seconds, minutes, hours, days, weeks and years. + BN, // Big Number support. + constants, // Common constants, like the zero address and largest integers. + expectEvent, // Assertions for emitted events. + expectRevert, // Assertions for transactions that should fail. } = require("@openzeppelin/test-helpers"); // const web3 = require("Web3"); @@ -69,7 +69,7 @@ const stateExecuted = 7; * @return {number} Random Value */ function randomValue() { - return Math.floor(Math.random() * 1000); + return Math.floor(Math.random() * 1000); } /** @@ -82,11 +82,13 @@ function randomValue() { * @param {number} amount The amount to stake. */ async function stake(tokenInstance, stakingInstance, stakeFor, delegatee, amount) { - await tokenInstance.approve(stakingInstance.address, amount, { - from: stakeFor, - }); - let currentTimeStamp = await time.latest(); - await stakingInstance.stake(amount, currentTimeStamp.add(new BN(delay)), stakeFor, delegatee, { from: stakeFor }); + await tokenInstance.approve(stakingInstance.address, amount, { + from: stakeFor, + }); + let currentTimeStamp = await time.latest(); + await stakingInstance.stake(amount, currentTimeStamp.add(new BN(delay)), stakeFor, delegatee, { + from: stakeFor, + }); } /** @@ -95,245 +97,293 @@ async function stake(tokenInstance, stakingInstance, stakeFor, delegatee, amount * @param {number} num The block number you want to reach. */ async function advanceBlocks(num) { - let currentBlockNumber = await blockNumber(); - for (let i = currentBlockNumber; i < num; i++) { - await mineBlock(); - } + let currentBlockNumber = await blockNumber(); + for (let i = currentBlockNumber; i < num; i++) { + await mineBlock(); + } } contract("GovernorAlpha (Any User Functions)", (accounts) => { - let governorAlpha, stakingLogic, stakingProxy, timelock, testToken, setGet; - let guardianOne, guardianTwo, voterOne, voterTwo, voterThree, userOne, userTwo; - let targets, values, signatures, callDatas, eta, proposalId; - - before("Initiating Accounts & Contracts", async () => { - // Checking if we have enough accounts to test. - assert.isAtLeast(accounts.length, 7, "At least 7 accounts are required to test the contracts."); - [guardianOne, guardianTwo, voterOne, voterTwo, voterThree, userOne, userTwo] = accounts; - - // Creating the instance of Test Token. - testToken = await TestToken.new("TestToken", "TST", 18, totalSupply); - - // Creating the Staking Contract instance. - stakingLogic = await StakingLogic.new(testToken.address); - stakingProxy = await StakingProxy.new(testToken.address); - await stakingProxy.setImplementation(stakingLogic.address); - stakingLogic = await StakingLogic.at(stakingProxy.address); - - // Creating the Timelock Contract instance. - // We would be assigning the `guardianOne` as the admin for now. - timelock = await Timelock.new(guardianOne, delay); - - // Creating the Governor Contract Instance. - governorAlpha = await GovernorAlpha.new( - timelock.address, - stakingLogic.address, - guardianOne, - quorumPercentageVotes, - minPercentageVotes - ); - - // Transaction details to make the above governor as the admin of the Timelock Instance. - let target = timelock.address; - let value = zero; - let signature = "setPendingAdmin(address)"; - let callData = encodeParameters(["address"], [governorAlpha.address]); - let currentBlock = await ethers.provider.getBlock("latest"); - // console.log(currentBlock.timestamp); - // let currentBlock = await web3.eth.getBlock(await blockNumber()); - // let currentBlock = await ethers.provider.getBlock("latest"); - eta = new BN(currentBlock.timestamp).add(new BN(delay + 1)); - - // Adding the setPendingAdmin() to the Timelock Queue. - await timelock.queueTransaction(target, value, signature, callData, eta, { - from: guardianOne, - }); - // After the required delay time is over. - await increaseTime(delay + 2); - // The setPendingAdmin() transaction is executed. - await timelock.executeTransaction(target, value, signature, callData, eta, { - from: guardianOne, - }); - - // Using the current governor contract, we accept itself as the admin of Timelock. - await governorAlpha.__acceptAdmin({ from: guardianOne }); - - // Creating a new instance of SetGet - setGet = await SetGet.new(); - }); - - async function deploymentAndInitFixture(_wallets, _provider) { - // Calculating the tokens to be sent for the Voters to Stake. - let amount = new BN((quorumPercentageVotes * totalSupply + 1) / 100); - - // Minting new Tokens - await testToken.mint(guardianOne, amount * 2, { from: guardianOne }); - - // Transferring the calculated tokens. - await testToken.transfer(voterOne, amount, { from: guardianOne }); - await testToken.transfer(voterTwo, amount, { from: guardianOne }); - - // Making the Voters to stake. - await stake(testToken, stakingLogic, voterOne, constants.ZERO_ADDRESS, amount); - await stake(testToken, stakingLogic, voterTwo, constants.ZERO_ADDRESS, amount); - } - - beforeEach("", async () => { - await loadFixture(deploymentAndInitFixture); - }); - - it("Cannot execute proposal out of queue, can queue a successful proposal, emit ProposalQueued, execute it, emit ProposalExecuted, cannot remove it, and cannot execute it again", async () => { - // Proposal Parameters - targets = [testToken.address]; - values = [new BN("0")]; - signatures = ["balanceOf(address)"]; - callDatas = [encodeParameters(["address"], [voterOne])]; - - let txReceipt = await governorAlpha.propose(targets, values, signatures, callDatas, "Checking Token Balance", { from: voterOne }); - - // Getting the proposal id of the newly created proposal. - proposalId = await governorAlpha.latestProposalIds(voterOne); - - await mineBlock(); - - // Votes in majority. - await governorAlpha.castVote(proposalId, true, { from: voterOne }); - - // Finishing up the voting. - let endBlock = txReceipt["logs"]["0"]["args"].endBlock.toNumber() + 1; - await advanceBlocks(endBlock); - - // Checking current state of proposal - let currentState = await governorAlpha.state(proposalId); - - // Checking if the proposal went to Succeeded state. - assert.strictEqual(currentState.toNumber(), stateSucceeded, "The correct state was not achieved after endBlock passed."); - - /// @dev Former Cannot execute a proposal which is not queued. test - /// integrated into this one, to optimize test performance - // Trying to put the Proposal to execute. - await expectRevert( - governorAlpha.execute(proposalId, { from: userOne }), - "GovernorAlpha::execute: proposal can only be executed if it is queued" - ); - - // Puts the Proposal in Queue. - let txReceiptQueue = await governorAlpha.queue(proposalId, { from: userOne }); - - // Checking current state of proposal - currentState = await governorAlpha.state(proposalId); - - // Checking if the proposal went to Queue state. - assert.strictEqual(currentState.toNumber(), stateQueued, "The correct state was not achieved after proposal added to queue."); - - /// @dev Former "Adding a proposal to queue should emit ProposalQueued event." test - /// integrated into this one, to optimize test performance - expectEvent.inTransaction(txReceiptQueue.tx, governorAlpha, "ProposalQueued", { - id: proposalId, - }); - - /// @dev Former "Can execute a queued proposal." test - /// integrated into this one, to optimize test performance - let eta = txReceiptQueue["logs"]["0"]["args"].eta; - await time.increaseTo(eta); - await mineBlock(); - - // Puts the Proposal to execute. - txReceiptExecute = await governorAlpha.execute(proposalId, { from: userOne }); - - // Checking current state of proposal - currentState = await governorAlpha.state(proposalId); - - // Checking if the proposal went to Executed state. - assert.strictEqual(currentState.toNumber(), stateExecuted, "The correct state was not achieved after proposal executed."); - - /// @dev Former "Executing a proposal should emit the ProposalExecuted Event." test - /// integrated into this one, to optimize test performance - expectEvent.inTransaction(txReceiptExecute.tx, governorAlpha, "ProposalExecuted", { - id: proposalId, - }); - - /// @dev Former "Cannot remove a proposal which is already executed." test - /// integrated into this one, to optimize test performance - // Trying to put the Proposal to execute again. - await expectRevert(governorAlpha.cancel(proposalId, { from: userOne }), "GovernorAlpha::cancel: cannot cancel executed proposal"); - - /// @dev Former "Cannot execute a proposal which is already executed." test - /// integrated into this one, to optimize test performance - // Trying to put the Proposal to execute again. - await expectRevert( - governorAlpha.execute(proposalId, { from: userOne }), - "GovernorAlpha::execute: proposal can only be executed if it is queued" - ); - }); - - it("Cannot queue a defeated proposal.", async () => { - // Proposal Parameters - targets = [testToken.address]; - values = [new BN("0")]; - signatures = ["balanceOf(address)"]; - callDatas = [encodeParameters(["address"], [voterTwo])]; - - let txReceipt = await governorAlpha.propose(targets, values, signatures, callDatas, "Checking Token Balance", { from: voterOne }); - - // Getting the proposal id of the newly created proposal. - proposalId = await governorAlpha.latestProposalIds(voterOne); - - await mineBlock(); - - // Finishing up the voting. - let endBlock = txReceipt["logs"]["0"]["args"].endBlock.toNumber() + 1; - await advanceBlocks(endBlock); - - // Checking current state of proposal - let currentState = await governorAlpha.state(proposalId); - - // Checking if the proposal is in Defeated state. - assert.strictEqual(currentState.toNumber(), stateDefeated, "The correct state was not achieved after endBlock passed."); - - // Tries to put the Proposal in Queue. - await expectRevert( - governorAlpha.queue(proposalId, { from: userOne }), - "GovernorAlpha::queue: proposal can only be queued if it is succeeded" - ); - }); - - it("All actions mentioned in the queue of a proposal should be executed correctly.", async () => { - let value = randomValue() + 1; - // Proposal Parameters - targets = [setGet.address]; - values = [new BN("0")]; - signatures = ["set(uint256)"]; - callDatas = [encodeParameters(["uint256"], [value])]; - - let txReceipt = await governorAlpha.propose(targets, values, signatures, callDatas, "Setting new Value", { from: voterOne }); - - // Getting the proposal id of the newly created proposal. - proposalId = await governorAlpha.latestProposalIds(voterOne); - - await mineBlock(); - - // Votes in majority. - await governorAlpha.castVote(proposalId, true, { from: voterOne }); - - // Finishing up the voting. - let endBlock = txReceipt["logs"]["0"]["args"].endBlock.toNumber() + 1; - await advanceBlocks(endBlock); - - // Puts the Proposal in Queue. - txReceipt = await governorAlpha.queue(proposalId, { from: userOne }); - - let eta = txReceipt["logs"]["0"]["args"].eta; - await time.increaseTo(eta); - await mineBlock(); - - // Puts the Proposal to execute. - await governorAlpha.execute(proposalId, { from: userOne }); - - // Getting the value has been updated. - let cValue = await setGet.value(); - - // Checking if the value in the contract and the expected value is same. - assert.strictEqual(cValue.toNumber(), value, "Value was not correctly updated in the contract."); - }); + let governorAlpha, stakingLogic, stakingProxy, timelock, testToken, setGet; + let guardianOne, guardianTwo, voterOne, voterTwo, voterThree, userOne, userTwo; + let targets, values, signatures, callDatas, eta, proposalId; + + before("Initiating Accounts & Contracts", async () => { + // Checking if we have enough accounts to test. + assert.isAtLeast( + accounts.length, + 7, + "At least 7 accounts are required to test the contracts." + ); + [guardianOne, guardianTwo, voterOne, voterTwo, voterThree, userOne, userTwo] = accounts; + + // Creating the instance of Test Token. + testToken = await TestToken.new("TestToken", "TST", 18, totalSupply); + + // Creating the Staking Contract instance. + stakingLogic = await StakingLogic.new(testToken.address); + stakingProxy = await StakingProxy.new(testToken.address); + await stakingProxy.setImplementation(stakingLogic.address); + stakingLogic = await StakingLogic.at(stakingProxy.address); + + // Creating the Timelock Contract instance. + // We would be assigning the `guardianOne` as the admin for now. + timelock = await Timelock.new(guardianOne, delay); + + // Creating the Governor Contract Instance. + governorAlpha = await GovernorAlpha.new( + timelock.address, + stakingLogic.address, + guardianOne, + quorumPercentageVotes, + minPercentageVotes + ); + + // Transaction details to make the above governor as the admin of the Timelock Instance. + let target = timelock.address; + let value = zero; + let signature = "setPendingAdmin(address)"; + let callData = encodeParameters(["address"], [governorAlpha.address]); + let currentBlock = await ethers.provider.getBlock("latest"); + // console.log(currentBlock.timestamp); + // let currentBlock = await web3.eth.getBlock(await blockNumber()); + // let currentBlock = await ethers.provider.getBlock("latest"); + eta = new BN(currentBlock.timestamp).add(new BN(delay + 1)); + + // Adding the setPendingAdmin() to the Timelock Queue. + await timelock.queueTransaction(target, value, signature, callData, eta, { + from: guardianOne, + }); + // After the required delay time is over. + await increaseTime(delay + 2); + // The setPendingAdmin() transaction is executed. + await timelock.executeTransaction(target, value, signature, callData, eta, { + from: guardianOne, + }); + + // Using the current governor contract, we accept itself as the admin of Timelock. + await governorAlpha.__acceptAdmin({ from: guardianOne }); + + // Creating a new instance of SetGet + setGet = await SetGet.new(); + }); + + async function deploymentAndInitFixture(_wallets, _provider) { + // Calculating the tokens to be sent for the Voters to Stake. + let amount = new BN((quorumPercentageVotes * totalSupply + 1) / 100); + + // Minting new Tokens + await testToken.mint(guardianOne, amount * 2, { from: guardianOne }); + + // Transferring the calculated tokens. + await testToken.transfer(voterOne, amount, { from: guardianOne }); + await testToken.transfer(voterTwo, amount, { from: guardianOne }); + + // Making the Voters to stake. + await stake(testToken, stakingLogic, voterOne, constants.ZERO_ADDRESS, amount); + await stake(testToken, stakingLogic, voterTwo, constants.ZERO_ADDRESS, amount); + } + + beforeEach("", async () => { + await loadFixture(deploymentAndInitFixture); + }); + + it("Cannot execute proposal out of queue, can queue a successful proposal, emit ProposalQueued, execute it, emit ProposalExecuted, cannot remove it, and cannot execute it again", async () => { + // Proposal Parameters + targets = [testToken.address]; + values = [new BN("0")]; + signatures = ["balanceOf(address)"]; + callDatas = [encodeParameters(["address"], [voterOne])]; + + let txReceipt = await governorAlpha.propose( + targets, + values, + signatures, + callDatas, + "Checking Token Balance", + { from: voterOne } + ); + + // Getting the proposal id of the newly created proposal. + proposalId = await governorAlpha.latestProposalIds(voterOne); + + await mineBlock(); + + // Votes in majority. + await governorAlpha.castVote(proposalId, true, { from: voterOne }); + + // Finishing up the voting. + let endBlock = txReceipt["logs"]["0"]["args"].endBlock.toNumber() + 1; + await advanceBlocks(endBlock); + + // Checking current state of proposal + let currentState = await governorAlpha.state(proposalId); + + // Checking if the proposal went to Succeeded state. + assert.strictEqual( + currentState.toNumber(), + stateSucceeded, + "The correct state was not achieved after endBlock passed." + ); + + /// @dev Former Cannot execute a proposal which is not queued. test + /// integrated into this one, to optimize test performance + // Trying to put the Proposal to execute. + await expectRevert( + governorAlpha.execute(proposalId, { from: userOne }), + "GovernorAlpha::execute: proposal can only be executed if it is queued" + ); + + // Puts the Proposal in Queue. + let txReceiptQueue = await governorAlpha.queue(proposalId, { from: userOne }); + + // Checking current state of proposal + currentState = await governorAlpha.state(proposalId); + + // Checking if the proposal went to Queue state. + assert.strictEqual( + currentState.toNumber(), + stateQueued, + "The correct state was not achieved after proposal added to queue." + ); + + /// @dev Former "Adding a proposal to queue should emit ProposalQueued event." test + /// integrated into this one, to optimize test performance + expectEvent.inTransaction(txReceiptQueue.tx, governorAlpha, "ProposalQueued", { + id: proposalId, + }); + + /// @dev Former "Can execute a queued proposal." test + /// integrated into this one, to optimize test performance + let eta = txReceiptQueue["logs"]["0"]["args"].eta; + await time.increaseTo(eta); + await mineBlock(); + + // Puts the Proposal to execute. + txReceiptExecute = await governorAlpha.execute(proposalId, { from: userOne }); + + // Checking current state of proposal + currentState = await governorAlpha.state(proposalId); + + // Checking if the proposal went to Executed state. + assert.strictEqual( + currentState.toNumber(), + stateExecuted, + "The correct state was not achieved after proposal executed." + ); + + /// @dev Former "Executing a proposal should emit the ProposalExecuted Event." test + /// integrated into this one, to optimize test performance + expectEvent.inTransaction(txReceiptExecute.tx, governorAlpha, "ProposalExecuted", { + id: proposalId, + }); + + /// @dev Former "Cannot remove a proposal which is already executed." test + /// integrated into this one, to optimize test performance + // Trying to put the Proposal to execute again. + await expectRevert( + governorAlpha.cancel(proposalId, { from: userOne }), + "GovernorAlpha::cancel: cannot cancel executed proposal" + ); + + /// @dev Former "Cannot execute a proposal which is already executed." test + /// integrated into this one, to optimize test performance + // Trying to put the Proposal to execute again. + await expectRevert( + governorAlpha.execute(proposalId, { from: userOne }), + "GovernorAlpha::execute: proposal can only be executed if it is queued" + ); + }); + + it("Cannot queue a defeated proposal.", async () => { + // Proposal Parameters + targets = [testToken.address]; + values = [new BN("0")]; + signatures = ["balanceOf(address)"]; + callDatas = [encodeParameters(["address"], [voterTwo])]; + + let txReceipt = await governorAlpha.propose( + targets, + values, + signatures, + callDatas, + "Checking Token Balance", + { from: voterOne } + ); + + // Getting the proposal id of the newly created proposal. + proposalId = await governorAlpha.latestProposalIds(voterOne); + + await mineBlock(); + + // Finishing up the voting. + let endBlock = txReceipt["logs"]["0"]["args"].endBlock.toNumber() + 1; + await advanceBlocks(endBlock); + + // Checking current state of proposal + let currentState = await governorAlpha.state(proposalId); + + // Checking if the proposal is in Defeated state. + assert.strictEqual( + currentState.toNumber(), + stateDefeated, + "The correct state was not achieved after endBlock passed." + ); + + // Tries to put the Proposal in Queue. + await expectRevert( + governorAlpha.queue(proposalId, { from: userOne }), + "GovernorAlpha::queue: proposal can only be queued if it is succeeded" + ); + }); + + it("All actions mentioned in the queue of a proposal should be executed correctly.", async () => { + let value = randomValue() + 1; + // Proposal Parameters + targets = [setGet.address]; + values = [new BN("0")]; + signatures = ["set(uint256)"]; + callDatas = [encodeParameters(["uint256"], [value])]; + + let txReceipt = await governorAlpha.propose( + targets, + values, + signatures, + callDatas, + "Setting new Value", + { from: voterOne } + ); + + // Getting the proposal id of the newly created proposal. + proposalId = await governorAlpha.latestProposalIds(voterOne); + + await mineBlock(); + + // Votes in majority. + await governorAlpha.castVote(proposalId, true, { from: voterOne }); + + // Finishing up the voting. + let endBlock = txReceipt["logs"]["0"]["args"].endBlock.toNumber() + 1; + await advanceBlocks(endBlock); + + // Puts the Proposal in Queue. + txReceipt = await governorAlpha.queue(proposalId, { from: userOne }); + + let eta = txReceipt["logs"]["0"]["args"].eta; + await time.increaseTo(eta); + await mineBlock(); + + // Puts the Proposal to execute. + await governorAlpha.execute(proposalId, { from: userOne }); + + // Getting the value has been updated. + let cValue = await setGet.value(); + + // Checking if the value in the contract and the expected value is same. + assert.strictEqual( + cValue.toNumber(), + value, + "Value was not correctly updated in the contract." + ); + }); }); diff --git a/tests/Governance/GovernorAlpha/guardian.test.js b/tests/Governance/GovernorAlpha/guardian.test.js index a54370301..1469c61b1 100644 --- a/tests/Governance/GovernorAlpha/guardian.test.js +++ b/tests/Governance/GovernorAlpha/guardian.test.js @@ -21,13 +21,19 @@ const StakingLogic = artifacts.require("StakingMockup"); const StakingProxy = artifacts.require("StakingProxy"); const { - time, // Convert different time units to seconds. Available helpers are: seconds, minutes, hours, days, weeks and years. - BN, // Big Number support. - constants, // Common constants, like the zero address and largest integers. - expectRevert, // Assertions for transactions that should fail. + time, // Convert different time units to seconds. Available helpers are: seconds, minutes, hours, days, weeks and years. + BN, // Big Number support. + constants, // Common constants, like the zero address and largest integers. + expectRevert, // Assertions for transactions that should fail. } = require("@openzeppelin/test-helpers"); -const { encodeParameters, increaseTime, blockNumber, mineBlock, advanceBlocks } = require("../../Utils/Ethereum"); +const { + encodeParameters, + increaseTime, + blockNumber, + mineBlock, + advanceBlocks, +} = require("../../Utils/Ethereum"); const { assert } = require("chai"); const { ethers, waffle } = require("hardhat"); @@ -58,11 +64,13 @@ const stateExecuted = 7; * @param {number} amount The amount to stake. */ async function stake(tokenInstance, stakingInstance, stakeFor, delegatee, amount) { - await tokenInstance.approve(stakingInstance.address, amount, { - from: stakeFor, - }); - let currentTimeStamp = await time.latest(); - await stakingInstance.stake(amount, currentTimeStamp.add(new BN(delay)), stakeFor, delegatee, { from: stakeFor }); + await tokenInstance.approve(stakingInstance.address, amount, { + from: stakeFor, + }); + let currentTimeStamp = await time.latest(); + await stakingInstance.stake(amount, currentTimeStamp.add(new BN(delay)), stakeFor, delegatee, { + from: stakeFor, + }); } /** @@ -78,210 +86,245 @@ async function stake(tokenInstance, stakingInstance, stakeFor, delegatee, amount }*/ contract("GovernorAlpha (Guardian Functions)", (accounts) => { - let governorAlpha, stakingLogic, stakingProxy, timelock, testToken; - let guardianOne, guardianTwo, voterOne, voterTwo, voterThree, userOne, userTwo; - let targets, values, signatures, callDatas, proposalId; - let newGovernorAlpha; - - async function deploymentAndInitFixture(_wallets, _provider) { - // Checking if we have enough accounts to test. - assert.isAtLeast(accounts.length, 7, "At least 7 accounts are required to test the contracts."); - [guardianOne, guardianTwo, voterOne, voterTwo, voterThree, userOne, userTwo] = accounts; - - // Creating the instance of Test Token. - testToken = await TestToken.new("TestToken", "TST", 18, totalSupply); - - // Creating the Staking Contract instance. - stakingLogic = await StakingLogic.new(testToken.address); - stakingProxy = await StakingProxy.new(testToken.address); - await stakingProxy.setImplementation(stakingLogic.address); - stakingLogic = await StakingLogic.at(stakingProxy.address); - - // Calculating the tokens to be sent for the Voters to Stake. - let amountOne = new BN((quorumPercentageVotes * totalSupply + 1) / 100); - let amountTwo = new BN((minPercentageVotes * totalSupply + 1) / 100); - - // Transferring the calculated tokens. - await testToken.transfer(voterOne, amountOne, { from: guardianOne }); - await testToken.transfer(voterTwo, amountTwo, { from: guardianOne }); - - // Making the Voters to stake. - await stake(testToken, stakingLogic, voterOne, constants.ZERO_ADDRESS, amountOne); - await stake(testToken, stakingLogic, voterTwo, constants.ZERO_ADDRESS, amountTwo); - - // Creating the Timelock Contract instance. - // We would be assigning the `guardianOne` as the admin for now. - timelock = await Timelock.new(guardianOne, delay); - - // Creating the Governor Contract Instance. - governorAlpha = await GovernorAlpha.new( - timelock.address, - stakingLogic.address, - guardianOne, - quorumPercentageVotes, - minPercentageVotes - ); - - // Transaction details to make the above governor as the admin of the Timelock Instance. - let target = timelock.address; - let value = zero; - let signature = "setPendingAdmin(address)"; - let callData = encodeParameters(["address"], [governorAlpha.address]); - //let currentBlock = await web3.eth.getBlock(await blockNumber()); - let currentBlock = await ethers.provider.getBlock("latest"); - let eta = new BN(currentBlock.timestamp).add(new BN(delay + 1)); - - // Adding the setPendingAdmin() to the Timelock Queue. - await timelock.queueTransaction(target, value, signature, callData, eta, { - from: guardianOne, - }); - // After the required delay time is over. - await increaseTime(delay + 2); - // The setPendingAdmin() transaction is executed. - await timelock.executeTransaction(target, value, signature, callData, eta, { - from: guardianOne, - }); - - // Using the current governor contract, we accept itself as the admin of Timelock. - await governorAlpha.__acceptAdmin({ from: guardianOne }); - - /// @dev Moved code from tests 1 and 2 - // Proposal Parameters - targets = [testToken.address]; - values = [new BN("0")]; - signatures = ["balanceOf(address)"]; - callDatas = [encodeParameters(["address"], [voterOne])]; - - let txReceipt = await governorAlpha.propose(targets, values, signatures, callDatas, "Checking Token Balance", { from: voterOne }); - - // Getting the proposal id of the newly created proposal. - proposalId = await governorAlpha.latestProposalIds(voterOne); - await mineBlock(); - - // Votes in majority. - await governorAlpha.castVote(proposalId, true, { from: voterOne }); - - // Finishing up the voting. - let endBlock = txReceipt["logs"]["0"]["args"].endBlock.toNumber() + 1; - await advanceBlocks(endBlock); - - // Checking current state of proposal - let currentState = await governorAlpha.state(proposalId); - - // Checking if the proposal went to Succeeded state. - assert.strictEqual(currentState.toNumber(), stateSucceeded, "The correct state was not achieved after endBlock passed."); - - /// @dev From last tests - // Creating the new Governor Contract Instance. - newGovernorAlpha = await GovernorAlpha.new( - timelock.address, - stakingLogic.address, - guardianTwo, - quorumPercentageVotes, - minPercentageVotes - ); - } - - beforeEach("Initiating Accounts & Contracts", async () => { - await loadFixture(deploymentAndInitFixture); - }); - - it("Remove a successful proposal which was queued even if successful.", async () => { - // Cancels the proposal by guardian. - await governorAlpha.cancel(proposalId, { from: guardianOne }); - - // Checking current state of proposal - currentState = await governorAlpha.state(proposalId); - - // Checking if the proposal went to Cancelled state. - assert.strictEqual(currentState.toNumber(), stateCancelled, "The correct state was not achieved after proposal added to queue."); - }); - - it("Cannot remove a proposal which is already executed.", async () => { - // Puts the Proposal in Queue. - await governorAlpha.queue(proposalId, { from: voterOne }); - - // Checking current state of proposal - currentState = await governorAlpha.state(proposalId); - - // Checking if the proposal went to Succeeded state. - assert.strictEqual(currentState.toNumber(), stateQueued, "The correct state was not achieved after proposal added to queue."); - - await time.increase(delay); - - // Puts the Proposal to execution. - await governorAlpha.execute(proposalId, { from: voterOne }); - - // Checking current state of proposal - currentState = await governorAlpha.state(proposalId); - - // Checking if the proposal went to Cancelled state. - assert.strictEqual(currentState.toNumber(), stateExecuted, "The correct state was not achieved after proposal executed."); - - // Vote by anyone else should now revert. - await expectRevert( - governorAlpha.cancel(proposalId, { from: guardianOne }), - "GovernorAlpha::cancel: cannot cancel executed proposal" - ); - }); - - it("Make a new Governor the admin of current Timelock with being pendingAdmin.", async () => { - // Transaction details to make the above governor as the pending admin of the Timelock Instance. - let currentBlock = await web3.eth.getBlock(await blockNumber()); - let eta = new BN(currentBlock.timestamp).add(new BN(delay + 1)); - - // Adding the new governor as the Pending Admin to the Timelock Queue. - await governorAlpha.__queueSetTimelockPendingAdmin(newGovernorAlpha.address, eta, { - from: guardianOne, - }); - // After the required delay time is over. - await time.increase(delay + 1); - // Executing the the new governor as the Pending Admin to the Timelock Queue. - await governorAlpha.__executeSetTimelockPendingAdmin(newGovernorAlpha.address, eta, { - from: guardianOne, - }); - - // Checking whether the current pending admin is set to the new governor. - let newPendingAdmin = await timelock.pendingAdmin(); - assert.strictEqual(newPendingAdmin, newGovernorAlpha.address, "Pending admin was not set correctly."); - - // Using the new governor contract, we accept itself as the admin of current Timelock. - await newGovernorAlpha.__acceptAdmin({ from: guardianTwo }); - }); - - it("Should not be able to make a new Governor the admin of current Timelock without being pendingAdmin first.", async () => { - await expectRevert( - newGovernorAlpha.__acceptAdmin({ from: guardianTwo }), - "Timelock::acceptAdmin: Call must come from pendingAdmin." - ); - }); - - it("Should be possible to abdicate being a guardian.", async () => { - // Abdicating. - await governorAlpha.__abdicate({ from: guardianOne }); - - // Checking if the current guardian is zero address. - let currentGuardian = await governorAlpha.guardian(); - assert.strictEqual(currentGuardian, constants.ZERO_ADDRESS, "Abdication was not successful."); - }); - - it("Should not be able to perform any task related to guardian after abdication.", async () => { - // Creating the new Governor Contract Instance. - newGovernorAlpha = await GovernorAlpha.new( - timelock.address, - stakingLogic.address, - guardianOne, - quorumPercentageVotes, - minPercentageVotes - ); - - // Abdicating. - await newGovernorAlpha.__abdicate({ from: guardianOne }); - - // Adding the dummy address as the Pending Admin to the Timelock Queue. - await expectRevert( - newGovernorAlpha.__queueSetTimelockPendingAdmin(voterOne, 0, { from: guardianOne }), - "GovernorAlpha::__queueSetTimelockPendingAdmin: sender must be gov guardian" - ); - }); + let governorAlpha, stakingLogic, stakingProxy, timelock, testToken; + let guardianOne, guardianTwo, voterOne, voterTwo, voterThree, userOne, userTwo; + let targets, values, signatures, callDatas, proposalId; + let newGovernorAlpha; + + async function deploymentAndInitFixture(_wallets, _provider) { + // Checking if we have enough accounts to test. + assert.isAtLeast( + accounts.length, + 7, + "At least 7 accounts are required to test the contracts." + ); + [guardianOne, guardianTwo, voterOne, voterTwo, voterThree, userOne, userTwo] = accounts; + + // Creating the instance of Test Token. + testToken = await TestToken.new("TestToken", "TST", 18, totalSupply); + + // Creating the Staking Contract instance. + stakingLogic = await StakingLogic.new(testToken.address); + stakingProxy = await StakingProxy.new(testToken.address); + await stakingProxy.setImplementation(stakingLogic.address); + stakingLogic = await StakingLogic.at(stakingProxy.address); + + // Calculating the tokens to be sent for the Voters to Stake. + let amountOne = new BN((quorumPercentageVotes * totalSupply + 1) / 100); + let amountTwo = new BN((minPercentageVotes * totalSupply + 1) / 100); + + // Transferring the calculated tokens. + await testToken.transfer(voterOne, amountOne, { from: guardianOne }); + await testToken.transfer(voterTwo, amountTwo, { from: guardianOne }); + + // Making the Voters to stake. + await stake(testToken, stakingLogic, voterOne, constants.ZERO_ADDRESS, amountOne); + await stake(testToken, stakingLogic, voterTwo, constants.ZERO_ADDRESS, amountTwo); + + // Creating the Timelock Contract instance. + // We would be assigning the `guardianOne` as the admin for now. + timelock = await Timelock.new(guardianOne, delay); + + // Creating the Governor Contract Instance. + governorAlpha = await GovernorAlpha.new( + timelock.address, + stakingLogic.address, + guardianOne, + quorumPercentageVotes, + minPercentageVotes + ); + + // Transaction details to make the above governor as the admin of the Timelock Instance. + let target = timelock.address; + let value = zero; + let signature = "setPendingAdmin(address)"; + let callData = encodeParameters(["address"], [governorAlpha.address]); + //let currentBlock = await web3.eth.getBlock(await blockNumber()); + let currentBlock = await ethers.provider.getBlock("latest"); + let eta = new BN(currentBlock.timestamp).add(new BN(delay + 1)); + + // Adding the setPendingAdmin() to the Timelock Queue. + await timelock.queueTransaction(target, value, signature, callData, eta, { + from: guardianOne, + }); + // After the required delay time is over. + await increaseTime(delay + 2); + // The setPendingAdmin() transaction is executed. + await timelock.executeTransaction(target, value, signature, callData, eta, { + from: guardianOne, + }); + + // Using the current governor contract, we accept itself as the admin of Timelock. + await governorAlpha.__acceptAdmin({ from: guardianOne }); + + /// @dev Moved code from tests 1 and 2 + // Proposal Parameters + targets = [testToken.address]; + values = [new BN("0")]; + signatures = ["balanceOf(address)"]; + callDatas = [encodeParameters(["address"], [voterOne])]; + + let txReceipt = await governorAlpha.propose( + targets, + values, + signatures, + callDatas, + "Checking Token Balance", + { from: voterOne } + ); + + // Getting the proposal id of the newly created proposal. + proposalId = await governorAlpha.latestProposalIds(voterOne); + await mineBlock(); + + // Votes in majority. + await governorAlpha.castVote(proposalId, true, { from: voterOne }); + + // Finishing up the voting. + let endBlock = txReceipt["logs"]["0"]["args"].endBlock.toNumber() + 1; + await advanceBlocks(endBlock); + + // Checking current state of proposal + let currentState = await governorAlpha.state(proposalId); + + // Checking if the proposal went to Succeeded state. + assert.strictEqual( + currentState.toNumber(), + stateSucceeded, + "The correct state was not achieved after endBlock passed." + ); + + /// @dev From last tests + // Creating the new Governor Contract Instance. + newGovernorAlpha = await GovernorAlpha.new( + timelock.address, + stakingLogic.address, + guardianTwo, + quorumPercentageVotes, + minPercentageVotes + ); + } + + beforeEach("Initiating Accounts & Contracts", async () => { + await loadFixture(deploymentAndInitFixture); + }); + + it("Remove a successful proposal which was queued even if successful.", async () => { + // Cancels the proposal by guardian. + await governorAlpha.cancel(proposalId, { from: guardianOne }); + + // Checking current state of proposal + currentState = await governorAlpha.state(proposalId); + + // Checking if the proposal went to Cancelled state. + assert.strictEqual( + currentState.toNumber(), + stateCancelled, + "The correct state was not achieved after proposal added to queue." + ); + }); + + it("Cannot remove a proposal which is already executed.", async () => { + // Puts the Proposal in Queue. + await governorAlpha.queue(proposalId, { from: voterOne }); + + // Checking current state of proposal + currentState = await governorAlpha.state(proposalId); + + // Checking if the proposal went to Succeeded state. + assert.strictEqual( + currentState.toNumber(), + stateQueued, + "The correct state was not achieved after proposal added to queue." + ); + + await time.increase(delay); + + // Puts the Proposal to execution. + await governorAlpha.execute(proposalId, { from: voterOne }); + + // Checking current state of proposal + currentState = await governorAlpha.state(proposalId); + + // Checking if the proposal went to Cancelled state. + assert.strictEqual( + currentState.toNumber(), + stateExecuted, + "The correct state was not achieved after proposal executed." + ); + + // Vote by anyone else should now revert. + await expectRevert( + governorAlpha.cancel(proposalId, { from: guardianOne }), + "GovernorAlpha::cancel: cannot cancel executed proposal" + ); + }); + + it("Make a new Governor the admin of current Timelock with being pendingAdmin.", async () => { + // Transaction details to make the above governor as the pending admin of the Timelock Instance. + let currentBlock = await web3.eth.getBlock(await blockNumber()); + let eta = new BN(currentBlock.timestamp).add(new BN(delay + 1)); + + // Adding the new governor as the Pending Admin to the Timelock Queue. + await governorAlpha.__queueSetTimelockPendingAdmin(newGovernorAlpha.address, eta, { + from: guardianOne, + }); + // After the required delay time is over. + await time.increase(delay + 1); + // Executing the the new governor as the Pending Admin to the Timelock Queue. + await governorAlpha.__executeSetTimelockPendingAdmin(newGovernorAlpha.address, eta, { + from: guardianOne, + }); + + // Checking whether the current pending admin is set to the new governor. + let newPendingAdmin = await timelock.pendingAdmin(); + assert.strictEqual( + newPendingAdmin, + newGovernorAlpha.address, + "Pending admin was not set correctly." + ); + + // Using the new governor contract, we accept itself as the admin of current Timelock. + await newGovernorAlpha.__acceptAdmin({ from: guardianTwo }); + }); + + it("Should not be able to make a new Governor the admin of current Timelock without being pendingAdmin first.", async () => { + await expectRevert( + newGovernorAlpha.__acceptAdmin({ from: guardianTwo }), + "Timelock::acceptAdmin: Call must come from pendingAdmin." + ); + }); + + it("Should be possible to abdicate being a guardian.", async () => { + // Abdicating. + await governorAlpha.__abdicate({ from: guardianOne }); + + // Checking if the current guardian is zero address. + let currentGuardian = await governorAlpha.guardian(); + assert.strictEqual( + currentGuardian, + constants.ZERO_ADDRESS, + "Abdication was not successful." + ); + }); + + it("Should not be able to perform any task related to guardian after abdication.", async () => { + // Creating the new Governor Contract Instance. + newGovernorAlpha = await GovernorAlpha.new( + timelock.address, + stakingLogic.address, + guardianOne, + quorumPercentageVotes, + minPercentageVotes + ); + + // Abdicating. + await newGovernorAlpha.__abdicate({ from: guardianOne }); + + // Adding the dummy address as the Pending Admin to the Timelock Queue. + await expectRevert( + newGovernorAlpha.__queueSetTimelockPendingAdmin(voterOne, 0, { from: guardianOne }), + "GovernorAlpha::__queueSetTimelockPendingAdmin: sender must be gov guardian" + ); + }); }); diff --git a/tests/Governance/GovernorAlpha/proposers.test.js b/tests/Governance/GovernorAlpha/proposers.test.js index 158c35a0e..ad5943ef1 100644 --- a/tests/Governance/GovernorAlpha/proposers.test.js +++ b/tests/Governance/GovernorAlpha/proposers.test.js @@ -12,10 +12,10 @@ const StakingLogic = artifacts.require("StakingMockup"); const StakingProxy = artifacts.require("StakingProxy"); const { - time, // Convert different time units to seconds. Available helpers are: seconds, minutes, hours, days, weeks and years. - BN, // Big Number support. - constants, // Common constants, like the zero address and largest integers. - expectRevert, // Assertions for transactions that should fail. + time, // Convert different time units to seconds. Available helpers are: seconds, minutes, hours, days, weeks and years. + BN, // Big Number support. + constants, // Common constants, like the zero address and largest integers. + expectRevert, // Assertions for transactions that should fail. } = require("@openzeppelin/test-helpers"); const { encodeParameters, increaseTime, blockNumber } = require("../../Utils/Ethereum"); @@ -39,98 +39,110 @@ let minPercentageVotes = 5; * @param {number} amount The amount to stake. */ async function stake(tokenInstance, stakingInstance, stakeFor, delegatee, amount) { - await tokenInstance.approve(stakingInstance.address, amount, { - from: stakeFor, - }); - let currentTimeStamp = await time.latest(); - await stakingInstance.stake(amount, currentTimeStamp.add(new BN(delay)), stakeFor, delegatee, { from: stakeFor }); + await tokenInstance.approve(stakingInstance.address, amount, { + from: stakeFor, + }); + let currentTimeStamp = await time.latest(); + await stakingInstance.stake(amount, currentTimeStamp.add(new BN(delay)), stakeFor, delegatee, { + from: stakeFor, + }); } contract("GovernorAlpha (Proposer Functions)", (accounts) => { - let governorAlpha, stakingLogic, stakingProxy, timelock, testToken; - let guardianOne, guardianTwo, voterOne, voterTwo, voterThree, userOne, userTwo; - let targets, values, signatures, callDatas, eta; - - before("Initiating Accounts & Contracts", async () => { - // Checking if we have enough accounts to test. - assert.isAtLeast(accounts.length, 7, "At least 7 accounts are required to test the contracts."); - [guardianOne, guardianTwo, voterOne, voterTwo, voterThree, userOne, userTwo] = accounts; - - // Creating the instance of Test Token. - testToken = await TestToken.new("TestToken", "TST", 18, totalSupply); - - // Creating the Staking Contract instance. - stakingLogic = await StakingLogic.new(testToken.address); - stakingProxy = await StakingProxy.new(testToken.address); - await stakingProxy.setImplementation(stakingLogic.address); - stakingLogic = await StakingLogic.at(stakingProxy.address); - - // Creating the Timelock Contract instance. - // We would be assigning the `guardianOne` as the admin for now. - timelock = await Timelock.new(guardianOne, delay); - - // Creating the Governor Contract Instance. - governorAlpha = await GovernorAlpha.new( - timelock.address, - stakingLogic.address, - guardianOne, - quorumPercentageVotes, - minPercentageVotes - ); - - // Transaction details to make the above governor as the admin of the Timelock Instance. - let target = timelock.address; - let value = zero; - let signature = "setPendingAdmin(address)"; - let callData = encodeParameters(["address"], [governorAlpha.address]); - let currentBlock = await web3.eth.getBlock(await blockNumber()); - eta = new BN(currentBlock.timestamp).add(new BN(delay + 1)); - - // Adding the setPendingAdmin() to the Timelock Queue. - await timelock.queueTransaction(target, value, signature, callData, eta, { - from: guardianOne, - }); - // After the required delay time is over. - await increaseTime(delay + 2); - // The setPendingAdmin() transaction is executed. - await timelock.executeTransaction(target, value, signature, callData, eta, { - from: guardianOne, - }); - - // Using the current governor contract, we accept itself as the admin of Timelock. - await governorAlpha.__acceptAdmin({ from: guardianOne }); - - // Calculating the tokens to be sent for the Voters to Stake. - let amountOne = new BN((quorumPercentageVotes * totalSupply + 1) / 100); - let amountTwo = new BN((minPercentageVotes * totalSupply + 1) / 100); - - // Transferring the calculated tokens. - await testToken.transfer(voterOne, amountOne, { from: guardianOne }); - await testToken.transfer(voterTwo, amountTwo, { from: guardianOne }); - - // Making the Voters to stake. - await stake(testToken, stakingLogic, voterOne, constants.ZERO_ADDRESS, amountOne); - await stake(testToken, stakingLogic, voterTwo, constants.ZERO_ADDRESS, amountTwo); - }); - - it("Should not create a new proposal if not enough staked.", async () => { - // Proposal Parameters - targets = [testToken.address]; - values = [new BN("0")]; - signatures = ["mint(address,uint256)"]; - callDatas = [encodeParameters(["address", "uint256"], [voterThree, 100])]; - - // Getting the staked value of voterThree - let blockNum = await blockNumber(); - let currentBlock = await web3.eth.getBlock(blockNum); - voterThreeStake = await stakingLogic.getPriorVotes(voterThree, blockNum - 1, currentBlock.timestamp); - - // Making sure that voterThree had no stake. - assert.strictEqual(voterThreeStake.toNumber(), 0, "Voter Three had some stake."); - - await expectRevert( - governorAlpha.propose(targets, values, signatures, callDatas, "Minting New Token", { from: voterThree }), - "GovernorAlpha::propose: proposer votes below proposal threshold" - ); - }); + let governorAlpha, stakingLogic, stakingProxy, timelock, testToken; + let guardianOne, guardianTwo, voterOne, voterTwo, voterThree, userOne, userTwo; + let targets, values, signatures, callDatas, eta; + + before("Initiating Accounts & Contracts", async () => { + // Checking if we have enough accounts to test. + assert.isAtLeast( + accounts.length, + 7, + "At least 7 accounts are required to test the contracts." + ); + [guardianOne, guardianTwo, voterOne, voterTwo, voterThree, userOne, userTwo] = accounts; + + // Creating the instance of Test Token. + testToken = await TestToken.new("TestToken", "TST", 18, totalSupply); + + // Creating the Staking Contract instance. + stakingLogic = await StakingLogic.new(testToken.address); + stakingProxy = await StakingProxy.new(testToken.address); + await stakingProxy.setImplementation(stakingLogic.address); + stakingLogic = await StakingLogic.at(stakingProxy.address); + + // Creating the Timelock Contract instance. + // We would be assigning the `guardianOne` as the admin for now. + timelock = await Timelock.new(guardianOne, delay); + + // Creating the Governor Contract Instance. + governorAlpha = await GovernorAlpha.new( + timelock.address, + stakingLogic.address, + guardianOne, + quorumPercentageVotes, + minPercentageVotes + ); + + // Transaction details to make the above governor as the admin of the Timelock Instance. + let target = timelock.address; + let value = zero; + let signature = "setPendingAdmin(address)"; + let callData = encodeParameters(["address"], [governorAlpha.address]); + let currentBlock = await web3.eth.getBlock(await blockNumber()); + eta = new BN(currentBlock.timestamp).add(new BN(delay + 1)); + + // Adding the setPendingAdmin() to the Timelock Queue. + await timelock.queueTransaction(target, value, signature, callData, eta, { + from: guardianOne, + }); + // After the required delay time is over. + await increaseTime(delay + 2); + // The setPendingAdmin() transaction is executed. + await timelock.executeTransaction(target, value, signature, callData, eta, { + from: guardianOne, + }); + + // Using the current governor contract, we accept itself as the admin of Timelock. + await governorAlpha.__acceptAdmin({ from: guardianOne }); + + // Calculating the tokens to be sent for the Voters to Stake. + let amountOne = new BN((quorumPercentageVotes * totalSupply + 1) / 100); + let amountTwo = new BN((minPercentageVotes * totalSupply + 1) / 100); + + // Transferring the calculated tokens. + await testToken.transfer(voterOne, amountOne, { from: guardianOne }); + await testToken.transfer(voterTwo, amountTwo, { from: guardianOne }); + + // Making the Voters to stake. + await stake(testToken, stakingLogic, voterOne, constants.ZERO_ADDRESS, amountOne); + await stake(testToken, stakingLogic, voterTwo, constants.ZERO_ADDRESS, amountTwo); + }); + + it("Should not create a new proposal if not enough staked.", async () => { + // Proposal Parameters + targets = [testToken.address]; + values = [new BN("0")]; + signatures = ["mint(address,uint256)"]; + callDatas = [encodeParameters(["address", "uint256"], [voterThree, 100])]; + + // Getting the staked value of voterThree + let blockNum = await blockNumber(); + let currentBlock = await web3.eth.getBlock(blockNum); + voterThreeStake = await stakingLogic.getPriorVotes( + voterThree, + blockNum - 1, + currentBlock.timestamp + ); + + // Making sure that voterThree had no stake. + assert.strictEqual(voterThreeStake.toNumber(), 0, "Voter Three had some stake."); + + await expectRevert( + governorAlpha.propose(targets, values, signatures, callDatas, "Minting New Token", { + from: voterThree, + }), + "GovernorAlpha::propose: proposer votes below proposal threshold" + ); + }); }); diff --git a/tests/Governance/GovernorAlpha/voter.test.js b/tests/Governance/GovernorAlpha/voter.test.js index f45756ac2..b6fb535f7 100644 --- a/tests/Governance/GovernorAlpha/voter.test.js +++ b/tests/Governance/GovernorAlpha/voter.test.js @@ -20,11 +20,11 @@ const StakingLogic = artifacts.require("StakingMockup"); const StakingProxy = artifacts.require("StakingProxy"); const { - time, // Convert different time units to seconds. Available helpers are: seconds, minutes, hours, days, weeks and years. - BN, // Big Number support. - constants, // Common constants, like the zero address and largest integers. - expectEvent, // Assertions for emitted events. - expectRevert, // Assertions for transactions that should fail. + time, // Convert different time units to seconds. Available helpers are: seconds, minutes, hours, days, weeks and years. + BN, // Big Number support. + constants, // Common constants, like the zero address and largest integers. + expectEvent, // Assertions for emitted events. + expectRevert, // Assertions for transactions that should fail. } = require("@openzeppelin/test-helpers"); const { encodeParameters, increaseTime, blockNumber, mineBlock } = require("../../Utils/Ethereum"); @@ -56,11 +56,13 @@ const stateQueued = 5; * @param {number} amount The amount to stake. */ async function stake(tokenInstance, stakingInstance, stakeFor, delegatee, amount) { - await tokenInstance.approve(stakingInstance.address, amount, { - from: stakeFor, - }); - let currentTimeStamp = await time.latest(); - await stakingInstance.stake(amount, currentTimeStamp.add(new BN(delay)), stakeFor, delegatee, { from: stakeFor }); + await tokenInstance.approve(stakingInstance.address, amount, { + from: stakeFor, + }); + let currentTimeStamp = await time.latest(); + await stakingInstance.stake(amount, currentTimeStamp.add(new BN(delay)), stakeFor, delegatee, { + from: stakeFor, + }); } /** @@ -69,125 +71,147 @@ async function stake(tokenInstance, stakingInstance, stakeFor, delegatee, amount * @param {number} num The block number you want to reach. */ async function advanceBlocks(num) { - let currentBlockNumber = await blockNumber(); - for (let i = currentBlockNumber; i < num; i++) { - await mineBlock(); - } + let currentBlockNumber = await blockNumber(); + for (let i = currentBlockNumber; i < num; i++) { + await mineBlock(); + } } contract("GovernorAlpha (Voter Functions)", (accounts) => { - let governorAlpha, stakingLogic, stakingProxy, timelock, testToken; - let guardianOne, guardianTwo, voterOne, voterTwo, voterThree, userOne, userTwo; - let targets, values, signatures, callDatas, eta, proposalId; - let txReceipt; - - before("Initiating Accounts & Contracts", async () => { - // Checking if we have enough accounts to test. - assert.isAtLeast(accounts.length, 7, "At least 7 accounts are required to test the contracts."); - [guardianOne, guardianTwo, voterOne, voterTwo, voterThree, userOne, userTwo] = accounts; - - // Creating the instance of Test Token. - testToken = await TestToken.new("TestToken", "TST", 18, totalSupply); - - // Creating the Staking Contract instance. - stakingLogic = await StakingLogic.new(testToken.address); - stakingProxy = await StakingProxy.new(testToken.address); - await stakingProxy.setImplementation(stakingLogic.address); - stakingLogic = await StakingLogic.at(stakingProxy.address); - - // Creating the Timelock Contract instance. - // We would be assigning the `guardianOne` as the admin for now. - timelock = await Timelock.new(guardianOne, delay); - - // Creating the Governor Contract Instance. - governorAlpha = await GovernorAlpha.new( - timelock.address, - stakingLogic.address, - guardianOne, - quorumPercentageVotes, - minPercentageVotes - ); - - // Transaction details to make the above governor as the admin of the Timelock Instance. - let target = timelock.address; - let value = zero; - let signature = "setPendingAdmin(address)"; - let callData = encodeParameters(["address"], [governorAlpha.address]); - let currentBlock = await web3.eth.getBlock(await blockNumber()); - eta = new BN(currentBlock.timestamp).add(new BN(delay + 1)); - - // Adding the setPendingAdmin() to the Timelock Queue. - await timelock.queueTransaction(target, value, signature, callData, eta, { - from: guardianOne, - }); - // After the required delay time is over. - await increaseTime(delay + 2); - // The setPendingAdmin() transaction is executed. - await timelock.executeTransaction(target, value, signature, callData, eta, { - from: guardianOne, - }); - - // Using the current governor contract, we accept itself as the admin of Timelock. - await governorAlpha.__acceptAdmin({ from: guardianOne }); - - // Calculating the tokens to be sent for the Voters to Stake. - let amountOne = new BN((quorumPercentageVotes * totalSupply + 1) / 100); - let amountTwo = new BN((minPercentageVotes * totalSupply + 1) / 100); - - // Transferring the calculated tokens. - await testToken.transfer(voterOne, amountOne, { from: guardianOne }); - await testToken.transfer(voterTwo, amountTwo, { from: guardianOne }); - - // Making the Voters to stake. - await stake(testToken, stakingLogic, voterOne, constants.ZERO_ADDRESS, amountOne); - await stake(testToken, stakingLogic, voterTwo, constants.ZERO_ADDRESS, amountTwo); - }); - - it("Voting should emit the VoteCast Event.", async () => { - // Proposal Parameters - targets = [testToken.address]; - values = [new BN("0")]; - signatures = ["balanceOf(address)"]; - callDatas = [encodeParameters(["address"], [voterOne])]; - - txReceipt = await governorAlpha.propose(targets, values, signatures, callDatas, "Checking Token Balance", { from: voterOne }); - - // Getting the proposal id of the newly created proposal. - proposalId = await governorAlpha.latestProposalIds(voterOne); - - await mineBlock(); - - // Votes in majority. - let txReceiptCastVote = await governorAlpha.castVote(proposalId, true, { from: voterOne }); - - expectEvent.inTransaction(txReceiptCastVote.tx, governorAlpha, "VoteCast", { - voter: voterOne, - proposalId: proposalId, - support: true, - }); - }); - - it("Should not be allowed to vote on a proposal with any other state than active.", async () => { - // Finishing up the voting. - let endBlock = txReceipt["logs"]["0"]["args"].endBlock.toNumber() + 1; - await advanceBlocks(endBlock); - - // Checking current state of proposal - let currentState = await governorAlpha.state(proposalId); - - // Checking if the proposal went to Succeeded state. - assert.strictEqual(currentState.toNumber(), stateSucceeded, "The correct state was not achieved after endBlock passed."); - - // Puts the Proposal in Queue. - await governorAlpha.queue(proposalId, { from: voterOne }); - - // Checking current state of proposal - currentState = await governorAlpha.state(proposalId); - - // Checking if the proposal went to Queued state. - assert.strictEqual(currentState.toNumber(), stateQueued, "The correct state was not achieved after proposal added to queue."); - - // Vote by anyone else should now revert. - await expectRevert(governorAlpha.castVote(proposalId, false, { from: voterTwo }), "GovernorAlpha::_castVote: voting is closed"); - }); + let governorAlpha, stakingLogic, stakingProxy, timelock, testToken; + let guardianOne, guardianTwo, voterOne, voterTwo, voterThree, userOne, userTwo; + let targets, values, signatures, callDatas, eta, proposalId; + let txReceipt; + + before("Initiating Accounts & Contracts", async () => { + // Checking if we have enough accounts to test. + assert.isAtLeast( + accounts.length, + 7, + "At least 7 accounts are required to test the contracts." + ); + [guardianOne, guardianTwo, voterOne, voterTwo, voterThree, userOne, userTwo] = accounts; + + // Creating the instance of Test Token. + testToken = await TestToken.new("TestToken", "TST", 18, totalSupply); + + // Creating the Staking Contract instance. + stakingLogic = await StakingLogic.new(testToken.address); + stakingProxy = await StakingProxy.new(testToken.address); + await stakingProxy.setImplementation(stakingLogic.address); + stakingLogic = await StakingLogic.at(stakingProxy.address); + + // Creating the Timelock Contract instance. + // We would be assigning the `guardianOne` as the admin for now. + timelock = await Timelock.new(guardianOne, delay); + + // Creating the Governor Contract Instance. + governorAlpha = await GovernorAlpha.new( + timelock.address, + stakingLogic.address, + guardianOne, + quorumPercentageVotes, + minPercentageVotes + ); + + // Transaction details to make the above governor as the admin of the Timelock Instance. + let target = timelock.address; + let value = zero; + let signature = "setPendingAdmin(address)"; + let callData = encodeParameters(["address"], [governorAlpha.address]); + let currentBlock = await web3.eth.getBlock(await blockNumber()); + eta = new BN(currentBlock.timestamp).add(new BN(delay + 1)); + + // Adding the setPendingAdmin() to the Timelock Queue. + await timelock.queueTransaction(target, value, signature, callData, eta, { + from: guardianOne, + }); + // After the required delay time is over. + await increaseTime(delay + 2); + // The setPendingAdmin() transaction is executed. + await timelock.executeTransaction(target, value, signature, callData, eta, { + from: guardianOne, + }); + + // Using the current governor contract, we accept itself as the admin of Timelock. + await governorAlpha.__acceptAdmin({ from: guardianOne }); + + // Calculating the tokens to be sent for the Voters to Stake. + let amountOne = new BN((quorumPercentageVotes * totalSupply + 1) / 100); + let amountTwo = new BN((minPercentageVotes * totalSupply + 1) / 100); + + // Transferring the calculated tokens. + await testToken.transfer(voterOne, amountOne, { from: guardianOne }); + await testToken.transfer(voterTwo, amountTwo, { from: guardianOne }); + + // Making the Voters to stake. + await stake(testToken, stakingLogic, voterOne, constants.ZERO_ADDRESS, amountOne); + await stake(testToken, stakingLogic, voterTwo, constants.ZERO_ADDRESS, amountTwo); + }); + + it("Voting should emit the VoteCast Event.", async () => { + // Proposal Parameters + targets = [testToken.address]; + values = [new BN("0")]; + signatures = ["balanceOf(address)"]; + callDatas = [encodeParameters(["address"], [voterOne])]; + + txReceipt = await governorAlpha.propose( + targets, + values, + signatures, + callDatas, + "Checking Token Balance", + { from: voterOne } + ); + + // Getting the proposal id of the newly created proposal. + proposalId = await governorAlpha.latestProposalIds(voterOne); + + await mineBlock(); + + // Votes in majority. + let txReceiptCastVote = await governorAlpha.castVote(proposalId, true, { from: voterOne }); + + expectEvent.inTransaction(txReceiptCastVote.tx, governorAlpha, "VoteCast", { + voter: voterOne, + proposalId: proposalId, + support: true, + }); + }); + + it("Should not be allowed to vote on a proposal with any other state than active.", async () => { + // Finishing up the voting. + let endBlock = txReceipt["logs"]["0"]["args"].endBlock.toNumber() + 1; + await advanceBlocks(endBlock); + + // Checking current state of proposal + let currentState = await governorAlpha.state(proposalId); + + // Checking if the proposal went to Succeeded state. + assert.strictEqual( + currentState.toNumber(), + stateSucceeded, + "The correct state was not achieved after endBlock passed." + ); + + // Puts the Proposal in Queue. + await governorAlpha.queue(proposalId, { from: voterOne }); + + // Checking current state of proposal + currentState = await governorAlpha.state(proposalId); + + // Checking if the proposal went to Queued state. + assert.strictEqual( + currentState.toNumber(), + stateQueued, + "The correct state was not achieved after proposal added to queue." + ); + + // Vote by anyone else should now revert. + await expectRevert( + governorAlpha.castVote(proposalId, false, { from: voterTwo }), + "GovernorAlpha::_castVote: voting is closed" + ); + }); }); diff --git a/tests/Governance/GovernorVault.js b/tests/Governance/GovernorVault.js index aa79ff840..e33ae72ae 100644 --- a/tests/Governance/GovernorVault.js +++ b/tests/Governance/GovernorVault.js @@ -24,132 +24,149 @@ const TOTAL_SUPPLY = "10000000000000000000000000"; const ZERO_ADDRESS = constants.ZERO_ADDRESS; contract("TeamVesting", (accounts) => { - const name = "Test token"; - const symbol = "TST"; - - let root, account1, account2, account3; - let token; - let vault; - - async function deploymentAndInitFixture(_wallets, _provider) { - token = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); - - vault = await GovernorVault.new(token.address); - } - - before(async () => { - [root, account1, account2, account3, ...accounts] = accounts; - }); - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - describe("transferTokens", () => { - it("Should be able to transfer tokens", async () => { - let amount = "1000"; - await token.transfer(vault.address, amount); - - let balance = await token.balanceOf.call(vault.address); - expect(balance.toString()).to.be.equal(amount); - - let tx = await vault.transferTokens(account1, token.address, amount); - - balance = await token.balanceOf.call(vault.address); - expect(balance.toString()).to.be.equal("0"); - let receiverBalance = await token.balanceOf.call(account1); - expect(receiverBalance.toString()).to.be.equal(amount); - - expectEvent(tx, "TokensTransferred", { - receiver: account1, - amount: amount, - }); - }); - - it("fails if the sender is not an owner", async () => { - await expectRevert(vault.transferTokens(account1, token.address, 100, { from: account1 }), "unauthorized"); - }); - - it("fails if the 0 receiver address is passed", async () => { - await expectRevert(vault.transferTokens(ZERO_ADDRESS, token.address, 100), "Invalid receiver address"); - }); - - it("fails if the 0 token address is passed", async () => { - await expectRevert(vault.transferTokens(account1, ZERO_ADDRESS, 100), "Invalid token address"); - }); - - it("fails if wrong token address", async () => { - await token.transfer(vault.address, 100); - - await expectRevert(vault.transferTokens(token.address, account1, 100), "revert"); - }); - - it("fails if amount passed is not available", async () => { - await expectRevert(vault.transferTokens(account1, token.address, 100), "invalid transfer"); - }); - }); - - describe("VaultController", () => { - it("Test internal uncovered function VaultController::vaultApprove", async () => { - let testCoverage = await TestCoverage.new(); - let to = account1; - let value = new BN(1000); - await testCoverage.testVaultController_vaultApprove(token.address, to, value); - - // Check the new allowance is correct - let allowance = await token.allowance(testCoverage.address, to); - // console.log("allowance: ", allowance.toString()); - expect(allowance).to.be.a.bignumber.equal(value); - - // Call again w/ new value - value = new BN(2000); - await testCoverage.testVaultController_vaultApprove(token.address, to, value); - - // Check the new allowance is again correct - allowance = await token.allowance(testCoverage.address, to); - // console.log("allowance: ", allowance.toString()); - expect(allowance).to.be.a.bignumber.equal(value); - }); - }); - - describe("transferRbtc", () => { - it("Should be able to transfer tokens", async () => { - let amount = "1000"; - let tx = await vault.sendTransaction({ from: root, value: amount }); - - expectEvent(tx, "Deposited", { - sender: root, - amount: amount, - }); - - let balance = await web3.eth.getBalance(vault.address); - expect(balance.toString()).to.be.equal(amount); - - let receiverBalanceBefore = await web3.eth.getBalance(account1); - tx = await vault.transferRbtc(account1, amount); - - balance = await web3.eth.getBalance(vault.address); - expect(balance.toString()).to.be.equal("0"); - - let receiverBalanceAfter = await web3.eth.getBalance(account1); - expect(new BN(receiverBalanceAfter).sub(new BN(receiverBalanceBefore)).toString()).to.be.equal(amount); - - expectEvent(tx, "RbtcTransferred", { - receiver: account1, - amount: amount, - }); - }); - - it("fails if the sender is not an owner", async () => { - await expectRevert(vault.transferRbtc(account1, 100, { from: account1 }), "unauthorized"); - }); - - it("fails if the 0 address is passed", async () => { - await expectRevert(vault.transferRbtc(ZERO_ADDRESS, 100), "Invalid receiver address"); - }); - - it("fails if amount passed is not available", async () => { - await expectRevert(vault.transferRbtc(account1, 100), "revert"); - }); - }); + const name = "Test token"; + const symbol = "TST"; + + let root, account1, account2, account3; + let token; + let vault; + + async function deploymentAndInitFixture(_wallets, _provider) { + token = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); + + vault = await GovernorVault.new(token.address); + } + + before(async () => { + [root, account1, account2, account3, ...accounts] = accounts; + }); + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + describe("transferTokens", () => { + it("Should be able to transfer tokens", async () => { + let amount = "1000"; + await token.transfer(vault.address, amount); + + let balance = await token.balanceOf.call(vault.address); + expect(balance.toString()).to.be.equal(amount); + + let tx = await vault.transferTokens(account1, token.address, amount); + + balance = await token.balanceOf.call(vault.address); + expect(balance.toString()).to.be.equal("0"); + let receiverBalance = await token.balanceOf.call(account1); + expect(receiverBalance.toString()).to.be.equal(amount); + + expectEvent(tx, "TokensTransferred", { + receiver: account1, + amount: amount, + }); + }); + + it("fails if the sender is not an owner", async () => { + await expectRevert( + vault.transferTokens(account1, token.address, 100, { from: account1 }), + "unauthorized" + ); + }); + + it("fails if the 0 receiver address is passed", async () => { + await expectRevert( + vault.transferTokens(ZERO_ADDRESS, token.address, 100), + "Invalid receiver address" + ); + }); + + it("fails if the 0 token address is passed", async () => { + await expectRevert( + vault.transferTokens(account1, ZERO_ADDRESS, 100), + "Invalid token address" + ); + }); + + it("fails if wrong token address", async () => { + await token.transfer(vault.address, 100); + + await expectRevert(vault.transferTokens(token.address, account1, 100), "revert"); + }); + + it("fails if amount passed is not available", async () => { + await expectRevert( + vault.transferTokens(account1, token.address, 100), + "invalid transfer" + ); + }); + }); + + describe("VaultController", () => { + it("Test internal uncovered function VaultController::vaultApprove", async () => { + let testCoverage = await TestCoverage.new(); + let to = account1; + let value = new BN(1000); + await testCoverage.testVaultController_vaultApprove(token.address, to, value); + + // Check the new allowance is correct + let allowance = await token.allowance(testCoverage.address, to); + // console.log("allowance: ", allowance.toString()); + expect(allowance).to.be.a.bignumber.equal(value); + + // Call again w/ new value + value = new BN(2000); + await testCoverage.testVaultController_vaultApprove(token.address, to, value); + + // Check the new allowance is again correct + allowance = await token.allowance(testCoverage.address, to); + // console.log("allowance: ", allowance.toString()); + expect(allowance).to.be.a.bignumber.equal(value); + }); + }); + + describe("transferRbtc", () => { + it("Should be able to transfer tokens", async () => { + let amount = "1000"; + let tx = await vault.sendTransaction({ from: root, value: amount }); + + expectEvent(tx, "Deposited", { + sender: root, + amount: amount, + }); + + let balance = await web3.eth.getBalance(vault.address); + expect(balance.toString()).to.be.equal(amount); + + let receiverBalanceBefore = await web3.eth.getBalance(account1); + tx = await vault.transferRbtc(account1, amount); + + balance = await web3.eth.getBalance(vault.address); + expect(balance.toString()).to.be.equal("0"); + + let receiverBalanceAfter = await web3.eth.getBalance(account1); + expect( + new BN(receiverBalanceAfter).sub(new BN(receiverBalanceBefore)).toString() + ).to.be.equal(amount); + + expectEvent(tx, "RbtcTransferred", { + receiver: account1, + amount: amount, + }); + }); + + it("fails if the sender is not an owner", async () => { + await expectRevert( + vault.transferRbtc(account1, 100, { from: account1 }), + "unauthorized" + ); + }); + + it("fails if the 0 address is passed", async () => { + await expectRevert(vault.transferRbtc(ZERO_ADDRESS, 100), "Invalid receiver address"); + }); + + it("fails if amount passed is not available", async () => { + await expectRevert(vault.transferRbtc(account1, 100), "revert"); + }); + }); }); diff --git a/tests/Locked/admin.test.js b/tests/Locked/admin.test.js index 31f39b8af..49b3827d6 100644 --- a/tests/Locked/admin.test.js +++ b/tests/Locked/admin.test.js @@ -18,9 +18,9 @@ const VestingFactory = artifacts.require("VestingFactory"); const VestingRegistry = artifacts.require("VestingRegistry3"); const { - BN, // Big Number support. - constants, - expectRevert, // Assertions for transactions that should fail. + BN, // Big Number support. + constants, + expectRevert, // Assertions for transactions that should fail. } = require("@openzeppelin/test-helpers"); const { assert } = require("chai"); @@ -34,115 +34,146 @@ let cliff = 1; // This is in 4 weeks. i.e. 1 * 4 weeks. let duration = 11; // This is in 4 weeks. i.e. 11 * 4 weeks. contract("Locked SOV (Admin Functions)", (accounts) => { - let sov, lockedSOV, newLockedSOV, vestingRegistry, vestingLogic, stakingLogic; - let creator, admin, newAdmin, userOne, userTwo, userThree, userFour, userFive; - let newVestingRegistry; - - async function deploymentAndInitFixture(_wallets, _provider) { - // Checking if we have enough accounts to test. - assert.isAtLeast(accounts.length, 8, "At least 8 accounts are required to test the contracts."); - [creator, admin, newAdmin, userOne, userTwo, userThree, userFour, userFive] = accounts; - - // Creating the instance of SOV Token. - sov = await SOV.new("Sovryn", "SOV", 18, zero); - wrbtc = await TestWrbtc.new(); - - // Creating the Staking Instance. - stakingLogic = await StakingLogic.new(sov.address); - staking = await StakingProxy.new(sov.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingLogic.at(staking.address); - - // Creating the FeeSharing Instance. - feeSharingProxy = await FeeSharingProxy.new(zeroAddress, staking.address); - - // Creating the Vesting Instance. - vestingLogic = await VestingLogic.new(); - vestingFactory = await VestingFactory.new(vestingLogic.address); - vestingRegistry = await VestingRegistry.new( - vestingFactory.address, - sov.address, - staking.address, - feeSharingProxy.address, - creator // This should be Governance Timelock Contract. - ); - await vestingFactory.transferOwnership(vestingRegistry.address); - - // Creating the instance of newLockedSOV Contract. - newLockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [admin]); - - // Creating the instance of LockedSOV Contract. - lockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [admin]); - - // Adding lockedSOV as an admin in the Vesting Registry. - await vestingRegistry.addAdmin(lockedSOV.address); - - newVestingRegistry = await VestingRegistry.new( - vestingFactory.address, - sov.address, - staking.address, - feeSharingProxy.address, - creator // This should be Governance Timelock Contract. - ); - } - - before("Initiating Accounts & Creating Test Token Instance.", async () => { - await loadFixture(deploymentAndInitFixture); - }); - - it("Admin should be able to add another admin.", async () => { - await lockedSOV.addAdmin(newAdmin, { from: admin }); - }); - - it("Admin should not be able to add zero address as another admin.", async () => { - await expectRevert(lockedSOV.addAdmin(zeroAddress, { from: admin }), "Invalid Address."); - }); - - it("Admin should not be able to add another admin more than once.", async () => { - await expectRevert(lockedSOV.addAdmin(newAdmin, { from: admin }), "Address is already admin."); - }); - - it("Admin should be able to remove an admin.", async () => { - await lockedSOV.removeAdmin(newAdmin, { from: admin }); - }); - - it("Admin should not be able to call removeAdmin() with a normal user address.", async () => { - await expectRevert(lockedSOV.removeAdmin(newAdmin, { from: admin }), "Address is not an admin."); - }); - - it("Admin should not be able to change the cliff and/or duration without changing the vesting registry.", async () => { - await expectRevert( - lockedSOV.changeRegistryCliffAndDuration(vestingRegistry.address, cliff + 1, duration + 1, { from: admin }), - "Vesting Registry has to be different for changing duration and cliff." - ); - }); - - it("Admin should be able to change the vestingRegistry, cliff and/or duration.", async () => { - await loadFixture(deploymentAndInitFixture); - await lockedSOV.changeRegistryCliffAndDuration(newVestingRegistry.address, cliff + 1, duration + 1, { from: admin }); - }); - - it("Admin should not be able to change the duration as zero.", async () => { - await loadFixture(deploymentAndInitFixture); - await expectRevert( - lockedSOV.changeRegistryCliffAndDuration(newVestingRegistry.address, cliff + 1, 0, { from: admin }), - "Duration cannot be zero." - ); - }); - - it("Admin should not be able to change the duration higher than 36.", async () => { - await loadFixture(deploymentAndInitFixture); - await expectRevert( - lockedSOV.changeRegistryCliffAndDuration(newVestingRegistry.address, cliff + 1, 100, { from: admin }), - "Duration is too long." - ); - }); - - it("Admin should be able to start migration.", async () => { - await lockedSOV.startMigration(newLockedSOV.address, { from: admin }); - }); - - it("Admin should not be able to start migration with locked sov as zero address.", async () => { - await expectRevert(lockedSOV.startMigration(zeroAddress, { from: admin }), "New Locked SOV Address is Invalid."); - }); + let sov, lockedSOV, newLockedSOV, vestingRegistry, vestingLogic, stakingLogic; + let creator, admin, newAdmin, userOne, userTwo, userThree, userFour, userFive; + let newVestingRegistry; + + async function deploymentAndInitFixture(_wallets, _provider) { + // Checking if we have enough accounts to test. + assert.isAtLeast( + accounts.length, + 8, + "At least 8 accounts are required to test the contracts." + ); + [creator, admin, newAdmin, userOne, userTwo, userThree, userFour, userFive] = accounts; + + // Creating the instance of SOV Token. + sov = await SOV.new("Sovryn", "SOV", 18, zero); + wrbtc = await TestWrbtc.new(); + + // Creating the Staking Instance. + stakingLogic = await StakingLogic.new(sov.address); + staking = await StakingProxy.new(sov.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); + + // Creating the FeeSharing Instance. + feeSharingProxy = await FeeSharingProxy.new(zeroAddress, staking.address); + + // Creating the Vesting Instance. + vestingLogic = await VestingLogic.new(); + vestingFactory = await VestingFactory.new(vestingLogic.address); + vestingRegistry = await VestingRegistry.new( + vestingFactory.address, + sov.address, + staking.address, + feeSharingProxy.address, + creator // This should be Governance Timelock Contract. + ); + await vestingFactory.transferOwnership(vestingRegistry.address); + + // Creating the instance of newLockedSOV Contract. + newLockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [ + admin, + ]); + + // Creating the instance of LockedSOV Contract. + lockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [ + admin, + ]); + + // Adding lockedSOV as an admin in the Vesting Registry. + await vestingRegistry.addAdmin(lockedSOV.address); + + newVestingRegistry = await VestingRegistry.new( + vestingFactory.address, + sov.address, + staking.address, + feeSharingProxy.address, + creator // This should be Governance Timelock Contract. + ); + } + + before("Initiating Accounts & Creating Test Token Instance.", async () => { + await loadFixture(deploymentAndInitFixture); + }); + + it("Admin should be able to add another admin.", async () => { + await lockedSOV.addAdmin(newAdmin, { from: admin }); + }); + + it("Admin should not be able to add zero address as another admin.", async () => { + await expectRevert(lockedSOV.addAdmin(zeroAddress, { from: admin }), "Invalid Address."); + }); + + it("Admin should not be able to add another admin more than once.", async () => { + await expectRevert( + lockedSOV.addAdmin(newAdmin, { from: admin }), + "Address is already admin." + ); + }); + + it("Admin should be able to remove an admin.", async () => { + await lockedSOV.removeAdmin(newAdmin, { from: admin }); + }); + + it("Admin should not be able to call removeAdmin() with a normal user address.", async () => { + await expectRevert( + lockedSOV.removeAdmin(newAdmin, { from: admin }), + "Address is not an admin." + ); + }); + + it("Admin should not be able to change the cliff and/or duration without changing the vesting registry.", async () => { + await expectRevert( + lockedSOV.changeRegistryCliffAndDuration( + vestingRegistry.address, + cliff + 1, + duration + 1, + { from: admin } + ), + "Vesting Registry has to be different for changing duration and cliff." + ); + }); + + it("Admin should be able to change the vestingRegistry, cliff and/or duration.", async () => { + await loadFixture(deploymentAndInitFixture); + await lockedSOV.changeRegistryCliffAndDuration( + newVestingRegistry.address, + cliff + 1, + duration + 1, + { from: admin } + ); + }); + + it("Admin should not be able to change the duration as zero.", async () => { + await loadFixture(deploymentAndInitFixture); + await expectRevert( + lockedSOV.changeRegistryCliffAndDuration(newVestingRegistry.address, cliff + 1, 0, { + from: admin, + }), + "Duration cannot be zero." + ); + }); + + it("Admin should not be able to change the duration higher than 36.", async () => { + await loadFixture(deploymentAndInitFixture); + await expectRevert( + lockedSOV.changeRegistryCliffAndDuration(newVestingRegistry.address, cliff + 1, 100, { + from: admin, + }), + "Duration is too long." + ); + }); + + it("Admin should be able to start migration.", async () => { + await lockedSOV.startMigration(newLockedSOV.address, { from: admin }); + }); + + it("Admin should not be able to start migration with locked sov as zero address.", async () => { + await expectRevert( + lockedSOV.startMigration(zeroAddress, { from: admin }), + "New Locked SOV Address is Invalid." + ); + }); }); diff --git a/tests/Locked/anyone.test.js b/tests/Locked/anyone.test.js index a99ff15f8..f4e6666c2 100644 --- a/tests/Locked/anyone.test.js +++ b/tests/Locked/anyone.test.js @@ -20,9 +20,9 @@ const VestingFactory = artifacts.require("VestingFactory"); const VestingRegistry = artifacts.require("VestingRegistry3"); const { - BN, // Big Number support. - expectRevert, - constants, // Assertions for transactions that should fail. + BN, // Big Number support. + expectRevert, + constants, // Assertions for transactions that should fail. } = require("@openzeppelin/test-helpers"); const { assert } = require("chai"); @@ -43,164 +43,198 @@ const maxRandom = 10000; * @return {number} Random Value. */ function randomValue() { - return Math.floor(Math.random() * maxRandom); + return Math.floor(Math.random() * maxRandom); } contract("Locked SOV (Any User Functions)", (accounts) => { - let sov, lockedSOV, newLockedSOV, vestingRegistry, vestingLogic, stakingLogic; - let creator, admin, newAdmin, userOne, userTwo, userThree, userFour, userFive; - - before("Initiating Accounts & Creating Test Token Instance.", async () => { - // Checking if we have enough accounts to test. - assert.isAtLeast(accounts.length, 8, "Alteast 8 accounts are required to test the contracts."); - [creator, admin, newAdmin, userOne, userTwo, userThree, userFour, userFive] = accounts; - - // Creating the instance of SOV Token. - sov = await SOV.new("Sovryn", "SOV", 18, zero); - wrbtc = await TestWrbtc.new(); - - // Creating the Staking Instance. - stakingLogic = await StakingLogic.new(sov.address); - staking = await StakingProxy.new(sov.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingLogic.at(staking.address); - - // Creating the FeeSharing Instance. - feeSharingProxy = await FeeSharingProxy.new(zeroAddress, staking.address); - - // Creating the Vesting Instance. - vestingLogic = await VestingLogic.new(); - vestingFactory = await VestingFactory.new(vestingLogic.address); - vestingRegistry = await VestingRegistry.new( - vestingFactory.address, - sov.address, - staking.address, - feeSharingProxy.address, - creator // This should be Governance Timelock Contract. - ); - await vestingFactory.transferOwnership(vestingRegistry.address); - - // Creating the instance of newLockedSOV Contract. - newLockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [admin]); - await vestingRegistry.addAdmin(newLockedSOV.address); - - // Creating the instance of LockedSOV Contract. - lockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [admin]); - - // Adding lockedSOV as an admin in the Vesting Registry. - await vestingRegistry.addAdmin(lockedSOV.address); - - /// @dev Moved from tests into init code, for speed optimization. - const infiniteTokens = maxRandom * 100; // A lot of tokens, enough to run all tests w/o extra minting - value = randomValue() + 10; - await sov.mint(userOne, infiniteTokens); - await sov.approve(lockedSOV.address, infiniteTokens, { from: userOne }); - }); - - it("Except Admin, no one should be able to add an admin.", async () => { - await expectRevert(lockedSOV.addAdmin(newAdmin, { from: userOne }), "Only admin can call this."); - }); - - it("Except Admin, no one should be able to remove an admin.", async () => { - await expectRevert(lockedSOV.removeAdmin(admin, { from: userOne }), "Only admin can call this."); - }); - - it("Except Admin, no one should be able to change the vestingRegistry, cliff and/or duration.", async () => { - await expectRevert( - lockedSOV.changeRegistryCliffAndDuration(vestingRegistry.address, cliff + 1, duration + 1, { from: userOne }), - "Only admin can call this." - ); - }); - - it("Anyone could deposit Tokens using deposit().", async () => { - let basisPoint = randomValue(); - await lockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); - }); - - it("No one could deposit Tokens using deposit() with 10000 as BasisPoint.", async () => { - let basisPoint = 10000; - await expectRevert(lockedSOV.deposit(userOne, value, basisPoint, { from: userOne }), "Basis Point has to be less than 10000."); - }); - - it("Anyone could deposit Tokens using depositSOV().", async () => { - await lockedSOV.depositSOV(userOne, value, { from: userOne }); - }); - - it("Anyone can withdraw unlocked Tokens using withdraw().", async () => { - let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. - await lockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); - await lockedSOV.withdraw(zeroAddress, { from: userOne }); - }); - - it("Anyone can withdraw unlocked Tokens to another wallet using withdraw().", async () => { - let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. - await lockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); - await lockedSOV.withdraw(userTwo, { from: userOne }); - }); - - it("Anyone can create a vesting schedule and stake tokens using createVestingAndStake().", async () => { - let value = randomValue() + 10; - await sov.mint(userThree, value); - await sov.approve(lockedSOV.address, value, { from: userThree }); - let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. - await lockedSOV.deposit(userThree, value, basisPoint, { from: userThree }); - await lockedSOV.createVestingAndStake({ from: userThree }); - }); - - it("No one can use createVestingAndStake() if he does not have any locked sov balance.", async () => { - await expectRevert(newLockedSOV.createVestingAndStake({ from: userOne }), "amount needs to be bigger than 0"); - }); - - it("Anyone can create a vesting schedule using createVesting() even with no locked sov balance.", async () => { - await lockedSOV.createVesting({ from: userOne }); - }); - - it("Anyone can use stakeTokens() to stake locked sov who already has a vesting contract.", async () => { - let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. - await lockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); - await lockedSOV.createVesting({ from: userOne }); - await lockedSOV.stakeTokens({ from: userOne }); - }); - - it("No one can use stakeTokens() who already has not created a vesting contract.", async () => { - let value = randomValue() + 10; - await sov.mint(userFive, value); - await sov.approve(lockedSOV.address, value, { from: userFive }); - let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. - await lockedSOV.deposit(userFive, value, basisPoint, { from: userFive }); - await expectRevert(lockedSOV.stakeTokens({ from: userFive }), "function call to a non-contract account"); - }); - - it("Anyone can withdraw unlocked and stake locked balance using withdrawAndStakeTokens().", async () => { - let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. - await lockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); - await lockedSOV.withdrawAndStakeTokens(userOne, { from: userOne }); - }); - - it("Anyone can withdraw unlocked and stake locked balance using withdrawAndStakeTokensFrom().", async () => { - let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. - await lockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); - await lockedSOV.withdrawAndStakeTokensFrom(userOne, { from: userTwo }); - }); - - it("Except Admin, no one should be able to start migration.", async () => { - await expectRevert(lockedSOV.startMigration(newLockedSOV.address, { from: userOne }), "Only admin can call this."); - }); - - it("No one can transfer locked balance using transfer() unless migration has started.", async () => { - let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. - await lockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); - - await expectRevert(lockedSOV.transfer({ from: userOne }), "Migration has not yet started."); - }); - - it("Anyone can transfer locked balance using transfer().", async () => { - let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. - await lockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); - - // Migratioin started by Admin - await lockedSOV.startMigration(newLockedSOV.address, { from: admin }); - - await lockedSOV.transfer({ from: userOne }); - }); + let sov, lockedSOV, newLockedSOV, vestingRegistry, vestingLogic, stakingLogic; + let creator, admin, newAdmin, userOne, userTwo, userThree, userFour, userFive; + + before("Initiating Accounts & Creating Test Token Instance.", async () => { + // Checking if we have enough accounts to test. + assert.isAtLeast( + accounts.length, + 8, + "Alteast 8 accounts are required to test the contracts." + ); + [creator, admin, newAdmin, userOne, userTwo, userThree, userFour, userFive] = accounts; + + // Creating the instance of SOV Token. + sov = await SOV.new("Sovryn", "SOV", 18, zero); + wrbtc = await TestWrbtc.new(); + + // Creating the Staking Instance. + stakingLogic = await StakingLogic.new(sov.address); + staking = await StakingProxy.new(sov.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); + + // Creating the FeeSharing Instance. + feeSharingProxy = await FeeSharingProxy.new(zeroAddress, staking.address); + + // Creating the Vesting Instance. + vestingLogic = await VestingLogic.new(); + vestingFactory = await VestingFactory.new(vestingLogic.address); + vestingRegistry = await VestingRegistry.new( + vestingFactory.address, + sov.address, + staking.address, + feeSharingProxy.address, + creator // This should be Governance Timelock Contract. + ); + await vestingFactory.transferOwnership(vestingRegistry.address); + + // Creating the instance of newLockedSOV Contract. + newLockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [ + admin, + ]); + await vestingRegistry.addAdmin(newLockedSOV.address); + + // Creating the instance of LockedSOV Contract. + lockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [ + admin, + ]); + + // Adding lockedSOV as an admin in the Vesting Registry. + await vestingRegistry.addAdmin(lockedSOV.address); + + /// @dev Moved from tests into init code, for speed optimization. + const infiniteTokens = maxRandom * 100; // A lot of tokens, enough to run all tests w/o extra minting + value = randomValue() + 10; + await sov.mint(userOne, infiniteTokens); + await sov.approve(lockedSOV.address, infiniteTokens, { from: userOne }); + }); + + it("Except Admin, no one should be able to add an admin.", async () => { + await expectRevert( + lockedSOV.addAdmin(newAdmin, { from: userOne }), + "Only admin can call this." + ); + }); + + it("Except Admin, no one should be able to remove an admin.", async () => { + await expectRevert( + lockedSOV.removeAdmin(admin, { from: userOne }), + "Only admin can call this." + ); + }); + + it("Except Admin, no one should be able to change the vestingRegistry, cliff and/or duration.", async () => { + await expectRevert( + lockedSOV.changeRegistryCliffAndDuration( + vestingRegistry.address, + cliff + 1, + duration + 1, + { from: userOne } + ), + "Only admin can call this." + ); + }); + + it("Anyone could deposit Tokens using deposit().", async () => { + let basisPoint = randomValue(); + await lockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); + }); + + it("No one could deposit Tokens using deposit() with 10000 as BasisPoint.", async () => { + let basisPoint = 10000; + await expectRevert( + lockedSOV.deposit(userOne, value, basisPoint, { from: userOne }), + "Basis Point has to be less than 10000." + ); + }); + + it("Anyone could deposit Tokens using depositSOV().", async () => { + await lockedSOV.depositSOV(userOne, value, { from: userOne }); + }); + + it("Anyone can withdraw unlocked Tokens using withdraw().", async () => { + let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. + await lockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); + await lockedSOV.withdraw(zeroAddress, { from: userOne }); + }); + + it("Anyone can withdraw unlocked Tokens to another wallet using withdraw().", async () => { + let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. + await lockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); + await lockedSOV.withdraw(userTwo, { from: userOne }); + }); + + it("Anyone can create a vesting schedule and stake tokens using createVestingAndStake().", async () => { + let value = randomValue() + 10; + await sov.mint(userThree, value); + await sov.approve(lockedSOV.address, value, { from: userThree }); + let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. + await lockedSOV.deposit(userThree, value, basisPoint, { from: userThree }); + await lockedSOV.createVestingAndStake({ from: userThree }); + }); + + it("No one can use createVestingAndStake() if he does not have any locked sov balance.", async () => { + await expectRevert( + newLockedSOV.createVestingAndStake({ from: userOne }), + "amount needs to be bigger than 0" + ); + }); + + it("Anyone can create a vesting schedule using createVesting() even with no locked sov balance.", async () => { + await lockedSOV.createVesting({ from: userOne }); + }); + + it("Anyone can use stakeTokens() to stake locked sov who already has a vesting contract.", async () => { + let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. + await lockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); + await lockedSOV.createVesting({ from: userOne }); + await lockedSOV.stakeTokens({ from: userOne }); + }); + + it("No one can use stakeTokens() who already has not created a vesting contract.", async () => { + let value = randomValue() + 10; + await sov.mint(userFive, value); + await sov.approve(lockedSOV.address, value, { from: userFive }); + let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. + await lockedSOV.deposit(userFive, value, basisPoint, { from: userFive }); + await expectRevert( + lockedSOV.stakeTokens({ from: userFive }), + "function call to a non-contract account" + ); + }); + + it("Anyone can withdraw unlocked and stake locked balance using withdrawAndStakeTokens().", async () => { + let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. + await lockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); + await lockedSOV.withdrawAndStakeTokens(userOne, { from: userOne }); + }); + + it("Anyone can withdraw unlocked and stake locked balance using withdrawAndStakeTokensFrom().", async () => { + let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. + await lockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); + await lockedSOV.withdrawAndStakeTokensFrom(userOne, { from: userTwo }); + }); + + it("Except Admin, no one should be able to start migration.", async () => { + await expectRevert( + lockedSOV.startMigration(newLockedSOV.address, { from: userOne }), + "Only admin can call this." + ); + }); + + it("No one can transfer locked balance using transfer() unless migration has started.", async () => { + let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. + await lockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); + + await expectRevert( + lockedSOV.transfer({ from: userOne }), + "Migration has not yet started." + ); + }); + + it("Anyone can transfer locked balance using transfer().", async () => { + let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. + await lockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); + + // Migratioin started by Admin + await lockedSOV.startMigration(newLockedSOV.address, { from: admin }); + + await lockedSOV.transfer({ from: userOne }); + }); }); diff --git a/tests/Locked/creator.test.js b/tests/Locked/creator.test.js index aa364fb3a..66e9e9edd 100644 --- a/tests/Locked/creator.test.js +++ b/tests/Locked/creator.test.js @@ -16,9 +16,9 @@ const VestingFactory = artifacts.require("VestingFactory"); const VestingRegistry = artifacts.require("VestingRegistry3"); const { - BN, // Big Number support. - expectRevert, - constants, // Assertions for transactions that should fail. + BN, // Big Number support. + expectRevert, + constants, // Assertions for transactions that should fail. } = require("@openzeppelin/test-helpers"); const { assert } = require("chai"); @@ -30,77 +30,108 @@ let cliff = 1; // This is in 4 weeks. i.e. 1 * 4 weeks. let duration = 11; // This is in 4 weeks. i.e. 11 * 4 weeks. contract("Locked SOV (Creator Functions)", (accounts) => { - let sov, lockedSOV, newLockedSOV, vestingRegistry, vestingLogic, stakingLogic; - let creator, admin, newAdmin, userOne, userTwo, userThree, userFour, userFive; - - before("Initiating Accounts & Creating Test Token Instance.", async () => { - // Checking if we have enough accounts to test. - assert.isAtLeast(accounts.length, 8, "At least 8 accounts are required to test the contracts."); - [creator, admin, newAdmin, userOne, userTwo, userThree, userFour, userFive] = accounts; - - // Creating the instance of SOV Token. - sov = await SOV.new("Sovryn", "SOV", 18, zero); - wrbtc = await TestWrbtc.new(); - - // Creating the Staking Instance. - stakingLogic = await StakingLogic.new(sov.address); - staking = await StakingProxy.new(sov.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingLogic.at(staking.address); - - // Creating the FeeSharing Instance. - feeSharingProxy = await FeeSharingProxy.new(zeroAddress, staking.address); - - // Creating the Vesting Instance. - vestingLogic = await VestingLogic.new(); - vestingFactory = await VestingFactory.new(vestingLogic.address); - vestingRegistry = await VestingRegistry.new( - vestingFactory.address, - sov.address, - staking.address, - feeSharingProxy.address, - creator // This should be Governance Timelock Contract. - ); - await vestingFactory.transferOwnership(vestingRegistry.address); - - // Creating the instance of newLockedSOV Contract. - newLockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [admin]); - - // Creating the instance of LockedSOV Contract. - lockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [admin]); - - // Adding lockedSOV as an admin in the Vesting Registry. - await vestingRegistry.addAdmin(lockedSOV.address); - }); - - it("Creator should not be able to create Locked SOV Contract without specifying the sov contract.", async () => { - await expectRevert(LockedSOV.new(zeroAddress, vestingRegistry.address, cliff, duration, [admin]), "Invalid SOV Address."); - }); - - it("Creator should not be able to create Locked SOV Contract without specifying the vesting registry.", async () => { - await expectRevert(LockedSOV.new(sov.address, zeroAddress, cliff, duration, [admin]), "Vesting registry address is invalid."); - }); - - it("Creator should not be able to create Locked SOV Contract with duration higher than 36.", async () => { - await expectRevert(LockedSOV.new(sov.address, vestingRegistry.address, cliff, 100, [admin]), "Duration is too long."); - }); - - it("Except Admin, creator should not be able to add an admin.", async () => { - await expectRevert(lockedSOV.addAdmin(newAdmin, { from: creator }), "Only admin can call this."); - }); - - it("Except Admin, creator should not be able to remove an admin.", async () => { - await expectRevert(lockedSOV.removeAdmin(admin, { from: creator }), "Only admin can call this."); - }); - - it("Except Admin, creator should not be able to change the vestingRegistry, cliff and/or duration.", async () => { - await expectRevert( - lockedSOV.changeRegistryCliffAndDuration(vestingRegistry.address, cliff + 1, duration + 1, { from: creator }), - "Only admin can call this." - ); - }); - - it("Except Admin, creator should not be able to start migration.", async () => { - await expectRevert(lockedSOV.startMigration(newLockedSOV.address, { from: creator }), "Only admin can call this."); - }); + let sov, lockedSOV, newLockedSOV, vestingRegistry, vestingLogic, stakingLogic; + let creator, admin, newAdmin, userOne, userTwo, userThree, userFour, userFive; + + before("Initiating Accounts & Creating Test Token Instance.", async () => { + // Checking if we have enough accounts to test. + assert.isAtLeast( + accounts.length, + 8, + "At least 8 accounts are required to test the contracts." + ); + [creator, admin, newAdmin, userOne, userTwo, userThree, userFour, userFive] = accounts; + + // Creating the instance of SOV Token. + sov = await SOV.new("Sovryn", "SOV", 18, zero); + wrbtc = await TestWrbtc.new(); + + // Creating the Staking Instance. + stakingLogic = await StakingLogic.new(sov.address); + staking = await StakingProxy.new(sov.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); + + // Creating the FeeSharing Instance. + feeSharingProxy = await FeeSharingProxy.new(zeroAddress, staking.address); + + // Creating the Vesting Instance. + vestingLogic = await VestingLogic.new(); + vestingFactory = await VestingFactory.new(vestingLogic.address); + vestingRegistry = await VestingRegistry.new( + vestingFactory.address, + sov.address, + staking.address, + feeSharingProxy.address, + creator // This should be Governance Timelock Contract. + ); + await vestingFactory.transferOwnership(vestingRegistry.address); + + // Creating the instance of newLockedSOV Contract. + newLockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [ + admin, + ]); + + // Creating the instance of LockedSOV Contract. + lockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [ + admin, + ]); + + // Adding lockedSOV as an admin in the Vesting Registry. + await vestingRegistry.addAdmin(lockedSOV.address); + }); + + it("Creator should not be able to create Locked SOV Contract without specifying the sov contract.", async () => { + await expectRevert( + LockedSOV.new(zeroAddress, vestingRegistry.address, cliff, duration, [admin]), + "Invalid SOV Address." + ); + }); + + it("Creator should not be able to create Locked SOV Contract without specifying the vesting registry.", async () => { + await expectRevert( + LockedSOV.new(sov.address, zeroAddress, cliff, duration, [admin]), + "Vesting registry address is invalid." + ); + }); + + it("Creator should not be able to create Locked SOV Contract with duration higher than 36.", async () => { + await expectRevert( + LockedSOV.new(sov.address, vestingRegistry.address, cliff, 100, [admin]), + "Duration is too long." + ); + }); + + it("Except Admin, creator should not be able to add an admin.", async () => { + await expectRevert( + lockedSOV.addAdmin(newAdmin, { from: creator }), + "Only admin can call this." + ); + }); + + it("Except Admin, creator should not be able to remove an admin.", async () => { + await expectRevert( + lockedSOV.removeAdmin(admin, { from: creator }), + "Only admin can call this." + ); + }); + + it("Except Admin, creator should not be able to change the vestingRegistry, cliff and/or duration.", async () => { + await expectRevert( + lockedSOV.changeRegistryCliffAndDuration( + vestingRegistry.address, + cliff + 1, + duration + 1, + { from: creator } + ), + "Only admin can call this." + ); + }); + + it("Except Admin, creator should not be able to start migration.", async () => { + await expectRevert( + lockedSOV.startMigration(newLockedSOV.address, { from: creator }), + "Only admin can call this." + ); + }); }); diff --git a/tests/Locked/event.test.js b/tests/Locked/event.test.js index 7a0f99df3..bcc190e21 100644 --- a/tests/Locked/event.test.js +++ b/tests/Locked/event.test.js @@ -21,9 +21,9 @@ const VestingFactory = artifacts.require("VestingFactory"); const VestingRegistry = artifacts.require("VestingRegistry3"); const { - BN, // Big Number support. - expectEvent, - constants, // Assertions for transactions that should fail. + BN, // Big Number support. + expectEvent, + constants, // Assertions for transactions that should fail. } = require("@openzeppelin/test-helpers"); const { assert } = require("chai"); @@ -44,267 +44,288 @@ const maxRandom = 10000; * @return {number} Random Value. */ function randomValue() { - return Math.floor(Math.random() * maxRandom); + return Math.floor(Math.random() * maxRandom); } contract("Locked SOV (Events)", (accounts) => { - let sov, lockedSOV, newLockedSOV, vestingRegistry, newVestingRegistry, vestingLogic, stakingLogic; - let creator, admin, newAdmin, userOne, userTwo, userThree, userFour, userFive; - - before("Initiating Accounts & Creating Test Token Instance.", async () => { - // Checking if we have enough accounts to test. - assert.isAtLeast(accounts.length, 8, "At least 8 accounts are required to test the contracts."); - [creator, admin, newAdmin, userOne, userTwo, userThree, userFour, userFive] = accounts; - - // Creating the instance of SOV Token. - sov = await SOV.new("Sovryn", "SOV", 18, zero); - wrbtc = await TestWrbtc.new(); - - // Creating the Staking Instance. - stakingLogic = await StakingLogic.new(sov.address); - staking = await StakingProxy.new(sov.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingLogic.at(staking.address); - - // Creating the FeeSharing Instance. - feeSharingProxy = await FeeSharingProxy.new(zeroAddress, staking.address); - - // Creating the Vesting Instance. - vestingLogic = await VestingLogic.new(); - vestingFactory = await VestingFactory.new(vestingLogic.address); - vestingRegistry = await VestingRegistry.new( - vestingFactory.address, - sov.address, - staking.address, - feeSharingProxy.address, - creator // This should be Governance Timelock Contract. - ); - await vestingFactory.transferOwnership(vestingRegistry.address); - - // Creating the instance of newLockedSOV Contract. - newLockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [admin]); - await vestingRegistry.addAdmin(newLockedSOV.address); - - // Creating the instance of LockedSOV Contract. - lockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [admin]); - - // Adding lockedSOV as an admin in the Vesting Registry. - await vestingRegistry.addAdmin(lockedSOV.address); - - /// @dev Moved from tests into init code, for speed optimization. - const infiniteTokens = maxRandom * 100; // A lot of tokens, enough to run all tests w/o extra minting - value = randomValue() + 10; - await sov.mint(userOne, infiniteTokens); - await sov.approve(lockedSOV.address, infiniteTokens, { from: userOne }); - await sov.approve(newLockedSOV.address, infiniteTokens, { from: userOne }); - }); - - it("Adding another admin should emit AdminAdded.", async () => { - let txReceipt = await lockedSOV.addAdmin(newAdmin, { from: admin }); - expectEvent(txReceipt, "AdminAdded", { - _initiator: admin, - _newAdmin: newAdmin, - }); - }); - - it("Removing an admin should emit AdminRemoved.", async () => { - let txReceipt = await lockedSOV.removeAdmin(newAdmin, { from: admin }); - expectEvent(txReceipt, "AdminRemoved", { - _initiator: admin, - _removedAdmin: newAdmin, - }); - }); - - it("Changing the vestingRegistry, cliff and/or duration should emit RegistryCliffAndDurationUpdated.", async () => { - newVestingRegistry = await VestingRegistry.new( - vestingFactory.address, - sov.address, - staking.address, - feeSharingProxy.address, - creator // This should be Governance Timelock Contract. - ); - let txReceipt = await newLockedSOV.changeRegistryCliffAndDuration(newVestingRegistry.address, cliff + 1, duration + 1, { - from: admin, - }); - expectEvent(txReceipt, "RegistryCliffAndDurationUpdated", { - _initiator: admin, - _vestingRegistry: newVestingRegistry.address, - _cliff: new BN(cliff + 1), - _duration: new BN(duration + 1), - }); - }); - - it("Depositing Tokens using deposit() to own account should emit Deposited.", async () => { - let basisPoint = randomValue(); - let txReceipt = await lockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); - expectEvent(txReceipt, "Deposited", { - _initiator: userOne, - _userAddress: userOne, - _sovAmount: new BN(value), - _basisPoint: new BN(basisPoint), - }); - }); - - it("Depositing Tokens using deposit() to another account should emit Deposited.", async () => { - let basisPoint = randomValue(); - let txReceipt = await lockedSOV.deposit(userTwo, value, basisPoint, { from: userOne }); - expectEvent(txReceipt, "Deposited", { - _initiator: userOne, - _userAddress: userTwo, - _sovAmount: new BN(value), - _basisPoint: new BN(basisPoint), - }); - }); - - it("Depositing Tokens using depositSOV() to own account should emit Deposited.", async () => { - let txReceipt = await lockedSOV.depositSOV(userOne, value, { from: userOne }); - expectEvent(txReceipt, "Deposited", { - _initiator: userOne, - _userAddress: userOne, - _sovAmount: new BN(value), - _basisPoint: new BN(zero), - }); - }); - - it("Depositing Tokens using depositSOV() to another account should emit Deposited.", async () => { - let txReceipt = await lockedSOV.depositSOV(userTwo, value, { from: userOne }); - expectEvent(txReceipt, "Deposited", { - _initiator: userOne, - _userAddress: userTwo, - _sovAmount: new BN(value), - _basisPoint: new BN(zero), - }); - }); - - it("Withdrawing unlocked Tokens to own account using withdraw() should emit Withdrawn.", async () => { - let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. - await newLockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); - let txReceipt = await newLockedSOV.withdraw(zeroAddress, { from: userOne }); - expectEvent(txReceipt, "Withdrawn", { - _initiator: userOne, - _userAddress: userOne, - _sovAmount: new BN(Math.floor(value / 2)), - }); - }); - - it("Withdrawing unlocked Tokens to another account using withdraw() should emit Withdrawn.", async () => { - let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. - await newLockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); - let txReceipt = await newLockedSOV.withdraw(userTwo, { from: userOne }); - expectEvent(txReceipt, "Withdrawn", { - _initiator: userOne, - _userAddress: userTwo, - _sovAmount: new BN(Math.floor(value / 2)), - }); - }); - - it("Using createVestingAndStake() should emit both VestingCreated and TokenStaked.", async () => { - let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. - await lockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); - let lockedBal = await lockedSOV.getLockedBalance(userOne); - let txReceipt = await lockedSOV.createVestingAndStake({ from: userOne }); - let vestingAddr = await vestingRegistry.getVesting(userOne); - expectEvent(txReceipt, "VestingCreated", { - _initiator: userOne, - _userAddress: userOne, - _vesting: vestingAddr, - }); - expectEvent(txReceipt, "TokenStaked", { - _initiator: userOne, - _vesting: vestingAddr, - _amount: new BN(lockedBal), - }); - }); - - it("Using createVesting() should emit VestingCreated.", async () => { - let txReceipt = await lockedSOV.createVesting({ from: userOne }); - let vestingAddr = await vestingRegistry.getVesting(userOne); - expectEvent(txReceipt, "VestingCreated", { - _initiator: userOne, - _userAddress: userOne, - _vesting: vestingAddr, - }); - }); - - it("Using stakeTokens() should emit TokenStaked.", async () => { - let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. - await lockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); - await lockedSOV.createVesting({ from: userOne }); - let vestingAddr = await vestingRegistry.getVesting(userOne); - let txReceipt = await lockedSOV.stakeTokens({ from: userOne }); - expectEvent(txReceipt, "TokenStaked", { - _initiator: userOne, - _vesting: vestingAddr, - _amount: new BN(Math.ceil(value / 2)), - }); - }); - - it("Starting migration should emit MigrationStarted.", async () => { - let txReceipt = await lockedSOV.startMigration(newLockedSOV.address, { from: admin }); - expectEvent(txReceipt, "MigrationStarted", { - _initiator: admin, - _newLockedSOV: newLockedSOV.address, - }); - }); - - it("Transfering locked balance using transfer() should emit UserTransfered.", async () => { - let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. - await lockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); - - // Migratioin started by Admin - await lockedSOV.startMigration(newLockedSOV.address, { from: admin }); - - let txReceipt = await lockedSOV.transfer({ from: userOne }); - expectEvent(txReceipt, "UserTransfered", { - _initiator: userOne, - _amount: new BN(Math.ceil(value / 2)), - }); - }); - - it("Using withdrawAndStakeTokens() should emit Withdrawn and TokenStaked.", async () => { - // Creating the instance of LockedSOV Contract. - lockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [admin]); - - // Adding lockedSOV as an admin in the Vesting Registry. - await vestingRegistry.addAdmin(lockedSOV.address); - - let value = randomValue() + 10; - await sov.mint(userOne, value); - await sov.approve(lockedSOV.address, value, { from: userOne }); - - let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. - await lockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); - let vestingAddr = await vestingRegistry.getVesting(userOne); - let txReceipt = await lockedSOV.withdrawAndStakeTokens(userOne, { from: userOne }); - expectEvent(txReceipt, "Withdrawn", { - _initiator: userOne, - _userAddress: userOne, - _sovAmount: new BN(Math.floor(value / 2)), - }); - expectEvent(txReceipt, "TokenStaked", { - _initiator: userOne, - _vesting: vestingAddr, - _amount: new BN(Math.ceil(value / 2)), - }); - }); - - it("Using withdrawAndStakeTokensFrom() should emit Withdrawn and TokenStaked.", async () => { - let value = randomValue() + 10; - await sov.mint(userOne, value); - await sov.approve(lockedSOV.address, value, { from: userOne }); - let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. - await lockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); - let vestingAddr = await vestingRegistry.getVesting(userOne); - let txReceipt = await lockedSOV.withdrawAndStakeTokensFrom(userOne, { from: userTwo }); - expectEvent(txReceipt, "Withdrawn", { - _initiator: userOne, - _userAddress: userOne, - _sovAmount: new BN(Math.floor(value / 2)), - }); - expectEvent(txReceipt, "TokenStaked", { - _initiator: userOne, - _vesting: vestingAddr, - _amount: new BN(Math.ceil(value / 2)), - }); - }); + let sov, + lockedSOV, + newLockedSOV, + vestingRegistry, + newVestingRegistry, + vestingLogic, + stakingLogic; + let creator, admin, newAdmin, userOne, userTwo, userThree, userFour, userFive; + + before("Initiating Accounts & Creating Test Token Instance.", async () => { + // Checking if we have enough accounts to test. + assert.isAtLeast( + accounts.length, + 8, + "At least 8 accounts are required to test the contracts." + ); + [creator, admin, newAdmin, userOne, userTwo, userThree, userFour, userFive] = accounts; + + // Creating the instance of SOV Token. + sov = await SOV.new("Sovryn", "SOV", 18, zero); + wrbtc = await TestWrbtc.new(); + + // Creating the Staking Instance. + stakingLogic = await StakingLogic.new(sov.address); + staking = await StakingProxy.new(sov.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); + + // Creating the FeeSharing Instance. + feeSharingProxy = await FeeSharingProxy.new(zeroAddress, staking.address); + + // Creating the Vesting Instance. + vestingLogic = await VestingLogic.new(); + vestingFactory = await VestingFactory.new(vestingLogic.address); + vestingRegistry = await VestingRegistry.new( + vestingFactory.address, + sov.address, + staking.address, + feeSharingProxy.address, + creator // This should be Governance Timelock Contract. + ); + await vestingFactory.transferOwnership(vestingRegistry.address); + + // Creating the instance of newLockedSOV Contract. + newLockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [ + admin, + ]); + await vestingRegistry.addAdmin(newLockedSOV.address); + + // Creating the instance of LockedSOV Contract. + lockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [ + admin, + ]); + + // Adding lockedSOV as an admin in the Vesting Registry. + await vestingRegistry.addAdmin(lockedSOV.address); + + /// @dev Moved from tests into init code, for speed optimization. + const infiniteTokens = maxRandom * 100; // A lot of tokens, enough to run all tests w/o extra minting + value = randomValue() + 10; + await sov.mint(userOne, infiniteTokens); + await sov.approve(lockedSOV.address, infiniteTokens, { from: userOne }); + await sov.approve(newLockedSOV.address, infiniteTokens, { from: userOne }); + }); + + it("Adding another admin should emit AdminAdded.", async () => { + let txReceipt = await lockedSOV.addAdmin(newAdmin, { from: admin }); + expectEvent(txReceipt, "AdminAdded", { + _initiator: admin, + _newAdmin: newAdmin, + }); + }); + + it("Removing an admin should emit AdminRemoved.", async () => { + let txReceipt = await lockedSOV.removeAdmin(newAdmin, { from: admin }); + expectEvent(txReceipt, "AdminRemoved", { + _initiator: admin, + _removedAdmin: newAdmin, + }); + }); + + it("Changing the vestingRegistry, cliff and/or duration should emit RegistryCliffAndDurationUpdated.", async () => { + newVestingRegistry = await VestingRegistry.new( + vestingFactory.address, + sov.address, + staking.address, + feeSharingProxy.address, + creator // This should be Governance Timelock Contract. + ); + let txReceipt = await newLockedSOV.changeRegistryCliffAndDuration( + newVestingRegistry.address, + cliff + 1, + duration + 1, + { + from: admin, + } + ); + expectEvent(txReceipt, "RegistryCliffAndDurationUpdated", { + _initiator: admin, + _vestingRegistry: newVestingRegistry.address, + _cliff: new BN(cliff + 1), + _duration: new BN(duration + 1), + }); + }); + + it("Depositing Tokens using deposit() to own account should emit Deposited.", async () => { + let basisPoint = randomValue(); + let txReceipt = await lockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); + expectEvent(txReceipt, "Deposited", { + _initiator: userOne, + _userAddress: userOne, + _sovAmount: new BN(value), + _basisPoint: new BN(basisPoint), + }); + }); + + it("Depositing Tokens using deposit() to another account should emit Deposited.", async () => { + let basisPoint = randomValue(); + let txReceipt = await lockedSOV.deposit(userTwo, value, basisPoint, { from: userOne }); + expectEvent(txReceipt, "Deposited", { + _initiator: userOne, + _userAddress: userTwo, + _sovAmount: new BN(value), + _basisPoint: new BN(basisPoint), + }); + }); + + it("Depositing Tokens using depositSOV() to own account should emit Deposited.", async () => { + let txReceipt = await lockedSOV.depositSOV(userOne, value, { from: userOne }); + expectEvent(txReceipt, "Deposited", { + _initiator: userOne, + _userAddress: userOne, + _sovAmount: new BN(value), + _basisPoint: new BN(zero), + }); + }); + + it("Depositing Tokens using depositSOV() to another account should emit Deposited.", async () => { + let txReceipt = await lockedSOV.depositSOV(userTwo, value, { from: userOne }); + expectEvent(txReceipt, "Deposited", { + _initiator: userOne, + _userAddress: userTwo, + _sovAmount: new BN(value), + _basisPoint: new BN(zero), + }); + }); + + it("Withdrawing unlocked Tokens to own account using withdraw() should emit Withdrawn.", async () => { + let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. + await newLockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); + let txReceipt = await newLockedSOV.withdraw(zeroAddress, { from: userOne }); + expectEvent(txReceipt, "Withdrawn", { + _initiator: userOne, + _userAddress: userOne, + _sovAmount: new BN(Math.floor(value / 2)), + }); + }); + + it("Withdrawing unlocked Tokens to another account using withdraw() should emit Withdrawn.", async () => { + let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. + await newLockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); + let txReceipt = await newLockedSOV.withdraw(userTwo, { from: userOne }); + expectEvent(txReceipt, "Withdrawn", { + _initiator: userOne, + _userAddress: userTwo, + _sovAmount: new BN(Math.floor(value / 2)), + }); + }); + + it("Using createVestingAndStake() should emit both VestingCreated and TokenStaked.", async () => { + let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. + await lockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); + let lockedBal = await lockedSOV.getLockedBalance(userOne); + let txReceipt = await lockedSOV.createVestingAndStake({ from: userOne }); + let vestingAddr = await vestingRegistry.getVesting(userOne); + expectEvent(txReceipt, "VestingCreated", { + _initiator: userOne, + _userAddress: userOne, + _vesting: vestingAddr, + }); + expectEvent(txReceipt, "TokenStaked", { + _initiator: userOne, + _vesting: vestingAddr, + _amount: new BN(lockedBal), + }); + }); + + it("Using createVesting() should emit VestingCreated.", async () => { + let txReceipt = await lockedSOV.createVesting({ from: userOne }); + let vestingAddr = await vestingRegistry.getVesting(userOne); + expectEvent(txReceipt, "VestingCreated", { + _initiator: userOne, + _userAddress: userOne, + _vesting: vestingAddr, + }); + }); + + it("Using stakeTokens() should emit TokenStaked.", async () => { + let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. + await lockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); + await lockedSOV.createVesting({ from: userOne }); + let vestingAddr = await vestingRegistry.getVesting(userOne); + let txReceipt = await lockedSOV.stakeTokens({ from: userOne }); + expectEvent(txReceipt, "TokenStaked", { + _initiator: userOne, + _vesting: vestingAddr, + _amount: new BN(Math.ceil(value / 2)), + }); + }); + + it("Starting migration should emit MigrationStarted.", async () => { + let txReceipt = await lockedSOV.startMigration(newLockedSOV.address, { from: admin }); + expectEvent(txReceipt, "MigrationStarted", { + _initiator: admin, + _newLockedSOV: newLockedSOV.address, + }); + }); + + it("Transfering locked balance using transfer() should emit UserTransfered.", async () => { + let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. + await lockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); + + // Migratioin started by Admin + await lockedSOV.startMigration(newLockedSOV.address, { from: admin }); + + let txReceipt = await lockedSOV.transfer({ from: userOne }); + expectEvent(txReceipt, "UserTransfered", { + _initiator: userOne, + _amount: new BN(Math.ceil(value / 2)), + }); + }); + + it("Using withdrawAndStakeTokens() should emit Withdrawn and TokenStaked.", async () => { + // Creating the instance of LockedSOV Contract. + lockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [ + admin, + ]); + + // Adding lockedSOV as an admin in the Vesting Registry. + await vestingRegistry.addAdmin(lockedSOV.address); + + let value = randomValue() + 10; + await sov.mint(userOne, value); + await sov.approve(lockedSOV.address, value, { from: userOne }); + + let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. + await lockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); + let vestingAddr = await vestingRegistry.getVesting(userOne); + let txReceipt = await lockedSOV.withdrawAndStakeTokens(userOne, { from: userOne }); + expectEvent(txReceipt, "Withdrawn", { + _initiator: userOne, + _userAddress: userOne, + _sovAmount: new BN(Math.floor(value / 2)), + }); + expectEvent(txReceipt, "TokenStaked", { + _initiator: userOne, + _vesting: vestingAddr, + _amount: new BN(Math.ceil(value / 2)), + }); + }); + + it("Using withdrawAndStakeTokensFrom() should emit Withdrawn and TokenStaked.", async () => { + let value = randomValue() + 10; + await sov.mint(userOne, value); + await sov.approve(lockedSOV.address, value, { from: userOne }); + let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. + await lockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); + let vestingAddr = await vestingRegistry.getVesting(userOne); + let txReceipt = await lockedSOV.withdrawAndStakeTokensFrom(userOne, { from: userTwo }); + expectEvent(txReceipt, "Withdrawn", { + _initiator: userOne, + _userAddress: userOne, + _sovAmount: new BN(Math.floor(value / 2)), + }); + expectEvent(txReceipt, "TokenStaked", { + _initiator: userOne, + _vesting: vestingAddr, + _amount: new BN(Math.ceil(value / 2)), + }); + }); }); diff --git a/tests/Locked/state.test.js b/tests/Locked/state.test.js index d6f99fdc1..645235999 100644 --- a/tests/Locked/state.test.js +++ b/tests/Locked/state.test.js @@ -18,8 +18,8 @@ const VestingFactory = artifacts.require("VestingFactory"); const VestingRegistry = artifacts.require("VestingRegistry3"); const { - BN, // Big Number support. - constants, // Assertions for transactions that should fail. + BN, // Big Number support. + constants, // Assertions for transactions that should fail. } = require("@openzeppelin/test-helpers"); const { assert } = require("chai"); @@ -38,7 +38,7 @@ let fourWeeks = 4 * 7 * 24 * 60 * 60; * @return {number} Random Value. */ function randomValue() { - return Math.floor(Math.random() * 10000); + return Math.floor(Math.random() * 10000); } /** @@ -57,50 +57,54 @@ function randomValue() { * @param isAdmin True if `userAddr` is an admin, false otherwise. */ async function checkStatus( - contractInstance, - checkArray, - userAddr, - migration, - cliff, - duration, - vestingRegistry, - newLockedSOV, - lockedBalance, - unlockedBalance, - isAdmin + contractInstance, + checkArray, + userAddr, + migration, + cliff, + duration, + vestingRegistry, + newLockedSOV, + lockedBalance, + unlockedBalance, + isAdmin ) { - if (checkArray[0] == 1) { - let cValue = await contractInstance.migration(); - assert.strictEqual(migration, cValue, "The migration status does not match."); - } - if (checkArray[1] == 1) { - let cValue = await contractInstance.cliff(); - assert.strictEqual(cliff, cValue.toNumber() / fourWeeks, "The cliff does not match."); - } - if (checkArray[2] == 1) { - let cValue = await contractInstance.duration(); - assert.strictEqual(duration, cValue.toNumber() / fourWeeks, "The duration does not match."); - } - if (checkArray[3] == 1) { - let cValue = await contractInstance.vestingRegistry(); - assert.strictEqual(vestingRegistry, cValue, "The vesting registry does not match."); - } - if (checkArray[4] == 1) { - let cValue = await contractInstance.newLockedSOV(); - assert.equal(newLockedSOV, cValue, "The new locked sov does not match."); - } - if (checkArray[5] == 1) { - let cValue = await contractInstance.getLockedBalance(userAddr); - assert.equal(lockedBalance, cValue.toNumber(), "The locked balance does not match."); - } - if (checkArray[6] == 1) { - let cValue = await contractInstance.getUnlockedBalance(userAddr); - assert.equal(unlockedBalance, cValue.toNumber(), "The unlocked balance does not match."); - } - if (checkArray[7] == 1) { - let cValue = await contractInstance.adminStatus(userAddr); - assert.equal(isAdmin, cValue, "The admin status does not match."); - } + if (checkArray[0] == 1) { + let cValue = await contractInstance.migration(); + assert.strictEqual(migration, cValue, "The migration status does not match."); + } + if (checkArray[1] == 1) { + let cValue = await contractInstance.cliff(); + assert.strictEqual(cliff, cValue.toNumber() / fourWeeks, "The cliff does not match."); + } + if (checkArray[2] == 1) { + let cValue = await contractInstance.duration(); + assert.strictEqual( + duration, + cValue.toNumber() / fourWeeks, + "The duration does not match." + ); + } + if (checkArray[3] == 1) { + let cValue = await contractInstance.vestingRegistry(); + assert.strictEqual(vestingRegistry, cValue, "The vesting registry does not match."); + } + if (checkArray[4] == 1) { + let cValue = await contractInstance.newLockedSOV(); + assert.equal(newLockedSOV, cValue, "The new locked sov does not match."); + } + if (checkArray[5] == 1) { + let cValue = await contractInstance.getLockedBalance(userAddr); + assert.equal(lockedBalance, cValue.toNumber(), "The locked balance does not match."); + } + if (checkArray[6] == 1) { + let cValue = await contractInstance.getUnlockedBalance(userAddr); + assert.equal(unlockedBalance, cValue.toNumber(), "The unlocked balance does not match."); + } + if (checkArray[7] == 1) { + let cValue = await contractInstance.adminStatus(userAddr); + assert.equal(isAdmin, cValue, "The admin status does not match."); + } } /** @@ -114,10 +118,10 @@ async function checkStatus( * @return [SOV Balance, Locked Balance, Unlocked Balance]. */ async function getTokenBalances(addr, sovContract, lockedSOVContract) { - let sovBal = (await sovContract.balanceOf(addr)).toNumber(); - let lockedBal = (await lockedSOVContract.getLockedBalance(addr)).toNumber(); - let unlockedBal = (await lockedSOVContract.getUnlockedBalance(addr)).toNumber(); - return [sovBal, lockedBal, unlockedBal]; + let sovBal = (await sovContract.balanceOf(addr)).toNumber(); + let lockedBal = (await lockedSOVContract.getLockedBalance(addr)).toNumber(); + let unlockedBal = (await lockedSOVContract.getUnlockedBalance(addr)).toNumber(); + return [sovBal, lockedBal, unlockedBal]; } /** @@ -132,444 +136,487 @@ async function getTokenBalances(addr, sovContract, lockedSOVContract) { * @returns value The token amount which was deposited by user. */ async function userDeposits(sovContract, lockedSOVContract, sender, receiver, basisPoint) { - let value = randomValue() + 10; - await sovContract.mint(sender, value); - await sovContract.approve(lockedSOVContract.address, value, { from: sender }); - await lockedSOVContract.deposit(receiver, value, basisPoint, { from: sender }); - return value; + let value = randomValue() + 10; + await sovContract.mint(sender, value); + await sovContract.approve(lockedSOVContract.address, value, { from: sender }); + await lockedSOVContract.deposit(receiver, value, basisPoint, { from: sender }); + return value; } contract("Locked SOV (State)", (accounts) => { - let sov, lockedSOV, newLockedSOV, stakingLogic, staking, feeSharingProxy, vestingLogic, vestingFactory, vestingRegistry; - let creator, admin, newAdmin, userOne, userTwo, userThree, userFour, userFive; - - before("Initiating Accounts & Creating Test Token Instance.", async () => { - // Checking if we have enough accounts to test. - assert.isAtLeast(accounts.length, 8, "Alteast 8 accounts are required to test the contracts."); - [creator, admin, newAdmin, userOne, userTwo, userThree, userFour, userFive] = accounts; - - // Creating the instance of SOV Token. - sov = await SOV.new("Sovryn", "SOV", 18, zero); - wrbtc = await TestWrbtc.new(); - - // Creating the Staking Instance. - stakingLogic = await StakingLogic.new(sov.address); - staking = await StakingProxy.new(sov.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingLogic.at(staking.address); - - // Creating the FeeSharing Instance. - feeSharingProxy = await FeeSharingProxy.new(zeroAddress, staking.address); - - // Creating the Vesting Instance. - vestingLogic = await VestingLogic.new(); - vestingFactory = await VestingFactory.new(vestingLogic.address); - vestingRegistry = await VestingRegistry.new( - vestingFactory.address, - sov.address, - staking.address, - feeSharingProxy.address, - creator // This should be Governance Timelock Contract. - ); - await vestingFactory.transferOwnership(vestingRegistry.address); - - // Creating the instance of newLockedSOV Contract. - newLockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [admin]); - - // Creating the instance of LockedSOV Contract. - lockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [admin]); - - // Adding lockedSOV as an admin in the Vesting Registry. - await vestingRegistry.addAdmin(lockedSOV.address); - }); - - it("Creating an instance should set all the values correctly.", async () => { - await checkStatus( - lockedSOV, - [1, 1, 1, 1, 1, 1, 1, 1], - userOne, - false, - cliff, - duration, - vestingRegistry.address, - zeroAddress, - zero, - zero, - false - ); - await checkStatus( - lockedSOV, - [1, 1, 1, 1, 1, 1, 1, 1], - admin, - false, - cliff, - duration, - vestingRegistry.address, - zeroAddress, - zero, - zero, - true - ); - }); - - it("Adding a new user as Admin should correctly reflect in contract.", async () => { - await lockedSOV.addAdmin(newAdmin, { from: admin }); - await checkStatus( - lockedSOV, - [1, 1, 1, 1, 1, 1, 1, 1], - newAdmin, - false, - cliff, - duration, - vestingRegistry.address, - zeroAddress, - zero, - zero, - true - ); - }); - - it("Removing a new user as Admin should correctly reflect in contract.", async () => { - await lockedSOV.removeAdmin(newAdmin, { from: admin }); - await checkStatus( - lockedSOV, - [1, 1, 1, 1, 1, 1, 1, 1], - newAdmin, - false, - cliff, - duration, - vestingRegistry.address, - zeroAddress, - zero, - zero, - false - ); - }); - - it("Updating the vestingRegistry, cliff and/or duration should correctly reflect in contract.", async () => { - /// @dev Deploying a new contract is needed to check the update is working Ok. - let newVestingRegistry = await VestingRegistry.new( - vestingFactory.address, - sov.address, - staking.address, - feeSharingProxy.address, - creator // This should be Governance Timelock Contract. - ); - await newLockedSOV.changeRegistryCliffAndDuration(newVestingRegistry.address, cliff + 1, duration + 1, { from: admin }); - await checkStatus( - newLockedSOV, - [1, 1, 1, 1, 1, 1, 1, 1], - admin, - false, - cliff + 1, - duration + 1, - newVestingRegistry.address, - zeroAddress, - zero, - zero, - true - ); - }); - - it("Depositing Tokens using deposit() should update the user balances based on basis point.", async () => { - let basisPoint = randomValue(); - let value = await userDeposits(sov, lockedSOV, userOne, userOne, basisPoint); - let unlockedBal = Math.floor((value * basisPoint) / 10000); - let lockedBal = value - unlockedBal; - await checkStatus( - lockedSOV, - [1, 1, 1, 1, 1, 1, 1, 1], - userOne, - false, - cliff, - duration, - vestingRegistry.address, - zeroAddress, - lockedBal, - unlockedBal, - false - ); - }); - - it("Depositing Tokens using depositSOV() should update the user locked balances.", async () => { - let [tokenBal, lockedBal, unlockedBal] = await getTokenBalances(userOne, sov, lockedSOV); - let value = randomValue() + 1; - await sov.mint(userOne, value); - await sov.approve(lockedSOV.address, value, { from: userOne }); - await lockedSOV.depositSOV(userOne, value, { from: userOne }); - await checkStatus( - lockedSOV, - [1, 1, 1, 1, 1, 1, 1, 1], - userOne, - false, - cliff, - duration, - vestingRegistry.address, - zeroAddress, - value + lockedBal, - zero + unlockedBal, - false - ); - }); - - it("Withdrawing unlocked tokens themselves using withdraw() should update the unlocked balance and should not affect locked balance.", async () => { - let [, fLockedBal, fUnlockedBal] = await getTokenBalances(userOne, sov, lockedSOV); - let basisPoint = 5000; - let value = await userDeposits(sov, lockedSOV, userOne, userOne, basisPoint); - let unlockedBal = Math.floor((value * basisPoint) / 10000); - let lockedBal = value - unlockedBal + fLockedBal; - let [beforeBal, ,] = await getTokenBalances(userOne, sov, lockedSOV); - await lockedSOV.withdraw(zeroAddress, { from: userOne }); - let [afterBal, ,] = await getTokenBalances(userOne, sov, lockedSOV); - await checkStatus( - lockedSOV, - [1, 1, 1, 1, 1, 1, 1, 1], - userOne, - false, - cliff, - duration, - vestingRegistry.address, - zeroAddress, - lockedBal, - zero, - false - ); - assert.equal(afterBal, beforeBal + unlockedBal + fUnlockedBal, "Correct amount was not withdrawn."); - }); - - it("Withdrawing unlocked tokens to someone else using withdraw() should update the token balance of that user.", async () => { - let basisPoint = 5000; - let value = await userDeposits(sov, lockedSOV, userOne, userOne, basisPoint); - let unlockedBal = Math.floor((value * basisPoint) / 10000); - let [beforeBal, ,] = await getTokenBalances(userTwo, sov, lockedSOV); - await lockedSOV.withdraw(userTwo, { from: userOne }); - let [afterBal, ,] = await getTokenBalances(userTwo, sov, lockedSOV); - assert.equal(afterBal, beforeBal + unlockedBal, "Correct amount was not withdrawn."); - }); - - it("Using createVestingAndStake() should create vesting address and stake tokens correctly.", async () => { - let value = randomValue() + 10; - await sov.mint(userThree, value); - await sov.approve(lockedSOV.address, value, { from: userThree }); - let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. - await lockedSOV.deposit(userThree, value, basisPoint, { from: userThree }); - - let vestingAddr = await vestingRegistry.getVesting(userThree); - assert.equal(vestingAddr, zeroAddress, "Vesting Address should be zero."); - - await lockedSOV.createVestingAndStake({ from: userThree }); - - vestingAddr = await vestingRegistry.getVesting(userThree); - assert.notEqual(vestingAddr, zeroAddress, "Vesting Address should not be zero."); - - let balance = await staking.balanceOf(vestingAddr); - let lockedValue = Math.ceil((value * basisPoint) / 10000); - assert.equal(balance.toNumber(), Math.ceil(lockedValue), "Staking Balance does not match"); - - let getStakes = await staking.getStakes(vestingAddr); - assert.equal(getStakes.stakes[0].toNumber(), Math.ceil(lockedValue - Math.floor(lockedValue / duration) * (duration - 1))); - for (let index = 1; index < getStakes.dates.length; index++) { - assert.equal(getStakes.stakes[index].toNumber(), Math.floor(lockedValue / duration)); - } - }); - - it("Using createVesting() should create vesting address correctly.", async () => { - let vestingAddr = await vestingRegistry.getVesting(userTwo); - assert.equal(vestingAddr, zeroAddress, "Vesting Address should be zero."); - await lockedSOV.createVesting({ from: userTwo }); - vestingAddr = await vestingRegistry.getVesting(userTwo); - assert.notEqual(vestingAddr, zeroAddress, "Vesting Address should not be zero."); - }); - - it("Using stakeTokens() should correctly stake the locked tokens.", async () => { - // Creating the instance of LockedSOV Contract. - lockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [admin]); - - // Adding lockedSOV as an admin in the Vesting Registry. - await vestingRegistry.addAdmin(lockedSOV.address); - - let value = randomValue() + 10; - await sov.mint(userOne, value); - await sov.approve(lockedSOV.address, value, { from: userOne }); - let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. - await lockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); - - await lockedSOV.createVesting({ from: userOne }); - await lockedSOV.stakeTokens({ from: userOne }); - - vestingAddr = await vestingRegistry.getVesting(userOne); - assert.notEqual(vestingAddr, zeroAddress, "Vesting Address should not be zero."); - - let balance = await staking.balanceOf(vestingAddr); - let lockedValue = Math.ceil((value * basisPoint) / 10000); - assert.equal(balance.toNumber(), Math.ceil(lockedValue), "Staking Balance does not match"); - - let getStakes = await staking.getStakes(vestingAddr); - assert.equal(getStakes.stakes[0].toNumber(), Math.ceil(lockedValue - Math.floor(lockedValue / duration) * (duration - 1))); - for (let index = 1; index < getStakes.dates.length; index++) { - assert.equal(getStakes.stakes[index].toNumber(), Math.floor(lockedValue / duration)); - } - }); - - it("Using withdrawAndStakeTokens() should correctly withdraw all unlocked tokens and stake locked tokens correctly.", async () => { - let value = randomValue() + 10; - await sov.mint(userTwo, value); - await sov.approve(lockedSOV.address, value, { from: userTwo }); - let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. - await lockedSOV.deposit(userTwo, value, basisPoint, { from: userTwo }); - - let unlockedBal = Math.floor((value * basisPoint) / 10000); - let [beforeBal, ,] = await getTokenBalances(userTwo, sov, lockedSOV); - - await lockedSOV.withdrawAndStakeTokens(userTwo, { from: userTwo }); - - let [afterBal, ,] = await getTokenBalances(userTwo, sov, lockedSOV); - assert.equal(afterBal, beforeBal + unlockedBal, "Correct amount was not withdrawn."); - - vestingAddr = await vestingRegistry.getVesting(userTwo); - assert.notEqual(vestingAddr, zeroAddress, "Vesting Address should not be zero."); - - let balance = await staking.balanceOf(vestingAddr); - let lockedValue = Math.ceil((value * basisPoint) / 10000); - assert.equal(balance.toNumber(), Math.ceil(lockedValue), "Staking Balance does not match"); - - let getStakes = await staking.getStakes(vestingAddr); - assert.equal(getStakes.stakes[0].toNumber(), Math.ceil(lockedValue - Math.floor(lockedValue / duration) * (duration - 1))); - for (let index = 1; index < getStakes.dates.length; index++) { - assert.equal(getStakes.stakes[index].toNumber(), Math.floor(lockedValue / duration)); - } - }); - - it("Using withdrawAndStakeTokensFrom() should correctly withdraw all unlocked tokens and stake locked tokens correctly.", async () => { - let value = randomValue() + 10; - await sov.mint(userFour, value); - await sov.approve(lockedSOV.address, value, { from: userFour }); - let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. - await lockedSOV.deposit(userFour, value, basisPoint, { from: userFour }); - - let unlockedBal = Math.floor((value * basisPoint) / 10000); - let [beforeBal, ,] = await getTokenBalances(userFour, sov, lockedSOV); - - await lockedSOV.withdrawAndStakeTokensFrom(userFour, { from: userOne }); - - let [afterBal, ,] = await getTokenBalances(userFour, sov, lockedSOV); - assert.equal(afterBal, beforeBal + unlockedBal, "Correct amount was not withdrawn."); - - vestingAddr = await vestingRegistry.getVesting(userFour); - assert.notEqual(vestingAddr, zeroAddress, "Vesting Address should not be zero."); - - let balance = await staking.balanceOf(vestingAddr); - let lockedValue = Math.ceil((value * basisPoint) / 10000); - assert.equal(balance.toNumber(), Math.ceil(lockedValue), "Staking Balance does not match"); - - let getStakes = await staking.getStakes(vestingAddr); - assert.equal(getStakes.stakes[0].toNumber(), Math.ceil(lockedValue - Math.floor(lockedValue / duration) * (duration - 1))); - for (let index = 1; index < getStakes.dates.length; index++) { - assert.equal(getStakes.stakes[index].toNumber(), Math.floor(lockedValue / duration)); - } - }); - - it("Starting the migration should update the contract status correctly.", async () => { - await checkStatus( - lockedSOV, - [1, 1, 1, 1, 1, 1, 1, 1], - userThree, - false, - cliff, - duration, - vestingRegistry.address, - zeroAddress, - zero, - zero, - false - ); - await lockedSOV.startMigration(newLockedSOV.address, { from: admin }); - await checkStatus( - lockedSOV, - [1, 1, 1, 1, 1, 1, 1, 1], - userThree, - true, - cliff, - duration, - vestingRegistry.address, - newLockedSOV.address, - zero, - zero, - false - ); - }); - - it("Using transfer() should correctly transfer locked token to new locked sov.", async () => { - // Creating the instance of newLockedSOV Contract. - newLockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [admin]); - - // Creating the instance of LockedSOV Contract. - lockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [admin]); - - // Adding lockedSOV as an admin in the Vesting Registry. - await vestingRegistry.addAdmin(lockedSOV.address); - - let value = randomValue() + 10; - await sov.mint(userOne, value); - await sov.approve(lockedSOV.address, value, { from: userOne }); - let basisPoint = 0; - await lockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); - - // Migratioin started by Admin - await lockedSOV.startMigration(newLockedSOV.address, { from: admin }); - - await checkStatus( - lockedSOV, - [1, 1, 1, 1, 1, 1, 1, 1], - userOne, - true, - cliff, - duration, - vestingRegistry.address, - newLockedSOV.address, - value, - zero, - false - ); - await checkStatus( - newLockedSOV, - [1, 1, 1, 1, 1, 1, 1, 1], - userOne, - false, - cliff, - duration, - vestingRegistry.address, - zeroAddress, - zero, - zero, - false - ); - await lockedSOV.transfer({ from: userOne }); - await checkStatus( - lockedSOV, - [1, 1, 1, 1, 1, 1, 1, 1], - userOne, - true, - cliff, - duration, - vestingRegistry.address, - newLockedSOV.address, - zero, - zero, - false - ); - await checkStatus( - newLockedSOV, - [1, 1, 1, 1, 1, 1, 1, 1], - userOne, - false, - cliff, - duration, - vestingRegistry.address, - zeroAddress, - value, - zero, - false - ); - }); + let sov, + lockedSOV, + newLockedSOV, + stakingLogic, + staking, + feeSharingProxy, + vestingLogic, + vestingFactory, + vestingRegistry; + let creator, admin, newAdmin, userOne, userTwo, userThree, userFour, userFive; + + before("Initiating Accounts & Creating Test Token Instance.", async () => { + // Checking if we have enough accounts to test. + assert.isAtLeast( + accounts.length, + 8, + "Alteast 8 accounts are required to test the contracts." + ); + [creator, admin, newAdmin, userOne, userTwo, userThree, userFour, userFive] = accounts; + + // Creating the instance of SOV Token. + sov = await SOV.new("Sovryn", "SOV", 18, zero); + wrbtc = await TestWrbtc.new(); + + // Creating the Staking Instance. + stakingLogic = await StakingLogic.new(sov.address); + staking = await StakingProxy.new(sov.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); + + // Creating the FeeSharing Instance. + feeSharingProxy = await FeeSharingProxy.new(zeroAddress, staking.address); + + // Creating the Vesting Instance. + vestingLogic = await VestingLogic.new(); + vestingFactory = await VestingFactory.new(vestingLogic.address); + vestingRegistry = await VestingRegistry.new( + vestingFactory.address, + sov.address, + staking.address, + feeSharingProxy.address, + creator // This should be Governance Timelock Contract. + ); + await vestingFactory.transferOwnership(vestingRegistry.address); + + // Creating the instance of newLockedSOV Contract. + newLockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [ + admin, + ]); + + // Creating the instance of LockedSOV Contract. + lockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [ + admin, + ]); + + // Adding lockedSOV as an admin in the Vesting Registry. + await vestingRegistry.addAdmin(lockedSOV.address); + }); + + it("Creating an instance should set all the values correctly.", async () => { + await checkStatus( + lockedSOV, + [1, 1, 1, 1, 1, 1, 1, 1], + userOne, + false, + cliff, + duration, + vestingRegistry.address, + zeroAddress, + zero, + zero, + false + ); + await checkStatus( + lockedSOV, + [1, 1, 1, 1, 1, 1, 1, 1], + admin, + false, + cliff, + duration, + vestingRegistry.address, + zeroAddress, + zero, + zero, + true + ); + }); + + it("Adding a new user as Admin should correctly reflect in contract.", async () => { + await lockedSOV.addAdmin(newAdmin, { from: admin }); + await checkStatus( + lockedSOV, + [1, 1, 1, 1, 1, 1, 1, 1], + newAdmin, + false, + cliff, + duration, + vestingRegistry.address, + zeroAddress, + zero, + zero, + true + ); + }); + + it("Removing a new user as Admin should correctly reflect in contract.", async () => { + await lockedSOV.removeAdmin(newAdmin, { from: admin }); + await checkStatus( + lockedSOV, + [1, 1, 1, 1, 1, 1, 1, 1], + newAdmin, + false, + cliff, + duration, + vestingRegistry.address, + zeroAddress, + zero, + zero, + false + ); + }); + + it("Updating the vestingRegistry, cliff and/or duration should correctly reflect in contract.", async () => { + /// @dev Deploying a new contract is needed to check the update is working Ok. + let newVestingRegistry = await VestingRegistry.new( + vestingFactory.address, + sov.address, + staking.address, + feeSharingProxy.address, + creator // This should be Governance Timelock Contract. + ); + await newLockedSOV.changeRegistryCliffAndDuration( + newVestingRegistry.address, + cliff + 1, + duration + 1, + { from: admin } + ); + await checkStatus( + newLockedSOV, + [1, 1, 1, 1, 1, 1, 1, 1], + admin, + false, + cliff + 1, + duration + 1, + newVestingRegistry.address, + zeroAddress, + zero, + zero, + true + ); + }); + + it("Depositing Tokens using deposit() should update the user balances based on basis point.", async () => { + let basisPoint = randomValue(); + let value = await userDeposits(sov, lockedSOV, userOne, userOne, basisPoint); + let unlockedBal = Math.floor((value * basisPoint) / 10000); + let lockedBal = value - unlockedBal; + await checkStatus( + lockedSOV, + [1, 1, 1, 1, 1, 1, 1, 1], + userOne, + false, + cliff, + duration, + vestingRegistry.address, + zeroAddress, + lockedBal, + unlockedBal, + false + ); + }); + + it("Depositing Tokens using depositSOV() should update the user locked balances.", async () => { + let [tokenBal, lockedBal, unlockedBal] = await getTokenBalances(userOne, sov, lockedSOV); + let value = randomValue() + 1; + await sov.mint(userOne, value); + await sov.approve(lockedSOV.address, value, { from: userOne }); + await lockedSOV.depositSOV(userOne, value, { from: userOne }); + await checkStatus( + lockedSOV, + [1, 1, 1, 1, 1, 1, 1, 1], + userOne, + false, + cliff, + duration, + vestingRegistry.address, + zeroAddress, + value + lockedBal, + zero + unlockedBal, + false + ); + }); + + it("Withdrawing unlocked tokens themselves using withdraw() should update the unlocked balance and should not affect locked balance.", async () => { + let [, fLockedBal, fUnlockedBal] = await getTokenBalances(userOne, sov, lockedSOV); + let basisPoint = 5000; + let value = await userDeposits(sov, lockedSOV, userOne, userOne, basisPoint); + let unlockedBal = Math.floor((value * basisPoint) / 10000); + let lockedBal = value - unlockedBal + fLockedBal; + let [beforeBal, ,] = await getTokenBalances(userOne, sov, lockedSOV); + await lockedSOV.withdraw(zeroAddress, { from: userOne }); + let [afterBal, ,] = await getTokenBalances(userOne, sov, lockedSOV); + await checkStatus( + lockedSOV, + [1, 1, 1, 1, 1, 1, 1, 1], + userOne, + false, + cliff, + duration, + vestingRegistry.address, + zeroAddress, + lockedBal, + zero, + false + ); + assert.equal( + afterBal, + beforeBal + unlockedBal + fUnlockedBal, + "Correct amount was not withdrawn." + ); + }); + + it("Withdrawing unlocked tokens to someone else using withdraw() should update the token balance of that user.", async () => { + let basisPoint = 5000; + let value = await userDeposits(sov, lockedSOV, userOne, userOne, basisPoint); + let unlockedBal = Math.floor((value * basisPoint) / 10000); + let [beforeBal, ,] = await getTokenBalances(userTwo, sov, lockedSOV); + await lockedSOV.withdraw(userTwo, { from: userOne }); + let [afterBal, ,] = await getTokenBalances(userTwo, sov, lockedSOV); + assert.equal(afterBal, beforeBal + unlockedBal, "Correct amount was not withdrawn."); + }); + + it("Using createVestingAndStake() should create vesting address and stake tokens correctly.", async () => { + let value = randomValue() + 10; + await sov.mint(userThree, value); + await sov.approve(lockedSOV.address, value, { from: userThree }); + let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. + await lockedSOV.deposit(userThree, value, basisPoint, { from: userThree }); + + let vestingAddr = await vestingRegistry.getVesting(userThree); + assert.equal(vestingAddr, zeroAddress, "Vesting Address should be zero."); + + await lockedSOV.createVestingAndStake({ from: userThree }); + + vestingAddr = await vestingRegistry.getVesting(userThree); + assert.notEqual(vestingAddr, zeroAddress, "Vesting Address should not be zero."); + + let balance = await staking.balanceOf(vestingAddr); + let lockedValue = Math.ceil((value * basisPoint) / 10000); + assert.equal(balance.toNumber(), Math.ceil(lockedValue), "Staking Balance does not match"); + + let getStakes = await staking.getStakes(vestingAddr); + assert.equal( + getStakes.stakes[0].toNumber(), + Math.ceil(lockedValue - Math.floor(lockedValue / duration) * (duration - 1)) + ); + for (let index = 1; index < getStakes.dates.length; index++) { + assert.equal(getStakes.stakes[index].toNumber(), Math.floor(lockedValue / duration)); + } + }); + + it("Using createVesting() should create vesting address correctly.", async () => { + let vestingAddr = await vestingRegistry.getVesting(userTwo); + assert.equal(vestingAddr, zeroAddress, "Vesting Address should be zero."); + await lockedSOV.createVesting({ from: userTwo }); + vestingAddr = await vestingRegistry.getVesting(userTwo); + assert.notEqual(vestingAddr, zeroAddress, "Vesting Address should not be zero."); + }); + + it("Using stakeTokens() should correctly stake the locked tokens.", async () => { + // Creating the instance of LockedSOV Contract. + lockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [ + admin, + ]); + + // Adding lockedSOV as an admin in the Vesting Registry. + await vestingRegistry.addAdmin(lockedSOV.address); + + let value = randomValue() + 10; + await sov.mint(userOne, value); + await sov.approve(lockedSOV.address, value, { from: userOne }); + let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. + await lockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); + + await lockedSOV.createVesting({ from: userOne }); + await lockedSOV.stakeTokens({ from: userOne }); + + vestingAddr = await vestingRegistry.getVesting(userOne); + assert.notEqual(vestingAddr, zeroAddress, "Vesting Address should not be zero."); + + let balance = await staking.balanceOf(vestingAddr); + let lockedValue = Math.ceil((value * basisPoint) / 10000); + assert.equal(balance.toNumber(), Math.ceil(lockedValue), "Staking Balance does not match"); + + let getStakes = await staking.getStakes(vestingAddr); + assert.equal( + getStakes.stakes[0].toNumber(), + Math.ceil(lockedValue - Math.floor(lockedValue / duration) * (duration - 1)) + ); + for (let index = 1; index < getStakes.dates.length; index++) { + assert.equal(getStakes.stakes[index].toNumber(), Math.floor(lockedValue / duration)); + } + }); + + it("Using withdrawAndStakeTokens() should correctly withdraw all unlocked tokens and stake locked tokens correctly.", async () => { + let value = randomValue() + 10; + await sov.mint(userTwo, value); + await sov.approve(lockedSOV.address, value, { from: userTwo }); + let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. + await lockedSOV.deposit(userTwo, value, basisPoint, { from: userTwo }); + + let unlockedBal = Math.floor((value * basisPoint) / 10000); + let [beforeBal, ,] = await getTokenBalances(userTwo, sov, lockedSOV); + + await lockedSOV.withdrawAndStakeTokens(userTwo, { from: userTwo }); + + let [afterBal, ,] = await getTokenBalances(userTwo, sov, lockedSOV); + assert.equal(afterBal, beforeBal + unlockedBal, "Correct amount was not withdrawn."); + + vestingAddr = await vestingRegistry.getVesting(userTwo); + assert.notEqual(vestingAddr, zeroAddress, "Vesting Address should not be zero."); + + let balance = await staking.balanceOf(vestingAddr); + let lockedValue = Math.ceil((value * basisPoint) / 10000); + assert.equal(balance.toNumber(), Math.ceil(lockedValue), "Staking Balance does not match"); + + let getStakes = await staking.getStakes(vestingAddr); + assert.equal( + getStakes.stakes[0].toNumber(), + Math.ceil(lockedValue - Math.floor(lockedValue / duration) * (duration - 1)) + ); + for (let index = 1; index < getStakes.dates.length; index++) { + assert.equal(getStakes.stakes[index].toNumber(), Math.floor(lockedValue / duration)); + } + }); + + it("Using withdrawAndStakeTokensFrom() should correctly withdraw all unlocked tokens and stake locked tokens correctly.", async () => { + let value = randomValue() + 10; + await sov.mint(userFour, value); + await sov.approve(lockedSOV.address, value, { from: userFour }); + let basisPoint = 5000; // 50% will be unlocked, rest will go to locked balance. + await lockedSOV.deposit(userFour, value, basisPoint, { from: userFour }); + + let unlockedBal = Math.floor((value * basisPoint) / 10000); + let [beforeBal, ,] = await getTokenBalances(userFour, sov, lockedSOV); + + await lockedSOV.withdrawAndStakeTokensFrom(userFour, { from: userOne }); + + let [afterBal, ,] = await getTokenBalances(userFour, sov, lockedSOV); + assert.equal(afterBal, beforeBal + unlockedBal, "Correct amount was not withdrawn."); + + vestingAddr = await vestingRegistry.getVesting(userFour); + assert.notEqual(vestingAddr, zeroAddress, "Vesting Address should not be zero."); + + let balance = await staking.balanceOf(vestingAddr); + let lockedValue = Math.ceil((value * basisPoint) / 10000); + assert.equal(balance.toNumber(), Math.ceil(lockedValue), "Staking Balance does not match"); + + let getStakes = await staking.getStakes(vestingAddr); + assert.equal( + getStakes.stakes[0].toNumber(), + Math.ceil(lockedValue - Math.floor(lockedValue / duration) * (duration - 1)) + ); + for (let index = 1; index < getStakes.dates.length; index++) { + assert.equal(getStakes.stakes[index].toNumber(), Math.floor(lockedValue / duration)); + } + }); + + it("Starting the migration should update the contract status correctly.", async () => { + await checkStatus( + lockedSOV, + [1, 1, 1, 1, 1, 1, 1, 1], + userThree, + false, + cliff, + duration, + vestingRegistry.address, + zeroAddress, + zero, + zero, + false + ); + await lockedSOV.startMigration(newLockedSOV.address, { from: admin }); + await checkStatus( + lockedSOV, + [1, 1, 1, 1, 1, 1, 1, 1], + userThree, + true, + cliff, + duration, + vestingRegistry.address, + newLockedSOV.address, + zero, + zero, + false + ); + }); + + it("Using transfer() should correctly transfer locked token to new locked sov.", async () => { + // Creating the instance of newLockedSOV Contract. + newLockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [ + admin, + ]); + + // Creating the instance of LockedSOV Contract. + lockedSOV = await LockedSOV.new(sov.address, vestingRegistry.address, cliff, duration, [ + admin, + ]); + + // Adding lockedSOV as an admin in the Vesting Registry. + await vestingRegistry.addAdmin(lockedSOV.address); + + let value = randomValue() + 10; + await sov.mint(userOne, value); + await sov.approve(lockedSOV.address, value, { from: userOne }); + let basisPoint = 0; + await lockedSOV.deposit(userOne, value, basisPoint, { from: userOne }); + + // Migratioin started by Admin + await lockedSOV.startMigration(newLockedSOV.address, { from: admin }); + + await checkStatus( + lockedSOV, + [1, 1, 1, 1, 1, 1, 1, 1], + userOne, + true, + cliff, + duration, + vestingRegistry.address, + newLockedSOV.address, + value, + zero, + false + ); + await checkStatus( + newLockedSOV, + [1, 1, 1, 1, 1, 1, 1, 1], + userOne, + false, + cliff, + duration, + vestingRegistry.address, + zeroAddress, + zero, + zero, + false + ); + await lockedSOV.transfer({ from: userOne }); + await checkStatus( + lockedSOV, + [1, 1, 1, 1, 1, 1, 1, 1], + userOne, + true, + cliff, + duration, + vestingRegistry.address, + newLockedSOV.address, + zero, + zero, + false + ); + await checkStatus( + newLockedSOV, + [1, 1, 1, 1, 1, 1, 1, 1], + userOne, + false, + cliff, + duration, + vestingRegistry.address, + zeroAddress, + value, + zero, + false + ); + }); }); diff --git a/tests/OriginInvestorsClaim.test.js b/tests/OriginInvestorsClaim.test.js index 8a0cb60b8..4063c5d06 100644 --- a/tests/OriginInvestorsClaim.test.js +++ b/tests/OriginInvestorsClaim.test.js @@ -13,7 +13,14 @@ const { expect } = require("chai"); const { waffle } = require("hardhat"); const { loadFixture } = waffle; -const { expectRevert, expectEvent, constants, BN, balance, time } = require("@openzeppelin/test-helpers"); +const { + expectRevert, + expectEvent, + constants, + BN, + balance, + time, +} = require("@openzeppelin/test-helpers"); const { mineBlock, setNextBlockTimestamp } = require("./Utils/Ethereum"); @@ -39,326 +46,382 @@ const ZERO_ADDRESS = constants.ZERO_ADDRESS; const priceSats = "2500"; contract("OriginInvestorsClaim", (accounts) => { - let root, initializer, account1, investor1, investor2, investor3, investor4; - let SOV, kickoffTS; - let staking, stakingLogic, feeSharingProxy; - let vestingFactory, vestingLogic, vestingRegistry; - let investors; - let amounts, amount1, amount2, amount3, amount4; - let investorsClaim; - - function getTimeFromKickoff(offset) { - return kickoffTS.add(new BN(offset)); - } - - async function checkVestingContractCreatedAndStaked(txHash, receiver, cliff, amount) { - const vestingAddress = await vestingRegistry.getVesting(receiver); - await expectEvent.inTransaction(txHash, vestingRegistry, "VestingCreated", { - tokenOwner: receiver, - vesting: vestingAddress, - cliff: cliff, - duration: cliff, - amount: amount, - }); - - // event TokensStaked(address indexed vesting, uint256 amount); - await expectEvent.inTransaction(txHash, vestingRegistry, "TokensStaked", { - vesting: vestingAddress, - amount: amount, - }); - - const staked = await staking.balanceOf(vestingAddress); - expect(staked, "The vesting contract is not staked").to.be.bignumber.equal(amount); - } - - async function appendInvestorsAmountsList(_investors, _amounts, _fundContract = false) { - await investorsClaim.appendInvestorsAmountsList(_investors, _amounts); - if (_fundContract) { - await fundContract(_amounts); - } - } - - async function fundContract(_amounts = amounts) { - const totalReducer = (accumulator, currentValue) => accumulator.add(currentValue); - const total = _amounts.reduce(totalReducer); - await SOV.transfer(initializer, total); - await investorsClaim.authorizedBalanceWithdraw(root); // nullify balance - - await SOV.transfer(investorsClaim.address, total, { from: initializer }); - } - - async function createOriginInvestorsClaimContract({ - _initializeInvestorsList = false, - _fundContract = false, - _investors = investors, - _amounts = amounts, - }) { - investorsClaim = await OriginInvestorsClaim.new(vestingRegistry.address); - - if (_initializeInvestorsList) { - await appendInvestorsAmountsList(_investors, _amounts); - } - - if (_fundContract) { - await fundContract(_amounts); - } - } - - async function deploymentAndInitFixture(_wallets, _provider) { - await createOriginInvestorsClaimContract({ _initializeInvestorsList: false, _fundContract: true }); - } - - before(async () => { - [root, initializer, account1, investor1, investor2, investor3, investor4, claimedTokensReceiver, ...accounts] = accounts; - investors = [investor1, investor2, investor3, investor4]; - amount1 = new BN(ONE_THOUSAND); - amount2 = amount1.muln(2); - amount3 = amount1.muln(5); - amount4 = amount1.muln(3); - amounts = [amount1, amount2, amount3, amount4]; - - wrbtc = await TestWrbtc.new(); - SOV = await SOV_ABI.new(TOTAL_SUPPLY); - cSOV1 = await TestToken.new("cSOV1", "cSOV1", 18, TOTAL_SUPPLY); - cSOV2 = await TestToken.new("cSOV2", "cSOV2", 18, TOTAL_SUPPLY); - - stakingLogic = await StakingLogic.new(SOV.address); - staking = await StakingProxy.new(SOV.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingLogic.at(staking.address); - - feeSharingProxy = await FeeSharingProxy.new(ZERO_ADDRESS, staking.address); - - vestingLogic = await VestingLogic.new(); - vestingFactory = await VestingFactory.new(vestingLogic.address); - vestingRegistry = await VestingRegistry.new( - vestingFactory.address, - SOV.address, - [cSOV1.address, cSOV2.address], - priceSats, - staking.address, - feeSharingProxy.address, - account1 - ); - await vestingFactory.transferOwnership(vestingRegistry.address); - - kickoffTS = await staking.kickoffTS.call(); - - await createOriginInvestorsClaimContract({ _initializeInvestorsList: true }); - - await vestingRegistry.addAdmin(investorsClaim.address); - }); - - describe("setInvestorsList", () => { - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - it("set investors list", async () => { - // set first chunk of the list - let tx = await investorsClaim.appendInvestorsAmountsList(investors.slice(0, 2), amounts.slice(0, 2)); - expectEvent(tx, "InvestorsAmountsListAppended", { - qty: new BN(amounts.slice(0, 2).length), - amount: amount1.add(amount2), - }); - - await verifyAmounts(2); - - tx = await investorsClaim.appendInvestorsAmountsList(investors, amounts); - expectEvent(tx, "InvestorsAmountsListAppended", { - qty: new BN(amounts.length - amounts.slice(0, 2).length), - amount: amount3.add(amount4), - }); - - await verifyAmounts(3); - - async function verifyAmounts(upperBoundary) { - for (i = 0; i < upperBoundary; i++) { - amount = await investorsClaim.investorsAmountsList(investors[i]); - expect(amount).to.be.bignumber.equal(new BN(amounts[i]), `wrong investors list assignment at index ${i}`); - } - } - - await createOriginInvestorsClaimContract({ _initializeInvestorsList: true, _fundContract: false }); - await expectRevert( - investorsClaim.setInvestorsAmountsListInitialized(), - "OriginInvestorsClaim::setInvestorsAmountsList: the contract is not enough financed" - ); - - await fundContract(amounts); - await investorsClaim.setInvestorsAmountsListInitialized(); - - await expectRevert( - investorsClaim.appendInvestorsAmountsList(investors, amounts), - "OriginInvestorsClaim::notInitialized: the investors list should not be set as initialized" - ); - }); - - it("cannot add investors to the list after setInvestorsAmountsListIntilized() called", async () => { - await createOriginInvestorsClaimContract({ _initializeInvestorsList: true, _fundContract: true }); - - await investorsClaim.setInvestorsAmountsListInitialized(); - - await expectRevert( - investorsClaim.appendInvestorsAmountsList(investors, amounts), - "OriginInvestorsClaim::notInitialized: the investors list should not be set as initialized" - ); - }); - - it("fails if investors.length != amounts.length", async () => { - await createOriginInvestorsClaimContract({ _initializeInvestorsList: true, _fundContract: true }); - - const investorsReduced = investors.slice(1); - await expectRevert( - investorsClaim.appendInvestorsAmountsList(investorsReduced, amounts), - "investors.length != claimAmounts.length" - ); - }); - - it("only authorised can whitelist investors", async () => { - await createOriginInvestorsClaimContract({ _initializeInvestorsList: true, _fundContract: true }); - - await expectRevert( - investorsClaim.appendInvestorsAmountsList(investors, amounts, { from: account1 }), - "OriginInvestorsClaim::onlyAuthorized: should be authorized" - ); - - // Try again by adding account1 as admin - await investorsClaim.addAdmin(account1); - await investorsClaim.appendInvestorsAmountsList(investors, amounts, { from: account1 }); - - // Try again by removing account1 from admin - await investorsClaim.removeAdmin(account1); - await expectRevert( - investorsClaim.appendInvestorsAmountsList(investors, amounts, { from: account1 }), - "OriginInvestorsClaim::onlyAuthorized: should be authorized" - ); - }); - }); - - describe("process claims", () => { - before(async () => { - await createOriginInvestorsClaimContract({ _initializeInvestorsList: true, _fundContract: true }); - await vestingRegistry.addAdmin(investorsClaim.address); - // await fundContract({ approve: true, transfer: false }); - }); - - it("should revert when claiming from an investor having an active vesting contract", async () => { - await investorsClaim.setInvestorsAmountsListInitialized(); - - // Create an active vesting contract for investor4 - let amount = new BN(1000000); - let cliff = FOUR_WEEKS; - let duration = FOUR_WEEKS.mul(new BN(20)); - await vestingRegistry.createVesting(investor4, amount, cliff, duration); - - // Should fail due to conflict w/ current vesting contract - await expectRevert( - investorsClaim.claim({ from: investor4 }), - "OriginInvestorsClaim::withdraw: the claimer has an active vesting contract" - ); - }); - - it("should revert when no funds available to transfer", async () => { - // Nullify balance - await investorsClaim.authorizedBalanceWithdraw(root); - - // Should fail due to lack of funds - await expectRevert(investorsClaim.claim({ from: investor3 }), "ERC20: transfer amount exceeds balance"); - }); - - it("should create vesting contract within vesting period", async () => { - await createOriginInvestorsClaimContract({ _initializeInvestorsList: true, _fundContract: true }); - await vestingRegistry.addAdmin(investorsClaim.address); - - await expectRevert( - investorsClaim.claim({ from: investor1 }), - "OriginInvestorsClaim::initialized: the investors list has not been set yet" - ); - - await investorsClaim.setInvestorsAmountsListInitialized(); - - const timeFromKickoff = getTimeFromKickoff(ONE_WEEK); - await setNextBlockTimestamp(timeFromKickoff.toNumber()); - tx = await investorsClaim.claim({ from: investor1 }); - - await expectEvent(tx.receipt, "ClaimVested", { - investor: investor1, - amount: amount1, - }); - - let txHash = tx.receipt.transactionHash; - - await checkVestingContractCreatedAndStaked(txHash, investor1, SIX_WEEKS.sub(ONE_WEEK), amount1); - - expect(await investorsClaim.investorsAmountsList(investor1)).to.be.bignumber.equal(new BN(0)); - - await setNextBlockTimestamp(getTimeFromKickoff(SIX_WEEKS).subn(1).toNumber()); - tx = await investorsClaim.claim({ from: investor2 }); - - await expectEvent(tx.receipt, "ClaimVested", { - investor: investor2, - amount: amount2, - }); - - txHash = tx.receipt.transactionHash; - - await checkVestingContractCreatedAndStaked(txHash, investor2, new BN(1), amount2); - - expect(await investorsClaim.investorsAmountsList(investor2)).to.be.bignumber.equal(new BN(0)); - - // TODO: check vesting created and the user is in the list - }); - - // address1 claims - failure - - it("should get SOV directly when claiming after the cliff", async () => { - let balance; - // investor2 claims within cliff - vesting contract created with amounts[1] - // move time 1 ms past cliff - await setNextBlockTimestamp((await getTimeFromKickoff(SIX_WEEKS)).addn(100).toNumber()); - await mineBlock(); - - balance = await SOV.balanceOf(investor3); - expect(balance).to.be.bignumber.equal(new BN(0)); - - // await SOV.transfer(vestingRegistry.address, amount3); - tx = await investorsClaim.claim({ from: investor3 }); - - await expectEvent(tx.receipt, "ClaimTransferred", { - investor: investor3, - amount: amount3, - }); - - balance = await SOV.balanceOf(investor3); - expect(balance).to.be.bignumber.equal(amount3); - }); - - it("investors with vesting contracts created cannot withdraw here", async () => { - await expectRevert( - investorsClaim.claim({ from: investor1 }), - "OriginInvestorsClaim::onlyWhitelisted: not whitelisted or already claimed" - ); - }); - - it("can withdraw only once", async () => { - await expectRevert( - investorsClaim.claim({ from: investor3 }), - "OriginInvestorsClaim::onlyWhitelisted: not whitelisted or already claimed" - ); - }); - - it("owner should be able to move SOV balances from the contract", async () => { - const balanceBefore = await SOV.balanceOf(investorsClaim.address); - - await investorsClaim.authorizedBalanceWithdraw(account1); - - expect(await SOV.balanceOf(account1)).to.be.bignumber.equal(balanceBefore); - expect(await SOV.balanceOf(investorsClaim.address)).to.be.bignumber.equal(new BN(0)); - }); - - it("allows to claim only from whitelisted addresses", async () => { - await expectRevert(investorsClaim.claim({ from: account1 }), "not whitelisted or already claimed"); - }); - }); + let root, initializer, account1, investor1, investor2, investor3, investor4; + let SOV, kickoffTS; + let staking, stakingLogic, feeSharingProxy; + let vestingFactory, vestingLogic, vestingRegistry; + let investors; + let amounts, amount1, amount2, amount3, amount4; + let investorsClaim; + + function getTimeFromKickoff(offset) { + return kickoffTS.add(new BN(offset)); + } + + async function checkVestingContractCreatedAndStaked(txHash, receiver, cliff, amount) { + const vestingAddress = await vestingRegistry.getVesting(receiver); + await expectEvent.inTransaction(txHash, vestingRegistry, "VestingCreated", { + tokenOwner: receiver, + vesting: vestingAddress, + cliff: cliff, + duration: cliff, + amount: amount, + }); + + // event TokensStaked(address indexed vesting, uint256 amount); + await expectEvent.inTransaction(txHash, vestingRegistry, "TokensStaked", { + vesting: vestingAddress, + amount: amount, + }); + + const staked = await staking.balanceOf(vestingAddress); + expect(staked, "The vesting contract is not staked").to.be.bignumber.equal(amount); + } + + async function appendInvestorsAmountsList(_investors, _amounts, _fundContract = false) { + await investorsClaim.appendInvestorsAmountsList(_investors, _amounts); + if (_fundContract) { + await fundContract(_amounts); + } + } + + async function fundContract(_amounts = amounts) { + const totalReducer = (accumulator, currentValue) => accumulator.add(currentValue); + const total = _amounts.reduce(totalReducer); + await SOV.transfer(initializer, total); + await investorsClaim.authorizedBalanceWithdraw(root); // nullify balance + + await SOV.transfer(investorsClaim.address, total, { from: initializer }); + } + + async function createOriginInvestorsClaimContract({ + _initializeInvestorsList = false, + _fundContract = false, + _investors = investors, + _amounts = amounts, + }) { + investorsClaim = await OriginInvestorsClaim.new(vestingRegistry.address); + + if (_initializeInvestorsList) { + await appendInvestorsAmountsList(_investors, _amounts); + } + + if (_fundContract) { + await fundContract(_amounts); + } + } + + async function deploymentAndInitFixture(_wallets, _provider) { + await createOriginInvestorsClaimContract({ + _initializeInvestorsList: false, + _fundContract: true, + }); + } + + before(async () => { + [ + root, + initializer, + account1, + investor1, + investor2, + investor3, + investor4, + claimedTokensReceiver, + ...accounts + ] = accounts; + investors = [investor1, investor2, investor3, investor4]; + amount1 = new BN(ONE_THOUSAND); + amount2 = amount1.muln(2); + amount3 = amount1.muln(5); + amount4 = amount1.muln(3); + amounts = [amount1, amount2, amount3, amount4]; + + wrbtc = await TestWrbtc.new(); + SOV = await SOV_ABI.new(TOTAL_SUPPLY); + cSOV1 = await TestToken.new("cSOV1", "cSOV1", 18, TOTAL_SUPPLY); + cSOV2 = await TestToken.new("cSOV2", "cSOV2", 18, TOTAL_SUPPLY); + + stakingLogic = await StakingLogic.new(SOV.address); + staking = await StakingProxy.new(SOV.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); + + feeSharingProxy = await FeeSharingProxy.new(ZERO_ADDRESS, staking.address); + + vestingLogic = await VestingLogic.new(); + vestingFactory = await VestingFactory.new(vestingLogic.address); + vestingRegistry = await VestingRegistry.new( + vestingFactory.address, + SOV.address, + [cSOV1.address, cSOV2.address], + priceSats, + staking.address, + feeSharingProxy.address, + account1 + ); + await vestingFactory.transferOwnership(vestingRegistry.address); + + kickoffTS = await staking.kickoffTS.call(); + + await createOriginInvestorsClaimContract({ _initializeInvestorsList: true }); + + await vestingRegistry.addAdmin(investorsClaim.address); + }); + + describe("setInvestorsList", () => { + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + it("set investors list", async () => { + // set first chunk of the list + let tx = await investorsClaim.appendInvestorsAmountsList( + investors.slice(0, 2), + amounts.slice(0, 2) + ); + expectEvent(tx, "InvestorsAmountsListAppended", { + qty: new BN(amounts.slice(0, 2).length), + amount: amount1.add(amount2), + }); + + await verifyAmounts(2); + + tx = await investorsClaim.appendInvestorsAmountsList(investors, amounts); + expectEvent(tx, "InvestorsAmountsListAppended", { + qty: new BN(amounts.length - amounts.slice(0, 2).length), + amount: amount3.add(amount4), + }); + + await verifyAmounts(3); + + async function verifyAmounts(upperBoundary) { + for (i = 0; i < upperBoundary; i++) { + amount = await investorsClaim.investorsAmountsList(investors[i]); + expect(amount).to.be.bignumber.equal( + new BN(amounts[i]), + `wrong investors list assignment at index ${i}` + ); + } + } + + await createOriginInvestorsClaimContract({ + _initializeInvestorsList: true, + _fundContract: false, + }); + await expectRevert( + investorsClaim.setInvestorsAmountsListInitialized(), + "OriginInvestorsClaim::setInvestorsAmountsList: the contract is not enough financed" + ); + + await fundContract(amounts); + await investorsClaim.setInvestorsAmountsListInitialized(); + + await expectRevert( + investorsClaim.appendInvestorsAmountsList(investors, amounts), + "OriginInvestorsClaim::notInitialized: the investors list should not be set as initialized" + ); + }); + + it("cannot add investors to the list after setInvestorsAmountsListIntilized() called", async () => { + await createOriginInvestorsClaimContract({ + _initializeInvestorsList: true, + _fundContract: true, + }); + + await investorsClaim.setInvestorsAmountsListInitialized(); + + await expectRevert( + investorsClaim.appendInvestorsAmountsList(investors, amounts), + "OriginInvestorsClaim::notInitialized: the investors list should not be set as initialized" + ); + }); + + it("fails if investors.length != amounts.length", async () => { + await createOriginInvestorsClaimContract({ + _initializeInvestorsList: true, + _fundContract: true, + }); + + const investorsReduced = investors.slice(1); + await expectRevert( + investorsClaim.appendInvestorsAmountsList(investorsReduced, amounts), + "investors.length != claimAmounts.length" + ); + }); + + it("only authorised can whitelist investors", async () => { + await createOriginInvestorsClaimContract({ + _initializeInvestorsList: true, + _fundContract: true, + }); + + await expectRevert( + investorsClaim.appendInvestorsAmountsList(investors, amounts, { from: account1 }), + "OriginInvestorsClaim::onlyAuthorized: should be authorized" + ); + + // Try again by adding account1 as admin + await investorsClaim.addAdmin(account1); + await investorsClaim.appendInvestorsAmountsList(investors, amounts, { + from: account1, + }); + + // Try again by removing account1 from admin + await investorsClaim.removeAdmin(account1); + await expectRevert( + investorsClaim.appendInvestorsAmountsList(investors, amounts, { from: account1 }), + "OriginInvestorsClaim::onlyAuthorized: should be authorized" + ); + }); + }); + + describe("process claims", () => { + before(async () => { + await createOriginInvestorsClaimContract({ + _initializeInvestorsList: true, + _fundContract: true, + }); + await vestingRegistry.addAdmin(investorsClaim.address); + // await fundContract({ approve: true, transfer: false }); + }); + + it("should revert when claiming from an investor having an active vesting contract", async () => { + await investorsClaim.setInvestorsAmountsListInitialized(); + + // Create an active vesting contract for investor4 + let amount = new BN(1000000); + let cliff = FOUR_WEEKS; + let duration = FOUR_WEEKS.mul(new BN(20)); + await vestingRegistry.createVesting(investor4, amount, cliff, duration); + + // Should fail due to conflict w/ current vesting contract + await expectRevert( + investorsClaim.claim({ from: investor4 }), + "OriginInvestorsClaim::withdraw: the claimer has an active vesting contract" + ); + }); + + it("should revert when no funds available to transfer", async () => { + // Nullify balance + await investorsClaim.authorizedBalanceWithdraw(root); + + // Should fail due to lack of funds + await expectRevert( + investorsClaim.claim({ from: investor3 }), + "ERC20: transfer amount exceeds balance" + ); + }); + + it("should create vesting contract within vesting period", async () => { + await createOriginInvestorsClaimContract({ + _initializeInvestorsList: true, + _fundContract: true, + }); + await vestingRegistry.addAdmin(investorsClaim.address); + + await expectRevert( + investorsClaim.claim({ from: investor1 }), + "OriginInvestorsClaim::initialized: the investors list has not been set yet" + ); + + await investorsClaim.setInvestorsAmountsListInitialized(); + + const timeFromKickoff = getTimeFromKickoff(ONE_WEEK); + await setNextBlockTimestamp(timeFromKickoff.toNumber()); + tx = await investorsClaim.claim({ from: investor1 }); + + await expectEvent(tx.receipt, "ClaimVested", { + investor: investor1, + amount: amount1, + }); + + let txHash = tx.receipt.transactionHash; + + await checkVestingContractCreatedAndStaked( + txHash, + investor1, + SIX_WEEKS.sub(ONE_WEEK), + amount1 + ); + + expect(await investorsClaim.investorsAmountsList(investor1)).to.be.bignumber.equal( + new BN(0) + ); + + await setNextBlockTimestamp(getTimeFromKickoff(SIX_WEEKS).subn(1).toNumber()); + tx = await investorsClaim.claim({ from: investor2 }); + + await expectEvent(tx.receipt, "ClaimVested", { + investor: investor2, + amount: amount2, + }); + + txHash = tx.receipt.transactionHash; + + await checkVestingContractCreatedAndStaked(txHash, investor2, new BN(1), amount2); + + expect(await investorsClaim.investorsAmountsList(investor2)).to.be.bignumber.equal( + new BN(0) + ); + + // TODO: check vesting created and the user is in the list + }); + + // address1 claims - failure + + it("should get SOV directly when claiming after the cliff", async () => { + let balance; + // investor2 claims within cliff - vesting contract created with amounts[1] + // move time 1 ms past cliff + await setNextBlockTimestamp( + (await getTimeFromKickoff(SIX_WEEKS)).addn(100).toNumber() + ); + await mineBlock(); + + balance = await SOV.balanceOf(investor3); + expect(balance).to.be.bignumber.equal(new BN(0)); + + // await SOV.transfer(vestingRegistry.address, amount3); + tx = await investorsClaim.claim({ from: investor3 }); + + await expectEvent(tx.receipt, "ClaimTransferred", { + investor: investor3, + amount: amount3, + }); + + balance = await SOV.balanceOf(investor3); + expect(balance).to.be.bignumber.equal(amount3); + }); + + it("investors with vesting contracts created cannot withdraw here", async () => { + await expectRevert( + investorsClaim.claim({ from: investor1 }), + "OriginInvestorsClaim::onlyWhitelisted: not whitelisted or already claimed" + ); + }); + + it("can withdraw only once", async () => { + await expectRevert( + investorsClaim.claim({ from: investor3 }), + "OriginInvestorsClaim::onlyWhitelisted: not whitelisted or already claimed" + ); + }); + + it("owner should be able to move SOV balances from the contract", async () => { + const balanceBefore = await SOV.balanceOf(investorsClaim.address); + + await investorsClaim.authorizedBalanceWithdraw(account1); + + expect(await SOV.balanceOf(account1)).to.be.bignumber.equal(balanceBefore); + expect(await SOV.balanceOf(investorsClaim.address)).to.be.bignumber.equal(new BN(0)); + }); + + it("allows to claim only from whitelisted addresses", async () => { + await expectRevert( + investorsClaim.claim({ from: account1 }), + "not whitelisted or already claimed" + ); + }); + }); }); diff --git a/tests/PauseModules.test.js b/tests/PauseModules.test.js index 01c74a4f1..a0ad73d5e 100644 --- a/tests/PauseModules.test.js +++ b/tests/PauseModules.test.js @@ -40,245 +40,275 @@ const oneEth = new BN(wei("1", "ether")); const hunEth = new BN(wei("100", "ether")); const { increaseTime, blockNumber } = require("./Utils/Ethereum"); const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getLoanToken, - getLoanTokenWRBTC, - loan_pool_setup, - set_demand_curve, - getPriceFeeds, - getSovryn, - decodeLogs, - getSOV, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getLoanToken, + getLoanTokenWRBTC, + loan_pool_setup, + set_demand_curve, + getPriceFeeds, + getSovryn, + decodeLogs, + getSOV, } = require("./Utils/initializer.js"); const { ZERO_ADDRESS } = require("@openzeppelin/test-helpers/src/constants"); contract("Pause Modules", (accounts) => { - let sovryn, SUSD, WRBTC, RBTC, BZRX, loanToken, loanTokenWRBTC, priceFeeds, SOV; - let loanParams, loanParamsId; - /// @note https://stackoverflow.com/questions/68182729/implementing-fixtures-with-nomiclabs-hardhat-waffle - async function fixtureInitialize(_wallets, _provider) { - SUSD = await getSUSD(); // Underlying Token - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + let sovryn, SUSD, WRBTC, RBTC, BZRX, loanToken, loanTokenWRBTC, priceFeeds, SOV; + let loanParams, loanParamsId; + /// @note https://stackoverflow.com/questions/68182729/implementing-fixtures-with-nomiclabs-hardhat-waffle + async function fixtureInitialize(_wallets, _provider) { + SUSD = await getSUSD(); // Underlying Token + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - loanToken = await getLoanToken(owner, sovryn, WRBTC, SUSD); - loanTokenWRBTC = await getLoanTokenWRBTC(owner, sovryn, WRBTC, SUSD); - await loan_pool_setup(sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC); - SOV = await getSOV(sovryn, priceFeeds, SUSD, accounts); + loanToken = await getLoanToken(owner, sovryn, WRBTC, SUSD); + loanTokenWRBTC = await getLoanTokenWRBTC(owner, sovryn, WRBTC, SUSD); + await loan_pool_setup(sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC); + SOV = await getSOV(sovryn, priceFeeds, SUSD, accounts); - loanParams = { - id: "0x0000000000000000000000000000000000000000000000000000000000000000", - active: false, - owner: ZERO_ADDRESS, - loanToken: SUSD.address, - collateralToken: loanTokenWRBTC.address, - minInitialMargin: wei("50", "ether"), - maintenanceMargin: wei("15", "ether"), - maxLoanTerm: "2419200", - }; - } + loanParams = { + id: "0x0000000000000000000000000000000000000000000000000000000000000000", + active: false, + owner: ZERO_ADDRESS, + loanToken: SUSD.address, + collateralToken: loanTokenWRBTC.address, + minInitialMargin: wei("50", "ether"), + maintenanceMargin: wei("15", "ether"), + maxLoanTerm: "2419200", + }; + } - before(async () => { - [owner, trader, referrer, account1, account2, ...accounts] = accounts; - await loadFixture(fixtureInitialize); - }); + before(async () => { + [owner, trader, referrer, account1, account2, ...accounts] = accounts; + await loadFixture(fixtureInitialize); + }); - const setup_rollover_test = async (RBTC, SUSD, accounts, loanToken, set_demand_curve, sovryn) => { - await set_demand_curve(loanToken); - await SUSD.approve(loanToken.address, new BN(10).pow(new BN(40))); - const lender = accounts[0]; - const borrower = accounts[1]; - let tx = await sovryn.togglePaused(false); // Unpaused - await expectEvent(tx, "TogglePaused", { - sender: owner, - oldFlag: true, - newFlag: false, - }); - await loanToken.mint(lender, new BN(10).pow(new BN(30))); - const loan_token_sent = hunEth; - await SUSD.mint(borrower, loan_token_sent); - await SUSD.approve(loanToken.address, loan_token_sent, { from: borrower }); - const { receipt } = await loanToken.marginTrade( - "0x0", // loanId (0 for new loans) - new BN(2).mul(oneEth), // leverageAmount - loan_token_sent, // loanTokenSent - 0, // no collateral token sent - RBTC.address, // collateralTokenAddress - borrower, // trader, - 0, // slippage - "0x", // loanDataBytes (only required with ether) - { from: borrower } - ); + const setup_rollover_test = async ( + RBTC, + SUSD, + accounts, + loanToken, + set_demand_curve, + sovryn + ) => { + await set_demand_curve(loanToken); + await SUSD.approve(loanToken.address, new BN(10).pow(new BN(40))); + const lender = accounts[0]; + const borrower = accounts[1]; + let tx = await sovryn.togglePaused(false); // Unpaused + await expectEvent(tx, "TogglePaused", { + sender: owner, + oldFlag: true, + newFlag: false, + }); + await loanToken.mint(lender, new BN(10).pow(new BN(30))); + const loan_token_sent = hunEth; + await SUSD.mint(borrower, loan_token_sent); + await SUSD.approve(loanToken.address, loan_token_sent, { from: borrower }); + const { receipt } = await loanToken.marginTrade( + "0x0", // loanId (0 for new loans) + new BN(2).mul(oneEth), // leverageAmount + loan_token_sent, // loanTokenSent + 0, // no collateral token sent + RBTC.address, // collateralTokenAddress + borrower, // trader, + 0, // slippage + "0x", // loanDataBytes (only required with ether) + { from: borrower } + ); - const decode = decodeLogs(receipt.rawLogs, LoanOpeningsEvents, "Trade"); - const loan_id = decode[0].args["loanId"]; - const loan = await sovryn.getLoan(loan_id); - const num = await blockNumber(); - let currentBlock = await web3.eth.getBlock(num); - const block_timestamp = currentBlock.timestamp; - const time_until_loan_end = loan["endTimestamp"] - block_timestamp; - await increaseTime(time_until_loan_end); - return [borrower, loan, loan_id, parseInt(loan["endTimestamp"])]; - }; + const decode = decodeLogs(receipt.rawLogs, LoanOpeningsEvents, "Trade"); + const loan_id = decode[0].args["loanId"]; + const loan = await sovryn.getLoan(loan_id); + const num = await blockNumber(); + let currentBlock = await web3.eth.getBlock(num); + const block_timestamp = currentBlock.timestamp; + const time_until_loan_end = loan["endTimestamp"] - block_timestamp; + await increaseTime(time_until_loan_end); + return [borrower, loan, loan_id, parseInt(loan["endTimestamp"])]; + }; - describe("Pause Affiliates", () => { - it("Should pause setting Affiliates referrer", async () => { - loanTokenLogic = await MockLoanTokenLogic.new(); - testWrbtc = await TestWrbtc.new(); - doc = await TestToken.new("dollar on chain", "DOC", 18, wei("20000", "ether")); - loanTokenV1 = await LoanToken.new(owner, loanTokenLogic.address, sovryn.address, testWrbtc.address); - await loanTokenV1.initialize(doc.address, "SUSD", "SUSD"); - loanTokenV2 = await MockLoanTokenLogic.at(loanTokenV1.address); - const loanTokenAddress = await loanTokenV1.loanTokenAddress(); - if (owner == (await sovryn.owner())) { - await sovryn.setLoanPool([loanTokenV2.address], [loanTokenAddress]); - } - let tx = await sovryn.togglePaused(true); // Paused - await expectRevert(sovryn.togglePaused(true), "Can't toggle"); - await expectEvent(tx, "TogglePaused", { - sender: owner, - oldFlag: false, - newFlag: true, - }); - await expectRevert(loanTokenV2.setAffiliatesReferrer(trader, referrer), "Paused"); - }); - }); + describe("Pause Affiliates", () => { + it("Should pause setting Affiliates referrer", async () => { + loanTokenLogic = await MockLoanTokenLogic.new(); + testWrbtc = await TestWrbtc.new(); + doc = await TestToken.new("dollar on chain", "DOC", 18, wei("20000", "ether")); + loanTokenV1 = await LoanToken.new( + owner, + loanTokenLogic.address, + sovryn.address, + testWrbtc.address + ); + await loanTokenV1.initialize(doc.address, "SUSD", "SUSD"); + loanTokenV2 = await MockLoanTokenLogic.at(loanTokenV1.address); + const loanTokenAddress = await loanTokenV1.loanTokenAddress(); + if (owner == (await sovryn.owner())) { + await sovryn.setLoanPool([loanTokenV2.address], [loanTokenAddress]); + } + let tx = await sovryn.togglePaused(true); // Paused + await expectRevert(sovryn.togglePaused(true), "Can't toggle"); + await expectEvent(tx, "TogglePaused", { + sender: owner, + oldFlag: false, + newFlag: true, + }); + await expectRevert(loanTokenV2.setAffiliatesReferrer(trader, referrer), "Paused"); + }); + }); - describe("Pause LoanClosingBase", () => { - it("Test rollover reward payment", async () => { - // prepare the test - const [, initial_loan, loan_id] = await setup_rollover_test(RBTC, SUSD, accounts, loanToken, set_demand_curve, sovryn); + describe("Pause LoanClosingBase", () => { + it("Test rollover reward payment", async () => { + // prepare the test + const [, initial_loan, loan_id] = await setup_rollover_test( + RBTC, + SUSD, + accounts, + loanToken, + set_demand_curve, + sovryn + ); - const num = await blockNumber(); - let currentBlock = await web3.eth.getBlock(num); - const block_timestamp = currentBlock.timestamp; - const time_until_loan_end = initial_loan["endTimestamp"] - block_timestamp; - await increaseTime(time_until_loan_end); + const num = await blockNumber(); + let currentBlock = await web3.eth.getBlock(num); + const block_timestamp = currentBlock.timestamp; + const time_until_loan_end = initial_loan["endTimestamp"] - block_timestamp; + await increaseTime(time_until_loan_end); - const receiver = accounts[3]; - expect((await RBTC.balanceOf(receiver)).toNumber() == 0).to.be.true; - let tx = await sovryn.togglePaused(true); // Paused - await expectEvent(tx, "TogglePaused", { - sender: owner, - oldFlag: false, - newFlag: true, - }); - await expectRevert(sovryn.rollover(loan_id, "0x", { from: receiver }), "Paused"); - }); - }); + const receiver = accounts[3]; + expect((await RBTC.balanceOf(receiver)).toNumber() == 0).to.be.true; + let tx = await sovryn.togglePaused(true); // Paused + await expectEvent(tx, "TogglePaused", { + sender: owner, + oldFlag: false, + newFlag: true, + }); + await expectRevert(sovryn.rollover(loan_id, "0x", { from: receiver }), "Paused"); + }); + }); - describe("Pause ProtocolSettings", () => { - it("Should pause setting SOV token address", async () => { - await expectRevert(sovryn.setSOVTokenAddress(SOV.address), "Paused"); - }); + describe("Pause ProtocolSettings", () => { + it("Should pause setting SOV token address", async () => { + await expectRevert(sovryn.setSOVTokenAddress(SOV.address), "Paused"); + }); - it("Should pause setting LockedSOV token address", async () => { - const lockedSOV = await LockedSOV.new(SOV.address, [accounts[0]]); - await expectRevert(sovryn.setLockedSOVAddress(lockedSOV.address), "Paused"); - }); + it("Should pause setting LockedSOV token address", async () => { + const lockedSOV = await LockedSOV.new(SOV.address, [accounts[0]]); + await expectRevert(sovryn.setLockedSOVAddress(lockedSOV.address), "Paused"); + }); - it("Should pause setting affiliate fee percent", async () => { - const affiliateFeePercent = web3.utils.toWei("20", "ether"); - await expectRevert(sovryn.setAffiliateFeePercent(affiliateFeePercent), "Paused"); - }); + it("Should pause setting affiliate fee percent", async () => { + const affiliateFeePercent = web3.utils.toWei("20", "ether"); + await expectRevert(sovryn.setAffiliateFeePercent(affiliateFeePercent), "Paused"); + }); - it("Should pause setting affiliate fee percent", async () => { - const affiliateTradingTokenFeePercent = web3.utils.toWei("20", "ether"); - await expectRevert(sovryn.setAffiliateTradingTokenFeePercent(affiliateTradingTokenFeePercent), "Paused"); - }); + it("Should pause setting affiliate fee percent", async () => { + const affiliateTradingTokenFeePercent = web3.utils.toWei("20", "ether"); + await expectRevert( + sovryn.setAffiliateTradingTokenFeePercent(affiliateTradingTokenFeePercent), + "Paused" + ); + }); - it("Should set affiliate fee percent when Unpaused", async () => { - let tx = await sovryn.togglePaused(false); // Unpaused - await expectRevert(sovryn.togglePaused(false), "Can't toggle"); - await expectEvent(tx, "TogglePaused", { - sender: owner, - oldFlag: true, - newFlag: false, - }); - const affiliateTradingTokenFeePercent = web3.utils.toWei("20", "ether"); - const invalidAffiliateTradingTokenFeePercent = web3.utils.toWei("101", "ether"); - // Should revert if set with non owner - await expectRevert( - sovryn.setAffiliateTradingTokenFeePercent(affiliateTradingTokenFeePercent, { from: accounts[1] }), - "unauthorized" - ); - // Should revert if value too high - await expectRevert(sovryn.setAffiliateTradingTokenFeePercent(invalidAffiliateTradingTokenFeePercent), "value too high"); + it("Should set affiliate fee percent when Unpaused", async () => { + let tx = await sovryn.togglePaused(false); // Unpaused + await expectRevert(sovryn.togglePaused(false), "Can't toggle"); + await expectEvent(tx, "TogglePaused", { + sender: owner, + oldFlag: true, + newFlag: false, + }); + const affiliateTradingTokenFeePercent = web3.utils.toWei("20", "ether"); + const invalidAffiliateTradingTokenFeePercent = web3.utils.toWei("101", "ether"); + // Should revert if set with non owner + await expectRevert( + sovryn.setAffiliateTradingTokenFeePercent(affiliateTradingTokenFeePercent, { + from: accounts[1], + }), + "unauthorized" + ); + // Should revert if value too high + await expectRevert( + sovryn.setAffiliateTradingTokenFeePercent(invalidAffiliateTradingTokenFeePercent), + "value too high" + ); - await sovryn.setAffiliateTradingTokenFeePercent(affiliateTradingTokenFeePercent); - expect((await sovryn.affiliateTradingTokenFeePercent()).toString() == affiliateTradingTokenFeePercent).to.be.true; - }); - }); + await sovryn.setAffiliateTradingTokenFeePercent(affiliateTradingTokenFeePercent); + expect( + (await sovryn.affiliateTradingTokenFeePercent()).toString() == + affiliateTradingTokenFeePercent + ).to.be.true; + }); + }); - describe("Pause LoanSettings", () => { - it("Able to setupLoanParams & disableLoanParamsEvents when unpaused", async () => { - let tx = await sovryn.setupLoanParams([Object.values(loanParams)]); - loanParamsId = tx.logs[1].args.id; + describe("Pause LoanSettings", () => { + it("Able to setupLoanParams & disableLoanParamsEvents when unpaused", async () => { + let tx = await sovryn.setupLoanParams([Object.values(loanParams)]); + loanParamsId = tx.logs[1].args.id; - tx = await sovryn.disableLoanParams([loanParamsId], { from: owner }); + tx = await sovryn.disableLoanParams([loanParamsId], { from: owner }); - await expectEvent(tx, "LoanParamsIdDisabled", { owner: owner }); - assert(tx.logs[1]["id"] != "0x0"); + await expectEvent(tx, "LoanParamsIdDisabled", { owner: owner }); + assert(tx.logs[1]["id"] != "0x0"); - await expectEvent(tx, "LoanParamsDisabled", { - owner: owner, - loanToken: SUSD.address, - collateralToken: loanTokenWRBTC.address, - minInitialMargin: wei("50", "ether"), - maintenanceMargin: wei("15", "ether"), - maxLoanTerm: "2419200", - }); - assert(tx.logs[0]["id"] != "0x0"); - }); + await expectEvent(tx, "LoanParamsDisabled", { + owner: owner, + loanToken: SUSD.address, + collateralToken: loanTokenWRBTC.address, + minInitialMargin: wei("50", "ether"), + maintenanceMargin: wei("15", "ether"), + maxLoanTerm: "2419200", + }); + assert(tx.logs[0]["id"] != "0x0"); + }); - it("setupLoanParams & disableLoanParamsEvents freezes when protocol is paused", async () => { - let tx = await sovryn.togglePaused(true); // Paused - await expectEvent(tx, "TogglePaused", { - sender: owner, - oldFlag: false, - newFlag: true, - }); - await expectRevert(sovryn.setupLoanParams([Object.values(loanParams)]), "Paused"); - }); - }); + it("setupLoanParams & disableLoanParamsEvents freezes when protocol is paused", async () => { + let tx = await sovryn.togglePaused(true); // Paused + await expectEvent(tx, "TogglePaused", { + sender: owner, + oldFlag: false, + newFlag: true, + }); + await expectRevert(sovryn.setupLoanParams([Object.values(loanParams)]), "Paused"); + }); + }); - describe("Testing isProtocolPaused()", () => { - it("isProtocolPaused() returns correct result when toggling pause/unpause", async () => { - await loadFixture(fixtureInitialize); - await sovryn.togglePaused(true); - expect(await sovryn.isProtocolPaused()).to.be.true; + describe("Testing isProtocolPaused()", () => { + it("isProtocolPaused() returns correct result when toggling pause/unpause", async () => { + await loadFixture(fixtureInitialize); + await sovryn.togglePaused(true); + expect(await sovryn.isProtocolPaused()).to.be.true; - // Check deterministic result when trying to set current value - expectRevert.unspecified(sovryn.togglePaused(true)); - expect(await sovryn.isProtocolPaused()).to.be.true; + // Check deterministic result when trying to set current value + expectRevert.unspecified(sovryn.togglePaused(true)); + expect(await sovryn.isProtocolPaused()).to.be.true; - // Pause true -> false - await sovryn.togglePaused(false); - expect(await sovryn.isProtocolPaused()).to.be.false; - expectRevert.unspecified(sovryn.togglePaused(false)); - expect(await sovryn.isProtocolPaused()).to.be.false; + // Pause true -> false + await sovryn.togglePaused(false); + expect(await sovryn.isProtocolPaused()).to.be.false; + expectRevert.unspecified(sovryn.togglePaused(false)); + expect(await sovryn.isProtocolPaused()).to.be.false; - // Pause false -> true - await sovryn.togglePaused(true); - expect(await sovryn.isProtocolPaused()).to.be.true; - }); - }); + // Pause false -> true + await sovryn.togglePaused(true); + expect(await sovryn.isProtocolPaused()).to.be.true; + }); + }); - describe("Testing Pausable contract", () => { - it("Pausable function runs if not paused", async () => { - testCoverage = await TestCoverage.new(); - await testCoverage.dummyPausableFunction(); - }); - it("Pausable function reverts if paused", async () => { - testCoverage = await TestCoverage.new(); - await testCoverage.togglePause("dummyPausableFunction()", true); - await expectRevert(testCoverage.dummyPausableFunction(), "unauthorized"); - }); - }); + describe("Testing Pausable contract", () => { + it("Pausable function runs if not paused", async () => { + testCoverage = await TestCoverage.new(); + await testCoverage.dummyPausableFunction(); + }); + it("Pausable function reverts if paused", async () => { + testCoverage = await TestCoverage.new(); + await testCoverage.togglePause("dummyPausableFunction()", true); + await expectRevert(testCoverage.dummyPausableFunction(), "unauthorized"); + }); + }); }); diff --git a/tests/TimelockTest.js b/tests/TimelockTest.js index 2ebe1535d..2daad5aa3 100644 --- a/tests/TimelockTest.js +++ b/tests/TimelockTest.js @@ -1,433 +1,492 @@ const { expect } = require("chai"); const BigNumber = require("bignumber.js"); -const { expectRevert, expectEvent, constants, BN, balance, time } = require("@openzeppelin/test-helpers"); +const { + expectRevert, + expectEvent, + constants, + BN, + balance, + time, +} = require("@openzeppelin/test-helpers"); const Timelock = artifacts.require("TimelockHarness"); -const { encodeParameters, etherUnsigned, setTime, keccak256, setNextBlockTimestamp, increaseTime } = require("./Utils/Ethereum"); +const { + encodeParameters, + etherUnsigned, + setTime, + keccak256, + setNextBlockTimestamp, + increaseTime, +} = require("./Utils/Ethereum"); const oneWeekInSeconds = etherUnsigned(7 * 24 * 60 * 60); const zero = etherUnsigned(0); const gracePeriod = oneWeekInSeconds.multipliedBy(2); contract("Timelock", (accounts) => { - let root, notAdmin, newAdmin; - let blockTimestamp; - let timelock; - let delay = oneWeekInSeconds; - let newDelay = delay.multipliedBy(2); - let target; - let value = zero; - let signature = "setDelay(uint256)"; - let data = encodeParameters(["uint256"], [newDelay.toFixed()]); - let revertData = encodeParameters(["uint256"], [etherUnsigned(60 * 60).toFixed()]); - let eta; - let queuedTxHash; - - beforeEach(async () => { - [root, notAdmin, newAdmin] = accounts; - timelock = await Timelock.new(root, delay); - - blockTimestamp = new BigNumber((await ethers.provider.getBlock("latest")).timestamp); - target = timelock.address; - eta = blockTimestamp.plus(delay); - - queuedTxHash = keccak256( - encodeParameters( - ["address", "uint256", "string", "bytes", "uint256"], - [target, value.toString(), signature, data, eta.toString()] - ) - ); - }); - - describe("constructor", () => { - it("sets address of admin", async () => { - let configuredAdmin = await timelock.admin.call(); - expect(configuredAdmin).to.be.equal(root); - }); - - it("sets delay", async () => { - let configuredDelay = await timelock.delay.call(); - expect(configuredDelay).to.be.bignumber.equal(delay.toString()); - }); - }); - - describe("setDelay", () => { - it("requires msg.sender to be Timelock", async () => { - await expectRevert(timelock.setDelay(delay, { from: root }), "Timelock::setDelay: Call must come from Timelock."); - }); - }); - - describe("setPendingAdmin", () => { - it("requires msg.sender to be Timelock", async () => { - await expectRevert( - timelock.setPendingAdmin(newAdmin, { from: root }), - "Timelock::setPendingAdmin: Call must come from Timelock." - ); - }); - }); - - describe("acceptAdmin", () => { - afterEach(async () => { - await timelock.harnessSetAdmin(root, { from: root }); - }); - - it("requires msg.sender to be pendingAdmin", async () => { - await expectRevert(timelock.acceptAdmin({ from: notAdmin }), "Timelock::acceptAdmin: Call must come from pendingAdmin."); - }); - - it("sets pendingAdmin to address 0 and changes admin", async () => { - await timelock.harnessSetPendingAdmin(newAdmin, { from: root }); - const pendingAdminBefore = await timelock.pendingAdmin.call(); - expect(pendingAdminBefore).to.be.equal(newAdmin); - - const result = await timelock.acceptAdmin({ from: newAdmin }); - const pendingAdminAfter = await timelock.pendingAdmin.call(); - expect(pendingAdminAfter).to.be.equal("0x0000000000000000000000000000000000000000"); - - const timelockAdmin = await timelock.admin.call(); - expect(timelockAdmin).to.be.equal(newAdmin); - - expectEvent(result, "NewAdmin", { newAdmin: newAdmin }); - }); - }); - - describe("queueTransaction", () => { - beforeEach(async () => { - const configuredDelay = await timelock.delay.call(); - delay = etherUnsigned(configuredDelay); - - blockTimestamp = new BigNumber((await ethers.provider.getBlock("latest")).timestamp); - setNextBlockTimestamp(blockTimestamp.plus(100).toNumber()); - eta = blockTimestamp.plus(delay).plus(100); - - queuedTxHash = keccak256( - encodeParameters( - ["address", "uint256", "string", "bytes", "uint256"], - [target, value.toString(), signature, data, eta.toString()] - ) - ); - }); - - it("requires admin to be msg.sender", async () => { - await expectRevert( - timelock.queueTransaction(target, value, signature, data, eta, { from: notAdmin }), - "Timelock::queueTransaction: Call must come from admin." - ); - }); - - it("requires eta to exceed delay", async () => { - const etaLessThanDelay = blockTimestamp.plus(delay).minus(1); - - await expectRevert( - timelock.queueTransaction(target, value, signature, data, etaLessThanDelay, { from: root }), - "Timelock::queueTransaction: Estimated execution block must satisfy delay." - ); - }); - - it("sets hash as true in queuedTransactions mapping", async () => { - const queueTransactionsHashValueBefore = await timelock.queuedTransactions.call(queuedTxHash); - expect(queueTransactionsHashValueBefore).to.be.equal(false); - - await timelock.queueTransaction(target, value, signature, data, eta, { from: root }); - - const queueTransactionsHashValueAfter = await timelock.queuedTransactions.call(queuedTxHash); - expect(queueTransactionsHashValueAfter).to.be.equal(true); - }); - - it("should emit QueueTransaction event", async () => { - const result = await timelock.queueTransaction(target, value, signature, data, eta, { - from: root, - }); - - expectEvent(result, "QueueTransaction", { - data: data, - signature: signature, - target: target, - eta: eta.toString(), - txHash: queuedTxHash, - value: value.toString(), - }); - }); - }); - - describe("cancelTransaction", () => { - beforeEach(async () => { - const configuredDelay = await timelock.delay.call(); - delay = etherUnsigned(configuredDelay); - blockTimestamp = new BigNumber((await ethers.provider.getBlock("latest")).timestamp); - setNextBlockTimestamp(blockTimestamp.plus(100).toNumber()); - eta = blockTimestamp.plus(delay).plus(100); - queuedTxHash = keccak256( - encodeParameters( - ["address", "uint256", "string", "bytes", "uint256"], - [target, value.toString(), signature, data, eta.toString()] - ) - ); - await timelock.queueTransaction(target, value, signature, data, eta, { from: root }); - }); - - it("requires admin to be msg.sender", async () => { - await expectRevert( - timelock.cancelTransaction(target, value, signature, data, eta, { from: notAdmin }), - "Timelock::cancelTransaction: Call must come from admin." - ); - }); - - it("sets hash from true to false in queuedTransactions mapping", async () => { - const queueTransactionsHashValueBefore = await timelock.queuedTransactions.call(queuedTxHash); - expect(queueTransactionsHashValueBefore).to.be.equal(true); - - await timelock.cancelTransaction(target, value, signature, data, eta, { from: root }); - - const queueTransactionsHashValueAfter = await timelock.queuedTransactions.call(queuedTxHash); - expect(queueTransactionsHashValueAfter).to.be.equal(false); - }); - - it("should emit CancelTransaction event", async () => { - const result = await timelock.cancelTransaction(target, value, signature, data, eta, { - from: root, - }); - - expectEvent(result, "CancelTransaction", { - data: data, - signature: signature, - target: target, - eta: eta.toString(), - txHash: queuedTxHash, - value: value.toString(), - }); - }); - }); - - describe("queue and cancel empty", () => { - beforeEach(async () => { - const configuredDelay = await timelock.delay.call(); - delay = etherUnsigned(configuredDelay); - blockTimestamp = new BigNumber((await ethers.provider.getBlock("latest")).timestamp); - setNextBlockTimestamp(blockTimestamp.plus(100).toNumber()); - eta = blockTimestamp.plus(delay).plus(100); - }); - - it("can queue and cancel an empty signature and data", async () => { - const txHash = keccak256( - encodeParameters(["address", "uint256", "string", "bytes", "uint256"], [target, value.toString(), "", "0x", eta.toString()]) - ); - expect(await timelock.queuedTransactions.call(txHash)).to.be.equal(false); - await timelock.queueTransaction(target, value, "", "0x", eta, { from: root }); - expect(await timelock.queuedTransactions.call(txHash)).to.be.equal(true); - await timelock.cancelTransaction(target, value, "", "0x", eta, { from: root }); - expect(await timelock.queuedTransactions(txHash)).to.be.equal(false); - }); - }); - - describe("executeTransaction (setDelay)", () => { - beforeEach(async () => { - const configuredDelay = await timelock.delay.call(); - delay = etherUnsigned(configuredDelay); - blockTimestamp = new BigNumber((await ethers.provider.getBlock("latest")).timestamp); - - setNextBlockTimestamp(blockTimestamp.plus(100).toNumber()); - eta = blockTimestamp.plus(delay).plus(100); - - // Queue transaction that will succeed - await timelock.queueTransaction(target, value, signature, data, eta, { - from: root, - }); - - blockTimestamp = new BigNumber((await ethers.provider.getBlock("latest")).timestamp); - queuedTxHash = keccak256( - encodeParameters( - ["address", "uint256", "string", "bytes", "uint256"], - [target, value.toString(), signature, data, eta.toString()] - ) - ); - }); - - it("requires admin to be msg.sender", async () => { - await expectRevert( - timelock.executeTransaction(target, value, signature, data, eta, { from: notAdmin }), - "Timelock::executeTransaction: Call must come from admin." - ); - }); - - it("requires transaction to be queued", async () => { - const differentEta = eta.plus(1); - await expectRevert( - timelock.executeTransaction(target, value, signature, data, differentEta, { from: root }), - "Timelock::executeTransaction: Transaction hasn't been queued." - ); - }); - - it("requires timestamp to be greater than or equal to eta", async () => { - await expectRevert( - timelock.executeTransaction(target, value, signature, data, eta, { from: root }), - "Timelock::executeTransaction: Transaction hasn't surpassed time lock." - ); - }); - - it("requires timestamp to be less than eta plus gracePeriod", async () => { - await setNextBlockTimestamp(blockTimestamp.plus(delay).plus(gracePeriod).plus(1).toNumber()); - await expectRevert( - timelock.executeTransaction(target, value, signature, data, eta, { from: root }), - "Timelock::executeTransaction: Transaction is stale." - ); - }); - - it("requires target.call transaction to succeed", async () => { - setNextBlockTimestamp(blockTimestamp.plus(100).toNumber()); - eta = blockTimestamp.plus(delay).plus(100); - await timelock.queueTransaction(target, value, signature, revertData, eta, { - from: root, - }); - - //await setTime(eta.toNumber()); - //blockTimestamp = new BigNumber((await ethers.provider.getBlock("latest")).timestamp); - setNextBlockTimestamp(eta.toNumber()); - //eta = blockTimestamp.plus(delay).plus(100); - await expectRevert( - timelock.executeTransaction(target, value, signature, revertData, eta, { from: root }), - "Timelock::executeTransaction: Timelock::setDelay: Delay must exceed minimum delay." - ); - }); - - it("sets hash from true to false in queuedTransactions mapping, updates delay, and emits ExecuteTransaction event", async () => { - const configuredDelayBefore = await timelock.delay.call(); - expect(configuredDelayBefore.toString()).to.be.equal(delay.toString()); - - const queueTransactionsHashValueBefore = await timelock.queuedTransactions.call(queuedTxHash); - expect(queueTransactionsHashValueBefore).to.be.equal(true); - - const newBlockTimestamp = blockTimestamp.plus(delay).plus(1); - - //await setTime(newBlockTimestamp.toNumber()); - await time.increaseTo(newBlockTimestamp.toNumber()); - - const result = await timelock.executeTransaction(target, value, signature, data, eta, { - from: root, - }); - - const queueTransactionsHashValueAfter = await timelock.queuedTransactions.call(queuedTxHash); - expect(queueTransactionsHashValueAfter).to.be.equal(false); - - const configuredDelayAfter = await timelock.delay.call(); - expect(configuredDelayAfter.toString()).to.be.equal(newDelay.toString()); - - expectEvent(result, "ExecuteTransaction", { - data: data, - signature: signature, - target: target, - eta: eta.toString(), - txHash: queuedTxHash, - value: value.toString(), - }); - - expectEvent(result, "NewDelay", { - newDelay: newDelay.toString(), - }); - }); - }); - - describe("executeTransaction (setPendingAdmin)", () => { - beforeEach(async () => { - const configuredDelay = await timelock.delay.call(); - delay = etherUnsigned(configuredDelay); - signature = "setPendingAdmin(address)"; - data = encodeParameters(["address"], [newAdmin]); - //blockTimestamp = etherUnsigned(100); - //await setTime(blockTimestamp.toNumber()); - blockTimestamp = new BigNumber((await ethers.provider.getBlock("latest")).timestamp); - setNextBlockTimestamp(blockTimestamp.plus(100).toNumber()); - eta = blockTimestamp.plus(delay).plus(100); - - queuedTxHash = keccak256( - encodeParameters( - ["address", "uint256", "string", "bytes", "uint256"], - [target, value.toString(), signature, data, eta.toString()] - ) - ); - - await timelock.queueTransaction(target, value, signature, data, eta, { - from: root, - }); - - blockTimestamp = new BigNumber((await ethers.provider.getBlock("latest")).timestamp); - }); - - it("requires admin to be msg.sender", async () => { - await expectRevert( - timelock.executeTransaction(target, value, signature, data, eta, { from: notAdmin }), - "Timelock::executeTransaction: Call must come from admin." - ); - }); - - it("requires transaction to be queued", async () => { - const differentEta = eta.plus(1); - await expectRevert( - timelock.executeTransaction(target, value, signature, data, differentEta, { from: root }), - "Timelock::executeTransaction: Transaction hasn't been queued." - ); - }); - - it("requires timestamp to be greater than or equal to eta", async () => { - await expectRevert( - timelock.executeTransaction(target, value, signature, data, eta, { from: root }), - "Timelock::executeTransaction: Transaction hasn't surpassed time lock." - ); - }); - - it("requires timestamp to be less than eta plus gracePeriod", async () => { - //await setTime(blockTimestamp.plus(delay).plus(gracePeriod).plus(1).toNumber()); - //await mineBlock(); - //blockTimestamp = new BigNumber((await ethers.provider.getBlock("latest")).timestamp); - //blockTimestamp = new BigNumber(await time.latest()); - //eta = blockTimestamp.plus(delay); - await setNextBlockTimestamp(blockTimestamp.plus(delay).plus(gracePeriod).plus(1).toNumber()); - - await expectRevert( - timelock.executeTransaction(target, value, signature, data, eta, { from: root }), - "Timelock::executeTransaction: Transaction is stale." - ); - }); - - it("sets hash from true to false in queuedTransactions mapping, updates admin, and emits ExecuteTransaction event", async () => { - const configuredPendingAdminBefore = await timelock.pendingAdmin.call(); - expect(configuredPendingAdminBefore).to.be.equal("0x0000000000000000000000000000000000000000"); - - const queueTransactionsHashValueBefore = await timelock.queuedTransactions.call(queuedTxHash); - expect(queueTransactionsHashValueBefore).to.be.equal(true); - - const newBlockTimestamp = blockTimestamp.plus(delay).plus(1); - //await setTime(newBlockTimestamp.toNumber()); - - //await time.advanceBlock(); - //blockTimestamp = new BigNumber(await time.latest()); - //eta = blockTimestamp.plus(delay); - //await setNextBlockTimestamp(blockTimestamp.plus(delay).plus(gracePeriod).plus(1).toNumber()); - await time.increaseTo(newBlockTimestamp.toNumber()); - - const result = await timelock.executeTransaction(target, value, signature, data, eta, { - from: root, - }); - - const queueTransactionsHashValueAfter = await timelock.queuedTransactions.call(queuedTxHash); - expect(queueTransactionsHashValueAfter).to.be.equal(false); - - const configuredPendingAdminAfter = await timelock.pendingAdmin.call(); - expect(configuredPendingAdminAfter).to.be.equal(newAdmin); - - expectEvent(result, "ExecuteTransaction", { - data: data, - signature: signature, - target: target, - eta: eta.toString(), - txHash: queuedTxHash, - value: value.toString(), - }); - - expectEvent(result, "NewPendingAdmin", { - newPendingAdmin: newAdmin, - }); - }); - }); + let root, notAdmin, newAdmin; + let blockTimestamp; + let timelock; + let delay = oneWeekInSeconds; + let newDelay = delay.multipliedBy(2); + let target; + let value = zero; + let signature = "setDelay(uint256)"; + let data = encodeParameters(["uint256"], [newDelay.toFixed()]); + let revertData = encodeParameters(["uint256"], [etherUnsigned(60 * 60).toFixed()]); + let eta; + let queuedTxHash; + + beforeEach(async () => { + [root, notAdmin, newAdmin] = accounts; + timelock = await Timelock.new(root, delay); + + blockTimestamp = new BigNumber((await ethers.provider.getBlock("latest")).timestamp); + target = timelock.address; + eta = blockTimestamp.plus(delay); + + queuedTxHash = keccak256( + encodeParameters( + ["address", "uint256", "string", "bytes", "uint256"], + [target, value.toString(), signature, data, eta.toString()] + ) + ); + }); + + describe("constructor", () => { + it("sets address of admin", async () => { + let configuredAdmin = await timelock.admin.call(); + expect(configuredAdmin).to.be.equal(root); + }); + + it("sets delay", async () => { + let configuredDelay = await timelock.delay.call(); + expect(configuredDelay).to.be.bignumber.equal(delay.toString()); + }); + }); + + describe("setDelay", () => { + it("requires msg.sender to be Timelock", async () => { + await expectRevert( + timelock.setDelay(delay, { from: root }), + "Timelock::setDelay: Call must come from Timelock." + ); + }); + }); + + describe("setPendingAdmin", () => { + it("requires msg.sender to be Timelock", async () => { + await expectRevert( + timelock.setPendingAdmin(newAdmin, { from: root }), + "Timelock::setPendingAdmin: Call must come from Timelock." + ); + }); + }); + + describe("acceptAdmin", () => { + afterEach(async () => { + await timelock.harnessSetAdmin(root, { from: root }); + }); + + it("requires msg.sender to be pendingAdmin", async () => { + await expectRevert( + timelock.acceptAdmin({ from: notAdmin }), + "Timelock::acceptAdmin: Call must come from pendingAdmin." + ); + }); + + it("sets pendingAdmin to address 0 and changes admin", async () => { + await timelock.harnessSetPendingAdmin(newAdmin, { from: root }); + const pendingAdminBefore = await timelock.pendingAdmin.call(); + expect(pendingAdminBefore).to.be.equal(newAdmin); + + const result = await timelock.acceptAdmin({ from: newAdmin }); + const pendingAdminAfter = await timelock.pendingAdmin.call(); + expect(pendingAdminAfter).to.be.equal("0x0000000000000000000000000000000000000000"); + + const timelockAdmin = await timelock.admin.call(); + expect(timelockAdmin).to.be.equal(newAdmin); + + expectEvent(result, "NewAdmin", { newAdmin: newAdmin }); + }); + }); + + describe("queueTransaction", () => { + beforeEach(async () => { + const configuredDelay = await timelock.delay.call(); + delay = etherUnsigned(configuredDelay); + + blockTimestamp = new BigNumber((await ethers.provider.getBlock("latest")).timestamp); + setNextBlockTimestamp(blockTimestamp.plus(100).toNumber()); + eta = blockTimestamp.plus(delay).plus(100); + + queuedTxHash = keccak256( + encodeParameters( + ["address", "uint256", "string", "bytes", "uint256"], + [target, value.toString(), signature, data, eta.toString()] + ) + ); + }); + + it("requires admin to be msg.sender", async () => { + await expectRevert( + timelock.queueTransaction(target, value, signature, data, eta, { from: notAdmin }), + "Timelock::queueTransaction: Call must come from admin." + ); + }); + + it("requires eta to exceed delay", async () => { + const etaLessThanDelay = blockTimestamp.plus(delay).minus(1); + + await expectRevert( + timelock.queueTransaction(target, value, signature, data, etaLessThanDelay, { + from: root, + }), + "Timelock::queueTransaction: Estimated execution block must satisfy delay." + ); + }); + + it("sets hash as true in queuedTransactions mapping", async () => { + const queueTransactionsHashValueBefore = await timelock.queuedTransactions.call( + queuedTxHash + ); + expect(queueTransactionsHashValueBefore).to.be.equal(false); + + await timelock.queueTransaction(target, value, signature, data, eta, { from: root }); + + const queueTransactionsHashValueAfter = await timelock.queuedTransactions.call( + queuedTxHash + ); + expect(queueTransactionsHashValueAfter).to.be.equal(true); + }); + + it("should emit QueueTransaction event", async () => { + const result = await timelock.queueTransaction(target, value, signature, data, eta, { + from: root, + }); + + expectEvent(result, "QueueTransaction", { + data: data, + signature: signature, + target: target, + eta: eta.toString(), + txHash: queuedTxHash, + value: value.toString(), + }); + }); + }); + + describe("cancelTransaction", () => { + beforeEach(async () => { + const configuredDelay = await timelock.delay.call(); + delay = etherUnsigned(configuredDelay); + blockTimestamp = new BigNumber((await ethers.provider.getBlock("latest")).timestamp); + setNextBlockTimestamp(blockTimestamp.plus(100).toNumber()); + eta = blockTimestamp.plus(delay).plus(100); + queuedTxHash = keccak256( + encodeParameters( + ["address", "uint256", "string", "bytes", "uint256"], + [target, value.toString(), signature, data, eta.toString()] + ) + ); + await timelock.queueTransaction(target, value, signature, data, eta, { from: root }); + }); + + it("requires admin to be msg.sender", async () => { + await expectRevert( + timelock.cancelTransaction(target, value, signature, data, eta, { + from: notAdmin, + }), + "Timelock::cancelTransaction: Call must come from admin." + ); + }); + + it("sets hash from true to false in queuedTransactions mapping", async () => { + const queueTransactionsHashValueBefore = await timelock.queuedTransactions.call( + queuedTxHash + ); + expect(queueTransactionsHashValueBefore).to.be.equal(true); + + await timelock.cancelTransaction(target, value, signature, data, eta, { from: root }); + + const queueTransactionsHashValueAfter = await timelock.queuedTransactions.call( + queuedTxHash + ); + expect(queueTransactionsHashValueAfter).to.be.equal(false); + }); + + it("should emit CancelTransaction event", async () => { + const result = await timelock.cancelTransaction(target, value, signature, data, eta, { + from: root, + }); + + expectEvent(result, "CancelTransaction", { + data: data, + signature: signature, + target: target, + eta: eta.toString(), + txHash: queuedTxHash, + value: value.toString(), + }); + }); + }); + + describe("queue and cancel empty", () => { + beforeEach(async () => { + const configuredDelay = await timelock.delay.call(); + delay = etherUnsigned(configuredDelay); + blockTimestamp = new BigNumber((await ethers.provider.getBlock("latest")).timestamp); + setNextBlockTimestamp(blockTimestamp.plus(100).toNumber()); + eta = blockTimestamp.plus(delay).plus(100); + }); + + it("can queue and cancel an empty signature and data", async () => { + const txHash = keccak256( + encodeParameters( + ["address", "uint256", "string", "bytes", "uint256"], + [target, value.toString(), "", "0x", eta.toString()] + ) + ); + expect(await timelock.queuedTransactions.call(txHash)).to.be.equal(false); + await timelock.queueTransaction(target, value, "", "0x", eta, { from: root }); + expect(await timelock.queuedTransactions.call(txHash)).to.be.equal(true); + await timelock.cancelTransaction(target, value, "", "0x", eta, { from: root }); + expect(await timelock.queuedTransactions(txHash)).to.be.equal(false); + }); + }); + + describe("executeTransaction (setDelay)", () => { + beforeEach(async () => { + const configuredDelay = await timelock.delay.call(); + delay = etherUnsigned(configuredDelay); + blockTimestamp = new BigNumber((await ethers.provider.getBlock("latest")).timestamp); + + setNextBlockTimestamp(blockTimestamp.plus(100).toNumber()); + eta = blockTimestamp.plus(delay).plus(100); + + // Queue transaction that will succeed + await timelock.queueTransaction(target, value, signature, data, eta, { + from: root, + }); + + blockTimestamp = new BigNumber((await ethers.provider.getBlock("latest")).timestamp); + queuedTxHash = keccak256( + encodeParameters( + ["address", "uint256", "string", "bytes", "uint256"], + [target, value.toString(), signature, data, eta.toString()] + ) + ); + }); + + it("requires admin to be msg.sender", async () => { + await expectRevert( + timelock.executeTransaction(target, value, signature, data, eta, { + from: notAdmin, + }), + "Timelock::executeTransaction: Call must come from admin." + ); + }); + + it("requires transaction to be queued", async () => { + const differentEta = eta.plus(1); + await expectRevert( + timelock.executeTransaction(target, value, signature, data, differentEta, { + from: root, + }), + "Timelock::executeTransaction: Transaction hasn't been queued." + ); + }); + + it("requires timestamp to be greater than or equal to eta", async () => { + await expectRevert( + timelock.executeTransaction(target, value, signature, data, eta, { from: root }), + "Timelock::executeTransaction: Transaction hasn't surpassed time lock." + ); + }); + + it("requires timestamp to be less than eta plus gracePeriod", async () => { + await setNextBlockTimestamp( + blockTimestamp.plus(delay).plus(gracePeriod).plus(1).toNumber() + ); + await expectRevert( + timelock.executeTransaction(target, value, signature, data, eta, { from: root }), + "Timelock::executeTransaction: Transaction is stale." + ); + }); + + it("requires target.call transaction to succeed", async () => { + setNextBlockTimestamp(blockTimestamp.plus(100).toNumber()); + eta = blockTimestamp.plus(delay).plus(100); + await timelock.queueTransaction(target, value, signature, revertData, eta, { + from: root, + }); + + //await setTime(eta.toNumber()); + //blockTimestamp = new BigNumber((await ethers.provider.getBlock("latest")).timestamp); + setNextBlockTimestamp(eta.toNumber()); + //eta = blockTimestamp.plus(delay).plus(100); + await expectRevert( + timelock.executeTransaction(target, value, signature, revertData, eta, { + from: root, + }), + "Timelock::executeTransaction: Timelock::setDelay: Delay must exceed minimum delay." + ); + }); + + it("sets hash from true to false in queuedTransactions mapping, updates delay, and emits ExecuteTransaction event", async () => { + const configuredDelayBefore = await timelock.delay.call(); + expect(configuredDelayBefore.toString()).to.be.equal(delay.toString()); + + const queueTransactionsHashValueBefore = await timelock.queuedTransactions.call( + queuedTxHash + ); + expect(queueTransactionsHashValueBefore).to.be.equal(true); + + const newBlockTimestamp = blockTimestamp.plus(delay).plus(1); + + //await setTime(newBlockTimestamp.toNumber()); + await time.increaseTo(newBlockTimestamp.toNumber()); + + const result = await timelock.executeTransaction(target, value, signature, data, eta, { + from: root, + }); + + const queueTransactionsHashValueAfter = await timelock.queuedTransactions.call( + queuedTxHash + ); + expect(queueTransactionsHashValueAfter).to.be.equal(false); + + const configuredDelayAfter = await timelock.delay.call(); + expect(configuredDelayAfter.toString()).to.be.equal(newDelay.toString()); + + expectEvent(result, "ExecuteTransaction", { + data: data, + signature: signature, + target: target, + eta: eta.toString(), + txHash: queuedTxHash, + value: value.toString(), + }); + + expectEvent(result, "NewDelay", { + newDelay: newDelay.toString(), + }); + }); + }); + + describe("executeTransaction (setPendingAdmin)", () => { + beforeEach(async () => { + const configuredDelay = await timelock.delay.call(); + delay = etherUnsigned(configuredDelay); + signature = "setPendingAdmin(address)"; + data = encodeParameters(["address"], [newAdmin]); + //blockTimestamp = etherUnsigned(100); + //await setTime(blockTimestamp.toNumber()); + blockTimestamp = new BigNumber((await ethers.provider.getBlock("latest")).timestamp); + setNextBlockTimestamp(blockTimestamp.plus(100).toNumber()); + eta = blockTimestamp.plus(delay).plus(100); + + queuedTxHash = keccak256( + encodeParameters( + ["address", "uint256", "string", "bytes", "uint256"], + [target, value.toString(), signature, data, eta.toString()] + ) + ); + + await timelock.queueTransaction(target, value, signature, data, eta, { + from: root, + }); + + blockTimestamp = new BigNumber((await ethers.provider.getBlock("latest")).timestamp); + }); + + it("requires admin to be msg.sender", async () => { + await expectRevert( + timelock.executeTransaction(target, value, signature, data, eta, { + from: notAdmin, + }), + "Timelock::executeTransaction: Call must come from admin." + ); + }); + + it("requires transaction to be queued", async () => { + const differentEta = eta.plus(1); + await expectRevert( + timelock.executeTransaction(target, value, signature, data, differentEta, { + from: root, + }), + "Timelock::executeTransaction: Transaction hasn't been queued." + ); + }); + + it("requires timestamp to be greater than or equal to eta", async () => { + await expectRevert( + timelock.executeTransaction(target, value, signature, data, eta, { from: root }), + "Timelock::executeTransaction: Transaction hasn't surpassed time lock." + ); + }); + + it("requires timestamp to be less than eta plus gracePeriod", async () => { + //await setTime(blockTimestamp.plus(delay).plus(gracePeriod).plus(1).toNumber()); + //await mineBlock(); + //blockTimestamp = new BigNumber((await ethers.provider.getBlock("latest")).timestamp); + //blockTimestamp = new BigNumber(await time.latest()); + //eta = blockTimestamp.plus(delay); + await setNextBlockTimestamp( + blockTimestamp.plus(delay).plus(gracePeriod).plus(1).toNumber() + ); + + await expectRevert( + timelock.executeTransaction(target, value, signature, data, eta, { from: root }), + "Timelock::executeTransaction: Transaction is stale." + ); + }); + + it("sets hash from true to false in queuedTransactions mapping, updates admin, and emits ExecuteTransaction event", async () => { + const configuredPendingAdminBefore = await timelock.pendingAdmin.call(); + expect(configuredPendingAdminBefore).to.be.equal( + "0x0000000000000000000000000000000000000000" + ); + + const queueTransactionsHashValueBefore = await timelock.queuedTransactions.call( + queuedTxHash + ); + expect(queueTransactionsHashValueBefore).to.be.equal(true); + + const newBlockTimestamp = blockTimestamp.plus(delay).plus(1); + //await setTime(newBlockTimestamp.toNumber()); + + //await time.advanceBlock(); + //blockTimestamp = new BigNumber(await time.latest()); + //eta = blockTimestamp.plus(delay); + //await setNextBlockTimestamp(blockTimestamp.plus(delay).plus(gracePeriod).plus(1).toNumber()); + await time.increaseTo(newBlockTimestamp.toNumber()); + + const result = await timelock.executeTransaction(target, value, signature, data, eta, { + from: root, + }); + + const queueTransactionsHashValueAfter = await timelock.queuedTransactions.call( + queuedTxHash + ); + expect(queueTransactionsHashValueAfter).to.be.equal(false); + + const configuredPendingAdminAfter = await timelock.pendingAdmin.call(); + expect(configuredPendingAdminAfter).to.be.equal(newAdmin); + + expectEvent(result, "ExecuteTransaction", { + data: data, + signature: signature, + target: target, + eta: eta.toString(), + txHash: queuedTxHash, + value: value.toString(), + }); + + expectEvent(result, "NewPendingAdmin", { + newPendingAdmin: newAdmin, + }); + }); + }); }); diff --git a/tests/Utils/EIP712.js b/tests/Utils/EIP712.js index e0fbe0394..a2da22bda 100644 --- a/tests/Utils/EIP712.js +++ b/tests/Utils/EIP712.js @@ -4,111 +4,118 @@ const abi = require("ethereumjs-abi"); // Recursively finds all the dependencies of a type function dependencies(primaryType, found = [], types = {}) { - if (found.includes(primaryType)) { - return found; - } - if (types[primaryType] === undefined) { - return found; - } - found.push(primaryType); - for (let field of types[primaryType]) { - for (let dep of dependencies(field.type, found)) { - if (!found.includes(dep)) { - found.push(dep); - } - } - } - return found; + if (found.includes(primaryType)) { + return found; + } + if (types[primaryType] === undefined) { + return found; + } + found.push(primaryType); + for (let field of types[primaryType]) { + for (let dep of dependencies(field.type, found)) { + if (!found.includes(dep)) { + found.push(dep); + } + } + } + return found; } function encodeType(primaryType, types = {}) { - // Get dependencies primary first, then alphabetical - let deps = dependencies(primaryType); - deps = deps.filter((t) => t != primaryType); - deps = [primaryType].concat(deps.sort()); + // Get dependencies primary first, then alphabetical + let deps = dependencies(primaryType); + deps = deps.filter((t) => t != primaryType); + deps = [primaryType].concat(deps.sort()); - // Format as a string with fields - let result = ""; - for (let type of deps) { - if (!types[type]) throw new Error(`Type '${type}' not defined in types (${JSON.stringify(types)})`); - result += `${type}(${types[type].map(({ name, type }) => `${type} ${name}`).join(",")})`; - } - return result; + // Format as a string with fields + let result = ""; + for (let type of deps) { + if (!types[type]) + throw new Error(`Type '${type}' not defined in types (${JSON.stringify(types)})`); + result += `${type}(${types[type].map(({ name, type }) => `${type} ${name}`).join(",")})`; + } + return result; } function typeHash(primaryType, types = {}) { - return ethUtil.keccak256(encodeType(primaryType, types)); + return ethUtil.keccak256(encodeType(primaryType, types)); } function encodeData(primaryType, data, types = {}) { - let encTypes = []; - let encValues = []; + let encTypes = []; + let encValues = []; - // Add typehash - encTypes.push("bytes32"); - encValues.push(typeHash(primaryType, types)); + // Add typehash + encTypes.push("bytes32"); + encValues.push(typeHash(primaryType, types)); - // Add field contents - for (let field of types[primaryType]) { - let value = data[field.name]; - if (field.type == "string" || field.type == "bytes") { - encTypes.push("bytes32"); - value = ethUtil.keccak256(value); - encValues.push(value); - } else if (types[field.type] !== undefined) { - encTypes.push("bytes32"); - value = ethUtil.keccak256(encodeData(field.type, value, types)); - encValues.push(value); - } else if (field.type.lastIndexOf("]") === field.type.length - 1) { - throw "TODO: Arrays currently unimplemented in encodeData"; - } else { - encTypes.push(field.type); - encValues.push(value); - } - } + // Add field contents + for (let field of types[primaryType]) { + let value = data[field.name]; + if (field.type == "string" || field.type == "bytes") { + encTypes.push("bytes32"); + value = ethUtil.keccak256(value); + encValues.push(value); + } else if (types[field.type] !== undefined) { + encTypes.push("bytes32"); + value = ethUtil.keccak256(encodeData(field.type, value, types)); + encValues.push(value); + } else if (field.type.lastIndexOf("]") === field.type.length - 1) { + throw "TODO: Arrays currently unimplemented in encodeData"; + } else { + encTypes.push(field.type); + encValues.push(value); + } + } - return abi.rawEncode(encTypes, encValues); + return abi.rawEncode(encTypes, encValues); } function domainSeparator(domain) { - const types = { - EIP712Domain: [ - { name: "name", type: "string" }, - { name: "version", type: "string" }, - { name: "chainId", type: "uint256" }, - { name: "verifyingContract", type: "address" }, - { name: "salt", type: "bytes32" }, - ].filter((a) => domain[a.name]), - }; - return ethUtil.keccak256(encodeData("EIP712Domain", domain, types)); + const types = { + EIP712Domain: [ + { name: "name", type: "string" }, + { name: "version", type: "string" }, + { name: "chainId", type: "uint256" }, + { name: "verifyingContract", type: "address" }, + { name: "salt", type: "bytes32" }, + ].filter((a) => domain[a.name]), + }; + return ethUtil.keccak256(encodeData("EIP712Domain", domain, types)); } function structHash(primaryType, data, types = {}) { - return ethUtil.keccak256(encodeData(primaryType, data, types)); + return ethUtil.keccak256(encodeData(primaryType, data, types)); } function digestToSign(domain, primaryType, message, types = {}) { - return ethUtil.keccak256(Buffer.concat([Buffer.from("1901", "hex"), domainSeparator(domain), structHash(primaryType, message, types)])); + return ethUtil.keccak256( + Buffer.concat([ + Buffer.from("1901", "hex"), + domainSeparator(domain), + structHash(primaryType, message, types), + ]) + ); } function sign(domain, primaryType, message, types = {}, privateKey) { - const digest = digestToSign(domain, primaryType, message, types); - return { - domain, - primaryType, - message, - types, - digest, - ...ethUtil.ecsign(digest, ethUtil.toBuffer(privateKey)), - }; + const digest = digestToSign(domain, primaryType, message, types); + return { + domain, + primaryType, + message, + types, + digest, + ...ethUtil.ecsign(digest, ethUtil.toBuffer(privateKey)), + }; } module.exports = { - encodeType, - typeHash, - encodeData, - domainSeparator, - structHash, - digestToSign, - sign, + encodeType, + typeHash, + encodeData, + domainSeparator, + structHash, + digestToSign, + sign, }; diff --git a/tests/Utils/EIP712Ethers.js b/tests/Utils/EIP712Ethers.js index c664c8404..0775c6df0 100644 --- a/tests/Utils/EIP712Ethers.js +++ b/tests/Utils/EIP712Ethers.js @@ -3,137 +3,147 @@ const ethers = require("ethers"); function abiRawEncode(encTypes, encValues) { - const hexStr = ethers.utils.defaultAbiCoder.encode(encTypes, encValues); - return Buffer.from(hexStr.slice(2, hexStr.length), "hex"); + const hexStr = ethers.utils.defaultAbiCoder.encode(encTypes, encValues); + return Buffer.from(hexStr.slice(2, hexStr.length), "hex"); } function keccak256(arg) { - const hexStr = ethers.utils.keccak256(arg); - return Buffer.from(hexStr.slice(2, hexStr.length), "hex"); + const hexStr = ethers.utils.keccak256(arg); + return Buffer.from(hexStr.slice(2, hexStr.length), "hex"); } // Recursively finds all the dependencies of a type function dependencies(primaryType, found = [], types = {}) { - if (found.includes(primaryType)) { - return found; - } - if (types[primaryType] === undefined) { - return found; - } - found.push(primaryType); - for (let field of types[primaryType]) { - for (let dep of dependencies(field.type, found)) { - if (!found.includes(dep)) { - found.push(dep); - } - } - } - return found; + if (found.includes(primaryType)) { + return found; + } + if (types[primaryType] === undefined) { + return found; + } + found.push(primaryType); + for (let field of types[primaryType]) { + for (let dep of dependencies(field.type, found)) { + if (!found.includes(dep)) { + found.push(dep); + } + } + } + return found; } function encodeType(primaryType, types = {}) { - // Get dependencies primary first, then alphabetical - let deps = dependencies(primaryType); - deps = deps.filter((t) => t != primaryType); - deps = [primaryType].concat(deps.sort()); - - // Format as a string with fields - let result = ""; - for (let type of deps) { - if (!types[type]) throw new Error(`Type '${type}' not defined in types (${JSON.stringify(types)})`); - result += `${type}(${types[type].map(({ name, type }) => `${type} ${name}`).join(",")})`; - } - return result; + // Get dependencies primary first, then alphabetical + let deps = dependencies(primaryType); + deps = deps.filter((t) => t != primaryType); + deps = [primaryType].concat(deps.sort()); + + // Format as a string with fields + let result = ""; + for (let type of deps) { + if (!types[type]) + throw new Error(`Type '${type}' not defined in types (${JSON.stringify(types)})`); + result += `${type}(${types[type].map(({ name, type }) => `${type} ${name}`).join(",")})`; + } + return result; } function typeHash(primaryType, types = {}) { - return keccak256(Buffer.from(encodeType(primaryType, types))); + return keccak256(Buffer.from(encodeType(primaryType, types))); } function encodeData(primaryType, data, types = {}) { - let encTypes = []; - let encValues = []; - - // Add typehash - encTypes.push("bytes32"); - encValues.push(typeHash(primaryType, types)); - - // Add field contents - for (let field of types[primaryType]) { - let value = data[field.name]; - if (field.type == "string" || field.type == "bytes") { - encTypes.push("bytes32"); - value = keccak256(Buffer.from(value)); - encValues.push(value); - } else if (types[field.type] !== undefined) { - encTypes.push("bytes32"); - value = keccak256(encodeData(field.type, value, types)); - encValues.push(value); - } else if (field.type.lastIndexOf("]") === field.type.length - 1) { - throw "TODO: Arrays currently unimplemented in encodeData"; - } else { - encTypes.push(field.type); - encValues.push(value); - } - } - - return abiRawEncode(encTypes, encValues); + let encTypes = []; + let encValues = []; + + // Add typehash + encTypes.push("bytes32"); + encValues.push(typeHash(primaryType, types)); + + // Add field contents + for (let field of types[primaryType]) { + let value = data[field.name]; + if (field.type == "string" || field.type == "bytes") { + encTypes.push("bytes32"); + value = keccak256(Buffer.from(value)); + encValues.push(value); + } else if (types[field.type] !== undefined) { + encTypes.push("bytes32"); + value = keccak256(encodeData(field.type, value, types)); + encValues.push(value); + } else if (field.type.lastIndexOf("]") === field.type.length - 1) { + throw "TODO: Arrays currently unimplemented in encodeData"; + } else { + encTypes.push(field.type); + encValues.push(value); + } + } + + return abiRawEncode(encTypes, encValues); } function domainSeparator(domain) { - const types = { - EIP712Domain: [ - { name: "name", type: "string" }, - { name: "version", type: "string" }, - { name: "chainId", type: "uint256" }, - { name: "verifyingContract", type: "address" }, - { name: "salt", type: "bytes32" }, - ].filter((a) => domain[a.name]), - }; - return keccak256(encodeData("EIP712Domain", domain, types)); + const types = { + EIP712Domain: [ + { name: "name", type: "string" }, + { name: "version", type: "string" }, + { name: "chainId", type: "uint256" }, + { name: "verifyingContract", type: "address" }, + { name: "salt", type: "bytes32" }, + ].filter((a) => domain[a.name]), + }; + return keccak256(encodeData("EIP712Domain", domain, types)); } function structHash(primaryType, data, types = {}) { - return keccak256(encodeData(primaryType, data, types)); + return keccak256(encodeData(primaryType, data, types)); } function digestToSign(domain, primaryType, message, types = {}) { - return keccak256(Buffer.concat([Buffer.from("1901", "hex"), domainSeparator(domain), structHash(primaryType, message, types)])); + return keccak256( + Buffer.concat([ + Buffer.from("1901", "hex"), + domainSeparator(domain), + structHash(primaryType, message, types), + ]) + ); } async function sign(domain, primaryType, message, types = {}, signer) { - let signature; - - try { - if (signer._signingKey) { - const digest = digestToSign(domain, primaryType, message, types); - signature = signer._signingKey().signDigest(digest); - signature.v = "0x" + signature.v.toString(16); - } else { - const address = await signer.getAddress(); - const msgParams = JSON.stringify({ domain, primaryType, message, types }); - - signature = await signer.provider.jsonRpcFetchFunc("eth_signTypedData_v4", [address, msgParams]); - - const r = "0x" + signature.substring(2).substring(0, 64); - const s = "0x" + signature.substring(2).substring(64, 128); - const v = "0x" + signature.substring(2).substring(128, 130); - - signature = { r, s, v }; - } - } catch (e) { - throw new Error(e); - } - - return signature; + let signature; + + try { + if (signer._signingKey) { + const digest = digestToSign(domain, primaryType, message, types); + signature = signer._signingKey().signDigest(digest); + signature.v = "0x" + signature.v.toString(16); + } else { + const address = await signer.getAddress(); + const msgParams = JSON.stringify({ domain, primaryType, message, types }); + + signature = await signer.provider.jsonRpcFetchFunc("eth_signTypedData_v4", [ + address, + msgParams, + ]); + + const r = "0x" + signature.substring(2).substring(0, 64); + const s = "0x" + signature.substring(2).substring(64, 128); + const v = "0x" + signature.substring(2).substring(128, 130); + + signature = { r, s, v }; + } + } catch (e) { + throw new Error(e); + } + + return signature; } module.exports = { - encodeType, - typeHash, - encodeData, - domainSeparator, - structHash, - digestToSign, - sign, + encodeType, + typeHash, + encodeData, + domainSeparator, + structHash, + digestToSign, + sign, }; diff --git a/tests/Utils/Ethereum.js b/tests/Utils/Ethereum.js index 2460a4cb2..d6d09bec6 100644 --- a/tests/Utils/Ethereum.js +++ b/tests/Utils/Ethereum.js @@ -5,193 +5,199 @@ const BigNumber = require("bignumber.js"); const { ethers } = require("hardhat"); function UInt256Max() { - return ethers.constants.MaxUint256; + return ethers.constants.MaxUint256; } function address(n) { - return `0x${n.toString(16).padStart(40, "0")}`; + return `0x${n.toString(16).padStart(40, "0")}`; } function encodeParameters(types, values) { - const abi = new ethers.utils.AbiCoder(); - return abi.encode(types, values); + const abi = new ethers.utils.AbiCoder(); + return abi.encode(types, values); } async function etherBalance(addr) { - return new BigNumber(await web3.eth.getBalance(addr)); + return new BigNumber(await web3.eth.getBalance(addr)); } async function etherGasCost(receipt) { - const tx = await web3.eth.getTransaction(receipt.transactionHash); - const gasUsed = new BigNumber(receipt.gasUsed); - const gasPrice = new BigNumber(tx.gasPrice); - return gasUsed.times(gasPrice); + const tx = await web3.eth.getTransaction(receipt.transactionHash); + const gasUsed = new BigNumber(receipt.gasUsed); + const gasPrice = new BigNumber(tx.gasPrice); + return gasUsed.times(gasPrice); } function etherExp(num) { - return etherMantissa(num, 1e18); + return etherMantissa(num, 1e18); } function etherDouble(num) { - return etherMantissa(num, 1e36); + return etherMantissa(num, 1e36); } function etherMantissa(num, scale = 1e18) { - if (num < 0) return new BigNumber(2).pow(256).plus(num); - return new BigNumber(num).times(scale); + if (num < 0) return new BigNumber(2).pow(256).plus(num); + return new BigNumber(num).times(scale); } function etherUnsigned(num) { - return new BigNumber(num); + return new BigNumber(num); } function mergeInterface(into, from) { - const key = (item) => (item.inputs ? `${item.name}/${item.inputs.length}` : item.name); - const existing = into.options.jsonInterface.reduce((acc, item) => { - acc[key(item)] = true; - return acc; - }, {}); - const extended = from.options.jsonInterface.reduce((acc, item) => { - if (!(key(item) in existing)) acc.push(item); - return acc; - }, into.options.jsonInterface.slice()); - into.options.jsonInterface = into.options.jsonInterface.concat(from.options.jsonInterface); - return into; + const key = (item) => (item.inputs ? `${item.name}/${item.inputs.length}` : item.name); + const existing = into.options.jsonInterface.reduce((acc, item) => { + acc[key(item)] = true; + return acc; + }, {}); + const extended = from.options.jsonInterface.reduce((acc, item) => { + if (!(key(item) in existing)) acc.push(item); + return acc; + }, into.options.jsonInterface.slice()); + into.options.jsonInterface = into.options.jsonInterface.concat(from.options.jsonInterface); + return into; } function getContractDefaults() { - return { gas: 20000000, gasPrice: 20000 }; + return { gas: 20000000, gasPrice: 20000 }; } function keccak256(values) { - return ethers.utils.keccak256(values); + return ethers.utils.keccak256(values); } // not working with hardhat, use hardhat_utils as a workaround function unlockedAccounts() { - let provider = web3.currentProvider; - if (provider._providers) provider = provider._providers.find((p) => p._ganacheProvider)._ganacheProvider; - return provider.manager.state.unlocked_accounts; + let provider = web3.currentProvider; + if (provider._providers) + provider = provider._providers.find((p) => p._ganacheProvider)._ganacheProvider; + return provider.manager.state.unlocked_accounts; } // not working with hardhat function unlockedAccount(a) { - return unlockedAccounts()[a.toLowerCase()]; + return unlockedAccounts()[a.toLowerCase()]; } async function mineBlockNumber(blockNumber) { - return rpc({ method: "evm_mineBlockNumber", params: [blockNumber] }); + return rpc({ method: "evm_mineBlockNumber", params: [blockNumber] }); } async function mineBlock() { - return rpc({ method: "evm_mine" }); + return rpc({ method: "evm_mine" }); } async function increaseTime(seconds) { - await rpc({ method: "evm_increaseTime", params: [seconds] }); - return rpc({ method: "evm_mine" }); + await rpc({ method: "evm_increaseTime", params: [seconds] }); + return rpc({ method: "evm_mine" }); } // doesn't work with hardhat async function setTime(seconds) { - await rpc({ method: "evm_setTime", params: [new Date(seconds * 1000)] }); + await rpc({ method: "evm_setTime", params: [new Date(seconds * 1000)] }); } // doesn't work with hardhat async function freezeTime(seconds) { - await rpc({ method: "evm_freezeTime", params: [seconds] }); - return rpc({ method: "evm_mine" }); + await rpc({ method: "evm_freezeTime", params: [seconds] }); + return rpc({ method: "evm_mine" }); } // adapted for both truffle and hardhat async function advanceBlocks(blocks) { - //let res = parseInt(await rpc({ method: "eth_blockNumber" }), 16); - //console.log(`await rpc({ method: "eth_blockNumber" }): ${res}`); - let currentBlockNumber = await blockNumber(); - //console.log(`Ethereum::currentBlockNumber: ${currentBlockNumber}`); - //let { result: num } = await rpc({ method: "eth_blockNumber" }); - //await rpc({ method: "evm_mineBlockNumber", params: [blocks + parseInt(num)] }); - //await rpc({ method: "evm_mineBlockNumber", params: [blocks + num] }); - for (let i = currentBlockNumber; i < blocks; i++) { - await mineBlock(); - } + //let res = parseInt(await rpc({ method: "eth_blockNumber" }), 16); + //console.log(`await rpc({ method: "eth_blockNumber" }): ${res}`); + let currentBlockNumber = await blockNumber(); + //console.log(`Ethereum::currentBlockNumber: ${currentBlockNumber}`); + //let { result: num } = await rpc({ method: "eth_blockNumber" }); + //await rpc({ method: "evm_mineBlockNumber", params: [blocks + parseInt(num)] }); + //await rpc({ method: "evm_mineBlockNumber", params: [blocks + num] }); + for (let i = currentBlockNumber; i < blocks; i++) { + await mineBlock(); + } } async function setNextBlockTimestamp(timestamp) { - await rpc({ method: "evm_setNextBlockTimestamp", params: [timestamp] }); + await rpc({ method: "evm_setNextBlockTimestamp", params: [timestamp] }); } async function blockNumber() { - let { result: num } = await rpc({ method: "eth_blockNumber" }); - if (num === undefined) num = await rpc({ method: "eth_blockNumber" }); - //let { result: num } = await rpc({ method: "eth_blockNumber" }); - return parseInt(num); - //return num; + let { result: num } = await rpc({ method: "eth_blockNumber" }); + if (num === undefined) num = await rpc({ method: "eth_blockNumber" }); + //let { result: num } = await rpc({ method: "eth_blockNumber" }); + return parseInt(num); + //return num; } async function lastBlock() { - return await rpc({ method: "eth_getBlockByNumber", params: ["latest", true] }); + return await rpc({ method: "eth_getBlockByNumber", params: ["latest", true] }); } // doesn't work with hardhat async function minerStart() { - return rpc({ method: "miner_start" }); + return rpc({ method: "miner_start" }); } // doesn't work with hardhat async function minerStop() { - return rpc({ method: "miner_stop" }); + return rpc({ method: "miner_stop" }); } // adapted to work in both truffle and hardhat async function rpc(request) { - try { - return await network.provider.request(request); - } catch (e) { - if (typeof network != "undefined") console.error(e); - //console.log("Ethereum.js rpc:: network is undefined. Trying web3.currentProvider.send..."); - return new Promise((okay, fail) => web3.currentProvider.send(request, (err, res) => (err ? fail(err) : okay(res)))); - } + try { + return await network.provider.request(request); + } catch (e) { + if (typeof network != "undefined") console.error(e); + //console.log("Ethereum.js rpc:: network is undefined. Trying web3.currentProvider.send..."); + return new Promise((okay, fail) => + web3.currentProvider.send(request, (err, res) => (err ? fail(err) : okay(res))) + ); + } } async function both(contract, method, args = [], opts = {}) { - const reply = await call(contract, method, args, opts); - const receipt = await send(contract, method, args, opts); - return { reply, receipt }; + const reply = await call(contract, method, args, opts); + const receipt = await send(contract, method, args, opts); + return { reply, receipt }; } async function sendFallback(contract, opts = {}) { - const receipt = await web3.eth.sendTransaction({ to: contract._address, ...Object.assign(getContractDefaults(), opts) }); - return Object.assign(receipt, { events: receipt.logs }); + const receipt = await web3.eth.sendTransaction({ + to: contract._address, + ...Object.assign(getContractDefaults(), opts), + }); + return Object.assign(receipt, { events: receipt.logs }); } module.exports = { - address, - encodeParameters, - etherBalance, - etherGasCost, - etherExp, - etherDouble, - etherMantissa, - etherUnsigned, - mergeInterface, - keccak256, - unlockedAccounts, - unlockedAccount, - - advanceBlocks, - blockNumber, - lastBlock, - freezeTime, - increaseTime, - mineBlock, - mineBlockNumber, - minerStart, - minerStop, - rpc, - setTime, - setNextBlockTimestamp, - both, - sendFallback, - UInt256Max, + address, + encodeParameters, + etherBalance, + etherGasCost, + etherExp, + etherDouble, + etherMantissa, + etherUnsigned, + mergeInterface, + keccak256, + unlockedAccounts, + unlockedAccount, + + advanceBlocks, + blockNumber, + lastBlock, + freezeTime, + increaseTime, + mineBlock, + mineBlockNumber, + minerStart, + minerStop, + rpc, + setTime, + setNextBlockTimestamp, + both, + sendFallback, + UInt256Max, }; diff --git a/tests/Utils/hardhat_utils.js b/tests/Utils/hardhat_utils.js index 89aa4b36d..f1054726d 100644 --- a/tests/Utils/hardhat_utils.js +++ b/tests/Utils/hardhat_utils.js @@ -1,17 +1,25 @@ const hh = require("hardhat"); -const { normalizeHardhatNetworkAccountsConfig, derivePrivateKeys } = require("hardhat/internal/core/providers/util"); +const { + normalizeHardhatNetworkAccountsConfig, + derivePrivateKeys, +} = require("hardhat/internal/core/providers/util"); function getAccountsPrivateKeys() { - const netConfig = hh.network.config; - return normalizeHardhatNetworkAccountsConfig(netConfig.accounts); + const netConfig = hh.network.config; + return normalizeHardhatNetworkAccountsConfig(netConfig.accounts); } function getAccountsPrivateKeysBuffer() { - const accountsConfig = hh.network.config.accounts; - return derivePrivateKeys(accountsConfig.mnemonic, accountsConfig.path, accountsConfig.initialIndex, accountsConfig.count); + const accountsConfig = hh.network.config.accounts; + return derivePrivateKeys( + accountsConfig.mnemonic, + accountsConfig.path, + accountsConfig.initialIndex, + accountsConfig.count + ); } module.exports = { - getAccountsPrivateKeys, - getAccountsPrivateKeysBuffer, + getAccountsPrivateKeys, + getAccountsPrivateKeysBuffer, }; diff --git a/tests/Utils/initializer.js b/tests/Utils/initializer.js index 1af17eccc..ba818dd88 100644 --- a/tests/Utils/initializer.js +++ b/tests/Utils/initializer.js @@ -42,434 +42,492 @@ const tenKEth = new BN(wei("10", "kether")); const totalSupply = new BN(10).pow(new BN(50)).toString(); const CONSTANTS = { - ZERO_ADDRESS: "0x0000000000000000000000000000000000000000", - ONE_ADDRESS: "0x0000000000000000000000000000000000000001", - MAX_UINT: "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + ZERO_ADDRESS: "0x0000000000000000000000000000000000000000", + ONE_ADDRESS: "0x0000000000000000000000000000000000000001", + MAX_UINT: "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", }; const getSUSD = async () => { - const susd = await TestToken.new("SUSD", "SUSD", 18, totalSupply); - return susd; + const susd = await TestToken.new("SUSD", "SUSD", 18, totalSupply); + return susd; }; const getRBTC = async () => { - const rbtc = await TestToken.new("RBTC", "RBTC", 18, totalSupply); - return rbtc; + const rbtc = await TestToken.new("RBTC", "RBTC", 18, totalSupply); + return rbtc; }; const getWRBTC = async () => { - const wrbtc = await TestWrbtc.new(); - return wrbtc; + const wrbtc = await TestWrbtc.new(); + return wrbtc; }; const getBZRX = async () => { - const bzrx = await TestWrbtc.new(); - return bzrx; + const bzrx = await TestWrbtc.new(); + return bzrx; }; /// @dev This SOV token is not the SOV token on production /// but a test token with a simpler functionality, /// lacking for example approveAndCall method. const getSOV = async (sovryn, priceFeeds, SUSD, accounts) => { - const sov = await TestToken.new("SOV", "SOV", 18, totalSupply); - await sovryn.setSovrynProtocolAddress(sovryn.address); - await sovryn.setProtocolTokenAddress(sov.address); - await sovryn.setSOVTokenAddress(sov.address); - await sovryn.setLockedSOVAddress((await LockedSOVMockup.new(sov.address, [accounts[0]])).address); - - await priceFeeds.setRates(SUSD.address, sov.address, oneEth); - - await sov.approve(sovryn.address, new BN(10).pow(new BN(20))); - await sovryn.depositProtocolToken(new BN(10).pow(new BN(20))); - - return sov; + const sov = await TestToken.new("SOV", "SOV", 18, totalSupply); + await sovryn.setSovrynProtocolAddress(sovryn.address); + await sovryn.setProtocolTokenAddress(sov.address); + await sovryn.setSOVTokenAddress(sov.address); + await sovryn.setLockedSOVAddress( + ( + await LockedSOVMockup.new(sov.address, [accounts[0]]) + ).address + ); + + await priceFeeds.setRates(SUSD.address, sov.address, oneEth); + + await sov.approve(sovryn.address, new BN(10).pow(new BN(20))); + await sovryn.depositProtocolToken(new BN(10).pow(new BN(20))); + + return sov; }; const getPriceFeeds = async (WRBTC, SUSD, RBTC, BZRX) => { - const feeds = await PriceFeedsLocal.new(WRBTC.address, BZRX.address); + const feeds = await PriceFeedsLocal.new(WRBTC.address, BZRX.address); - await feeds.setRates(WRBTC.address, RBTC.address, oneEth.toString()); - await feeds.setRates(WRBTC.address, SUSD.address, new BN(10).pow(new BN(22)).toString()); - await feeds.setRates(RBTC.address, SUSD.address, new BN(10).pow(new BN(22)).toString()); - return feeds; + await feeds.setRates(WRBTC.address, RBTC.address, oneEth.toString()); + await feeds.setRates(WRBTC.address, SUSD.address, new BN(10).pow(new BN(22)).toString()); + await feeds.setRates(RBTC.address, SUSD.address, new BN(10).pow(new BN(22)).toString()); + return feeds; }; const getPriceFeedsRBTC = async (WRBTC, SUSD, RBTC, sovryn, BZRX) => { - const feeds = await PriceFeedsLocal.new(WRBTC.address, BZRX.address); + const feeds = await PriceFeedsLocal.new(WRBTC.address, BZRX.address); - await feeds.setRates(WRBTC.address, RBTC.address, oneEth.toString()); - await feeds.setRates(WRBTC.address, SUSD.address, oneEth.toString()); - await feeds.setRates(RBTC.address, SUSD.address, new BN(10).pow(new BN(22)).toString()); - return feeds; + await feeds.setRates(WRBTC.address, RBTC.address, oneEth.toString()); + await feeds.setRates(WRBTC.address, SUSD.address, oneEth.toString()); + await feeds.setRates(RBTC.address, SUSD.address, new BN(10).pow(new BN(22)).toString()); + return feeds; }; const getSovryn = async (WRBTC, SUSD, RBTC, priceFeeds) => { - const sovrynproxy = await sovrynProtocol.new(); - const sovryn = await ISovryn.at(sovrynproxy.address); - - await sovryn.replaceContract((await ProtocolSettings.new()).address); - await sovryn.replaceContract((await LoanSettings.new()).address); - await sovryn.replaceContract((await LoanMaintenance.new()).address); - await sovryn.replaceContract((await SwapsExternal.new()).address); - - const sovrynSwapSimulator = await TestSovrynSwap.new(priceFeeds.address); - await sovryn.setSovrynSwapContractRegistryAddress(sovrynSwapSimulator.address); - await sovryn.setSupportedTokens([SUSD.address, RBTC.address, WRBTC.address], [true, true, true]); - - await sovryn.setWrbtcToken(WRBTC.address); - - // loanOpening - const swaps = await SwapsImplSovrynSwap.new(); - await sovryn.replaceContract((await LoanOpenings.new()).address); - await sovryn.setPriceFeedContract(priceFeeds.address); - await sovryn.setSwapsImplContract(swaps.address); - - // loanClosing - await sovryn.replaceContract((await LoanClosingsWith.new()).address); - await sovryn.replaceContract((await LoanClosingsLiquidation.new()).address); - await sovryn.replaceContract((await LoanClosingsRollover.new()).address); - - // affiliates - await sovryn.replaceContract((await Affiliates.new()).address); - - return sovryn; + const sovrynproxy = await sovrynProtocol.new(); + const sovryn = await ISovryn.at(sovrynproxy.address); + + await sovryn.replaceContract((await ProtocolSettings.new()).address); + await sovryn.replaceContract((await LoanSettings.new()).address); + await sovryn.replaceContract((await LoanMaintenance.new()).address); + await sovryn.replaceContract((await SwapsExternal.new()).address); + + const sovrynSwapSimulator = await TestSovrynSwap.new(priceFeeds.address); + await sovryn.setSovrynSwapContractRegistryAddress(sovrynSwapSimulator.address); + await sovryn.setSupportedTokens( + [SUSD.address, RBTC.address, WRBTC.address], + [true, true, true] + ); + + await sovryn.setWrbtcToken(WRBTC.address); + + // loanOpening + const swaps = await SwapsImplSovrynSwap.new(); + await sovryn.replaceContract((await LoanOpenings.new()).address); + await sovryn.setPriceFeedContract(priceFeeds.address); + await sovryn.setSwapsImplContract(swaps.address); + + // loanClosing + await sovryn.replaceContract((await LoanClosingsWith.new()).address); + await sovryn.replaceContract((await LoanClosingsLiquidation.new()).address); + await sovryn.replaceContract((await LoanClosingsRollover.new()).address); + + // affiliates + await sovryn.replaceContract((await Affiliates.new()).address); + + return sovryn; }; // Loan Token const getLoanTokenLogic = async (isMockLoanToken = false) => { - /** Deploy LoanTokenLogicBeacon */ - const loanTokenLogicBeacon = await LoanTokenLogicBeacon.new(); + /** Deploy LoanTokenLogicBeacon */ + const loanTokenLogicBeacon = await LoanTokenLogicBeacon.new(); - /** Deploy LoanTokenSettingsLowerAdmin*/ - const loanTokenSettingsLowerAdmin = await LoanTokenSettingsLowerAdmin.new(); + /** Deploy LoanTokenSettingsLowerAdmin*/ + const loanTokenSettingsLowerAdmin = await LoanTokenSettingsLowerAdmin.new(); - /** Register Loan Token Modules to the Beacon */ - await loanTokenLogicBeacon.registerLoanTokenModule(loanTokenSettingsLowerAdmin.address); + /** Register Loan Token Modules to the Beacon */ + await loanTokenLogicBeacon.registerLoanTokenModule(loanTokenSettingsLowerAdmin.address); - /** Deploy LoanTokenLogicLM */ - let loanTokenLogicLM; - if (isMockLoanToken) { - loanTokenLogicLM = await MockLoanTokenLogic.new(); - } else { - loanTokenLogicLM = await LoanTokenLogicLM.new(); - } + /** Deploy LoanTokenLogicLM */ + let loanTokenLogicLM; + if (isMockLoanToken) { + loanTokenLogicLM = await MockLoanTokenLogic.new(); + } else { + loanTokenLogicLM = await LoanTokenLogicLM.new(); + } - /** Register Loan Token Logic LM to the Beacon */ - await loanTokenLogicBeacon.registerLoanTokenModule(loanTokenLogicLM.address); + /** Register Loan Token Logic LM to the Beacon */ + await loanTokenLogicBeacon.registerLoanTokenModule(loanTokenLogicLM.address); - /** Deploy LoanTokenLogicProxy */ - loanTokenLogicProxy = await LoanTokenLogicProxy.new(loanTokenLogicBeacon.address); + /** Deploy LoanTokenLogicProxy */ + loanTokenLogicProxy = await LoanTokenLogicProxy.new(loanTokenLogicBeacon.address); - return [loanTokenLogicProxy, loanTokenLogicBeacon]; + return [loanTokenLogicProxy, loanTokenLogicBeacon]; }; const getLoanTokenLogicWrbtc = async () => { - /** Deploy LoanTokenLogicBeacon */ - const loanTokenLogicBeacon = await LoanTokenLogicBeacon.new(); + /** Deploy LoanTokenLogicBeacon */ + const loanTokenLogicBeacon = await LoanTokenLogicBeacon.new(); - /** Deploy LoanTokenSettingsLowerAdmin*/ - const loanTokenSettingsLowerAdmin = await LoanTokenSettingsLowerAdmin.new(); + /** Deploy LoanTokenSettingsLowerAdmin*/ + const loanTokenSettingsLowerAdmin = await LoanTokenSettingsLowerAdmin.new(); - /** Register Loan Token Modules to the Beacon */ - await loanTokenLogicBeacon.registerLoanTokenModule(loanTokenSettingsLowerAdmin.address); + /** Register Loan Token Modules to the Beacon */ + await loanTokenLogicBeacon.registerLoanTokenModule(loanTokenSettingsLowerAdmin.address); - /** Deploy LoanTokenLogicLM */ - const loanTokenLogicWrbtc = await LoanTokenLogicWrbtc.new(); + /** Deploy LoanTokenLogicLM */ + const loanTokenLogicWrbtc = await LoanTokenLogicWrbtc.new(); - /** Register Loan Token Logic LM to the Beacon */ - await loanTokenLogicBeacon.registerLoanTokenModule(loanTokenLogicWrbtc.address); + /** Register Loan Token Logic LM to the Beacon */ + await loanTokenLogicBeacon.registerLoanTokenModule(loanTokenLogicWrbtc.address); - /** Deploy LoanTokenLogicProxy */ - loanTokenLogicProxy = await LoanTokenLogicProxy.new(loanTokenLogicBeacon.address); + /** Deploy LoanTokenLogicProxy */ + loanTokenLogicProxy = await LoanTokenLogicProxy.new(loanTokenLogicBeacon.address); - return [loanTokenLogicProxy, loanTokenLogicBeacon]; + return [loanTokenLogicProxy, loanTokenLogicBeacon]; }; const getLoanTokenSettings = async () => { - const loanSettings = await LoanSettings.new(); - return loanSettings; + const loanSettings = await LoanSettings.new(); + return loanSettings; }; const getLoanToken = async (owner, sovryn, WRBTC, SUSD, mockLogic = false) => { - const initLoanTokenLogic = await getLoanTokenLogic(mockLogic); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] - loanTokenLogic = initLoanTokenLogic[0]; - loanTokenLogicBeacon = initLoanTokenLogic[1]; - - let loanToken = await LoanToken.new(owner, loanTokenLogic.address, sovryn.address, WRBTC.address); - await loanToken.initialize(SUSD.address, "SUSD", "SUSD"); //iToken - - /** Initialize the loan token logic proxy */ - loanToken = await ILoanTokenLogicProxy.at(loanToken.address); - await loanToken.setBeaconAddress(loanTokenLogicBeacon.address); - - /** Use interface of LoanTokenModules */ - loanToken = await ILoanTokenModules.at(loanToken.address); - - // assert loanToken.tokenPrice() == loanToken.initialPrice() - // const initial_total_supply = await loanToken.totalSupply(); - // loan token total supply should be zero - // assert initial_total_supply == loanToken.totalSupply() - return loanToken; + const initLoanTokenLogic = await getLoanTokenLogic(mockLogic); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] + loanTokenLogic = initLoanTokenLogic[0]; + loanTokenLogicBeacon = initLoanTokenLogic[1]; + + let loanToken = await LoanToken.new( + owner, + loanTokenLogic.address, + sovryn.address, + WRBTC.address + ); + await loanToken.initialize(SUSD.address, "SUSD", "SUSD"); //iToken + + /** Initialize the loan token logic proxy */ + loanToken = await ILoanTokenLogicProxy.at(loanToken.address); + await loanToken.setBeaconAddress(loanTokenLogicBeacon.address); + + /** Use interface of LoanTokenModules */ + loanToken = await ILoanTokenModules.at(loanToken.address); + + // assert loanToken.tokenPrice() == loanToken.initialPrice() + // const initial_total_supply = await loanToken.totalSupply(); + // loan token total supply should be zero + // assert initial_total_supply == loanToken.totalSupply() + return loanToken; }; const getLoanTokenWRBTC = async (owner, sovryn, WRBTC, SUSD, mockLogic = false) => { - const initLoanTokenLogic = await getLoanTokenLogicWrbtc(mockLogic); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] - loanTokenLogicWrbtc = initLoanTokenLogic[0]; - loanTokenLogicBeacon = initLoanTokenLogic[1]; - - let loanTokenWRBTC = await LoanToken.new(owner, loanTokenLogicWrbtc.address, sovryn.address, WRBTC.address); - await loanTokenWRBTC.initialize(WRBTC.address, "iWRBTC", "iWRBTC"); //iToken - - /** Initialize the loan token logic proxy */ - loanTokenWRBTC = await ILoanTokenLogicProxy.at(loanTokenWRBTC.address); - await loanTokenWRBTC.setBeaconAddress(loanTokenLogicBeacon.address); - - /** Use interface of LoanTokenModules */ - loanTokenWRBTC = await ILoanTokenModules.at(loanTokenWRBTC.address); - - // assert loanToken.tokenPrice() == loanToken.initialPrice() - // const initial_total_supply = await loanToken.totalSupply(); - // loan token total supply should be zero - // assert initial_total_supply == loanToken.totalSupply() - return loanTokenWRBTC; + const initLoanTokenLogic = await getLoanTokenLogicWrbtc(mockLogic); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] + loanTokenLogicWrbtc = initLoanTokenLogic[0]; + loanTokenLogicBeacon = initLoanTokenLogic[1]; + + let loanTokenWRBTC = await LoanToken.new( + owner, + loanTokenLogicWrbtc.address, + sovryn.address, + WRBTC.address + ); + await loanTokenWRBTC.initialize(WRBTC.address, "iWRBTC", "iWRBTC"); //iToken + + /** Initialize the loan token logic proxy */ + loanTokenWRBTC = await ILoanTokenLogicProxy.at(loanTokenWRBTC.address); + await loanTokenWRBTC.setBeaconAddress(loanTokenLogicBeacon.address); + + /** Use interface of LoanTokenModules */ + loanTokenWRBTC = await ILoanTokenModules.at(loanTokenWRBTC.address); + + // assert loanToken.tokenPrice() == loanToken.initialPrice() + // const initial_total_supply = await loanToken.totalSupply(); + // loan token total supply should be zero + // assert initial_total_supply == loanToken.totalSupply() + return loanTokenWRBTC; }; -const loan_pool_setup = async (sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC, minInitialMargin = wei("20", "ether")) => { - let params = []; - let config = [ - "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object - false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans - owner, // address owner; // owner of this object - CONSTANTS.ZERO_ADDRESS, // address loanToken; // the token being loaned - RBTC.address, // address collateralToken; // the required collateral token - minInitialMargin, // uint256 minInitialMargin; // the minimum allowed initial margin - wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value - 0, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) - ]; - params.push(config); - const copy1 = [...config]; - copy1[4] = WRBTC.address; - params.push(copy1); - - await loanToken.setupLoanParams(params, false); - await loanToken.setupLoanParams(params, true); - - params = []; - const copy2 = [...config]; - copy2[4] = SUSD.address; - params.push(copy2); - - await loanTokenWRBTC.setupLoanParams(params, false); - await loanTokenWRBTC.setupLoanParams(params, true); - - await sovryn.setLoanPool([loanToken.address, loanTokenWRBTC.address], [SUSD.address, WRBTC.address]); +const loan_pool_setup = async ( + sovryn, + owner, + RBTC, + WRBTC, + SUSD, + loanToken, + loanTokenWRBTC, + minInitialMargin = wei("20", "ether") +) => { + let params = []; + let config = [ + "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object + false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans + owner, // address owner; // owner of this object + CONSTANTS.ZERO_ADDRESS, // address loanToken; // the token being loaned + RBTC.address, // address collateralToken; // the required collateral token + minInitialMargin, // uint256 minInitialMargin; // the minimum allowed initial margin + wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value + 0, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) + ]; + params.push(config); + const copy1 = [...config]; + copy1[4] = WRBTC.address; + params.push(copy1); + + await loanToken.setupLoanParams(params, false); + await loanToken.setupLoanParams(params, true); + + params = []; + const copy2 = [...config]; + copy2[4] = SUSD.address; + params.push(copy2); + + await loanTokenWRBTC.setupLoanParams(params, false); + await loanTokenWRBTC.setupLoanParams(params, true); + + await sovryn.setLoanPool( + [loanToken.address, loanTokenWRBTC.address], + [SUSD.address, WRBTC.address] + ); }; const set_demand_curve = async (loanToken) => { - const baseRate = wei("1", "ether"); - const rateMultiplier = wei("20.25", "ether"); - const targetLevel = wei("80", "ether"); - const kinkLevel = wei("90", "ether"); - const maxScaleRate = wei("100", "ether"); - - const localLoanToken = await ILoanTokenModules.at(loanToken.address); - await localLoanToken.setDemandCurve(baseRate, rateMultiplier, baseRate, rateMultiplier, targetLevel, kinkLevel, maxScaleRate); - // borrow_interest_rate = loanToken.borrowInterestRate(); - // print("borrowInterestRate: ", borrow_interest_rate); - // assert(borrow_interest_rate > baseRate); + const baseRate = wei("1", "ether"); + const rateMultiplier = wei("20.25", "ether"); + const targetLevel = wei("80", "ether"); + const kinkLevel = wei("90", "ether"); + const maxScaleRate = wei("100", "ether"); + + const localLoanToken = await ILoanTokenModules.at(loanToken.address); + await localLoanToken.setDemandCurve( + baseRate, + rateMultiplier, + baseRate, + rateMultiplier, + targetLevel, + kinkLevel, + maxScaleRate + ); + // borrow_interest_rate = loanToken.borrowInterestRate(); + // print("borrowInterestRate: ", borrow_interest_rate); + // assert(borrow_interest_rate > baseRate); }; const lend_to_pool = async (loanToken, SUSD, lender) => { - const lend_amount = new BN(10).pow(new BN(30)).toString(); - await SUSD.mint(lender, lend_amount); - await SUSD.approve(loanToken.address, lend_amount); - await loanToken.mint(lender, lend_amount); - return [lender, lend_amount]; + const lend_amount = new BN(10).pow(new BN(30)).toString(); + await SUSD.mint(lender, lend_amount); + await SUSD.approve(loanToken.address, lend_amount); + await loanToken.mint(lender, lend_amount); + return [lender, lend_amount]; }; const lend_to_pool_iBTC = async (loanTokenWRBTC, lender) => { - const lend_amount = new BN(10).pow(new BN(21)).toString(); - await loanTokenWRBTC.mintWithBTC(lender, false, { from: lender, value: lend_amount }); - return [lender, lend_amount]; + const lend_amount = new BN(10).pow(new BN(21)).toString(); + await loanTokenWRBTC.mintWithBTC(lender, false, { from: lender, value: lend_amount }); + return [lender, lend_amount]; }; const open_margin_trade_position = async ( - loanToken, - RBTC, - WRBTC, - SUSD, - trader, - collateral = "RBTC", - loan_token_sent = hunEth.toString(), - leverage_amount = new BN(2).mul(oneEth).toString() + loanToken, + RBTC, + WRBTC, + SUSD, + trader, + collateral = "RBTC", + loan_token_sent = hunEth.toString(), + leverage_amount = new BN(2).mul(oneEth).toString() ) => { - await SUSD.mint(trader, loan_token_sent); - await SUSD.approve(loanToken.address, loan_token_sent, { from: trader }); - - let collateralToken; - if (collateral == "RBTC") collateralToken = RBTC.address; - else collateralToken = WRBTC.address; - - const { receipt } = await loanToken.marginTrade( - "0x0", // loanId (0 for new loans) - leverage_amount, // leverageAmount - loan_token_sent, // loanTokenSent - 0, // no collateral token sent - collateralToken, // collateralTokenAddress - trader, // trader, - 0, // slippage - [], // loanDataBytes (only required with ether) - { from: trader } - ); - const decode = decodeLogs(receipt.rawLogs, LoanOpenings, "Trade"); - return [decode[0].args["loanId"], trader, loan_token_sent, leverage_amount]; + await SUSD.mint(trader, loan_token_sent); + await SUSD.approve(loanToken.address, loan_token_sent, { from: trader }); + + let collateralToken; + if (collateral == "RBTC") collateralToken = RBTC.address; + else collateralToken = WRBTC.address; + + const { receipt } = await loanToken.marginTrade( + "0x0", // loanId (0 for new loans) + leverage_amount, // leverageAmount + loan_token_sent, // loanTokenSent + 0, // no collateral token sent + collateralToken, // collateralTokenAddress + trader, // trader, + 0, // slippage + [], // loanDataBytes (only required with ether) + { from: trader } + ); + const decode = decodeLogs(receipt.rawLogs, LoanOpenings, "Trade"); + return [decode[0].args["loanId"], trader, loan_token_sent, leverage_amount]; }; const open_margin_trade_position_iBTC = async ( - loanTokenWRBTC, - SUSD, - trader, - loan_token_sent = oneEth.toString(), - leverage_amount = new BN(2).mul(oneEth).toString() + loanTokenWRBTC, + SUSD, + trader, + loan_token_sent = oneEth.toString(), + leverage_amount = new BN(2).mul(oneEth).toString() ) => { - const { receipt } = await loanTokenWRBTC.marginTrade( - "0x0", // loanId (0 for new loans) - leverage_amount, // leverageAmount - loan_token_sent, // loanTokenSent - 0, // no collateral token sent - SUSD.address, // collateralTokenAddress - trader, // trader, - 0, // slippage - [], // loanDataBytes (only required with ether) - { from: trader, value: loan_token_sent } - ); - - const decode = decodeLogs(receipt.rawLogs, LoanOpenings, "Trade"); - return [decode[0].args["loanId"], trader, loan_token_sent, leverage_amount]; + const { receipt } = await loanTokenWRBTC.marginTrade( + "0x0", // loanId (0 for new loans) + leverage_amount, // leverageAmount + loan_token_sent, // loanTokenSent + 0, // no collateral token sent + SUSD.address, // collateralTokenAddress + trader, // trader, + 0, // slippage + [], // loanDataBytes (only required with ether) + { from: trader, value: loan_token_sent } + ); + + const decode = decodeLogs(receipt.rawLogs, LoanOpenings, "Trade"); + return [decode[0].args["loanId"], trader, loan_token_sent, leverage_amount]; }; const borrow_indefinite_loan = async ( - loanToken, - sovryn, - SUSD, - RBTC, - accounts, - withdraw_amount = new BN(10).mul(oneEth).toString(), - margin = new BN(50).mul(oneEth).toString(), - duration_in_seconds = 60 * 60 * 24 * 10 + loanToken, + sovryn, + SUSD, + RBTC, + accounts, + withdraw_amount = new BN(10).mul(oneEth).toString(), + margin = new BN(50).mul(oneEth).toString(), + duration_in_seconds = 60 * 60 * 24 * 10 ) => { - const borrower = accounts[2]; - const receiver = accounts[1]; - const collateral_token_sent = await sovryn.getRequiredCollateral(SUSD.address, RBTC.address, withdraw_amount, margin, true); - // approve the transfer of the collateral - await RBTC.mint(borrower, collateral_token_sent); - await RBTC.approve(loanToken.address, collateral_token_sent, { from: borrower }); - // borrow some funds - const tx = await loanToken.borrow( - constants.ZERO_BYTES32, // bytes32 loanId - withdraw_amount, // uint256 withdrawAmount - duration_in_seconds, // uint256 initialLoanDuration - collateral_token_sent, // uint256 collateralTokenSent - RBTC.address, // address collateralTokenAddress - borrower, // address borrower - receiver, // address receiver - "0x", // bytes memory loanDataBytes - { from: borrower } - ); - const decode = decodeLogs(tx.receipt.rawLogs, LoanOpenings, "Borrow"); - const loan_id = decode[0].args["loanId"]; - return [loan_id, borrower, receiver, withdraw_amount, duration_in_seconds, margin, decode[0].args]; + const borrower = accounts[2]; + const receiver = accounts[1]; + const collateral_token_sent = await sovryn.getRequiredCollateral( + SUSD.address, + RBTC.address, + withdraw_amount, + margin, + true + ); + // approve the transfer of the collateral + await RBTC.mint(borrower, collateral_token_sent); + await RBTC.approve(loanToken.address, collateral_token_sent, { from: borrower }); + // borrow some funds + const tx = await loanToken.borrow( + constants.ZERO_BYTES32, // bytes32 loanId + withdraw_amount, // uint256 withdrawAmount + duration_in_seconds, // uint256 initialLoanDuration + collateral_token_sent, // uint256 collateralTokenSent + RBTC.address, // address collateralTokenAddress + borrower, // address borrower + receiver, // address receiver + "0x", // bytes memory loanDataBytes + { from: borrower } + ); + const decode = decodeLogs(tx.receipt.rawLogs, LoanOpenings, "Borrow"); + const loan_id = decode[0].args["loanId"]; + return [ + loan_id, + borrower, + receiver, + withdraw_amount, + duration_in_seconds, + margin, + decode[0].args, + ]; }; function decodeLogs(logs, emitter, eventName) { - let abi; - let address; - abi = emitter.abi; - try { - address = emitter.address; - } catch (e) { - address = null; - } - - let eventABI = abi.filter((x) => x.type === "event" && x.name === eventName); - if (eventABI.length === 0) { - throw new Error(`No ABI entry for event '${eventName}'`); - } else if (eventABI.length > 1) { - throw new Error(`Multiple ABI entries for event '${eventName}', only uniquely named events are supported`); - } - - eventABI = eventABI[0]; - - // The first topic will equal the hash of the event signature - const eventSignature = `${eventName}(${eventABI.inputs.map((input) => input.type).join(",")})`; - const eventTopic = web3.utils.sha3(eventSignature); - - // Only decode events of type 'EventName' - return logs - .filter((log) => log.topics.length > 0 && log.topics[0] === eventTopic && (!address || log.address === address)) - .map((log) => web3.eth.abi.decodeLog(eventABI.inputs, log.data, log.topics.slice(1))) - .map((decoded) => ({ event: eventName, args: decoded })); + let abi; + let address; + abi = emitter.abi; + try { + address = emitter.address; + } catch (e) { + address = null; + } + + let eventABI = abi.filter((x) => x.type === "event" && x.name === eventName); + if (eventABI.length === 0) { + throw new Error(`No ABI entry for event '${eventName}'`); + } else if (eventABI.length > 1) { + throw new Error( + `Multiple ABI entries for event '${eventName}', only uniquely named events are supported` + ); + } + + eventABI = eventABI[0]; + + // The first topic will equal the hash of the event signature + const eventSignature = `${eventName}(${eventABI.inputs.map((input) => input.type).join(",")})`; + const eventTopic = web3.utils.sha3(eventSignature); + + // Only decode events of type 'EventName' + return logs + .filter( + (log) => + log.topics.length > 0 && + log.topics[0] === eventTopic && + (!address || log.address === address) + ) + .map((log) => web3.eth.abi.decodeLog(eventABI.inputs, log.data, log.topics.slice(1))) + .map((decoded) => ({ event: eventName, args: decoded })); } const verify_sov_reward_payment = async ( - logs, - FeesEvents, - SOV, - borrower, - loan_id, - sov_initial_balance, - expected_events_number, - sourceTokenAddress, - destTokenAddress, - sovryn + logs, + FeesEvents, + SOV, + borrower, + loan_id, + sov_initial_balance, + expected_events_number, + sourceTokenAddress, + destTokenAddress, + sovryn ) => { - const earn_reward_events = decodeLogs(logs, FeesEvents, "EarnReward"); - const len = earn_reward_events.length; - expect(len).to.equal(expected_events_number); - - let reward = new BN(0); - let feeRebatePercent; - for (let i = 0; i < len; i++) { - const args = earn_reward_events[i].args; - if ((await sovryn.specialRebates(sourceTokenAddress, destTokenAddress)) > 0) { - feeRebatePercent = await sovryn.specialRebates(sourceTokenAddress, destTokenAddress); - } else { - feeRebatePercent = await sovryn.feeRebatePercent(); - } - expect(args["receiver"]).to.equal(borrower); - expect(args["token"]).to.equal(SOV.address); - expect(args["loanId"]).to.equal(loan_id); - expect(args["feeRebatePercent"] == feeRebatePercent).to.be.true; - reward = reward.add(new BN(args["amount"])); - } - - lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); - expect((await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower))).to.be.a.bignumber.equal( - sov_initial_balance.add(reward) - ); + const earn_reward_events = decodeLogs(logs, FeesEvents, "EarnReward"); + const len = earn_reward_events.length; + expect(len).to.equal(expected_events_number); + + let reward = new BN(0); + let feeRebatePercent; + for (let i = 0; i < len; i++) { + const args = earn_reward_events[i].args; + if ((await sovryn.specialRebates(sourceTokenAddress, destTokenAddress)) > 0) { + feeRebatePercent = await sovryn.specialRebates(sourceTokenAddress, destTokenAddress); + } else { + feeRebatePercent = await sovryn.feeRebatePercent(); + } + expect(args["receiver"]).to.equal(borrower); + expect(args["token"]).to.equal(SOV.address); + expect(args["loanId"]).to.equal(loan_id); + expect(args["feeRebatePercent"] == feeRebatePercent).to.be.true; + reward = reward.add(new BN(args["amount"])); + } + + lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); + expect( + (await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower)) + ).to.be.a.bignumber.equal(sov_initial_balance.add(reward)); }; module.exports = { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getSOV, - getLoanTokenLogic, - getLoanTokenLogicWrbtc, - getLoanToken, - getLoanTokenWRBTC, - loan_pool_setup, - lend_to_pool, - set_demand_curve, - getPriceFeeds, - getPriceFeedsRBTC, - getSovryn, - CONSTANTS, - decodeLogs, - verify_sov_reward_payment, - lend_to_pool_iBTC, - open_margin_trade_position, - open_margin_trade_position_iBTC, - borrow_indefinite_loan, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getSOV, + getLoanTokenLogic, + getLoanTokenLogicWrbtc, + getLoanToken, + getLoanTokenWRBTC, + loan_pool_setup, + lend_to_pool, + set_demand_curve, + getPriceFeeds, + getPriceFeedsRBTC, + getSovryn, + CONSTANTS, + decodeLogs, + verify_sov_reward_payment, + lend_to_pool_iBTC, + open_margin_trade_position, + open_margin_trade_position_iBTC, + borrow_indefinite_loan, }; diff --git a/tests/affiliates/affiliates.test.js b/tests/affiliates/affiliates.test.js index ef6edbb6c..2ae53cb14 100644 --- a/tests/affiliates/affiliates.test.js +++ b/tests/affiliates/affiliates.test.js @@ -49,114 +49,125 @@ const SwapsImplSovrynSwap = artifacts.require("SwapsImplSovrynSwap"); const Affiliates = artifacts.require("Affiliates"); const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getLoanTokenLogic, - getLoanToken, - getLoanTokenLogicWrbtc, - getLoanTokenWRBTC, - loan_pool_setup, - set_demand_curve, - getPriceFeeds, - getSovryn, - decodeLogs, - getSOV, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getLoanTokenLogic, + getLoanToken, + getLoanTokenLogicWrbtc, + getLoanTokenWRBTC, + loan_pool_setup, + set_demand_curve, + getPriceFeeds, + getSovryn, + decodeLogs, + getSOV, } = require("../Utils/initializer.js"); let cliff = 1; // This is in 4 weeks. i.e. 1 * 4 weeks. let duration = 11; // This is in 4 weeks. i.e. 11 * 4 weeks. contract("Affiliates", (accounts) => { - let loanTokenLogic; - let WRBTC; - let loanTokenLogicBeacon; - let doc; - let SUSD; - let lockedSOV; - let sovryn; - let loanTokenV2; - let feeds; - let wei = web3.utils.toWei; - - let swapsSovryn; - - async function deploymentAndInitFixture(_wallets, _provider) { - // Deploying sovrynProtocol w/ generic function from initializer.js - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - await sovryn.setSovrynProtocolAddress(sovryn.address); - - // Mock Loan Token Logic - const initLoanTokenLogic = await getLoanTokenLogic(true); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] - loanTokenLogic = initLoanTokenLogic[0]; - loanTokenLogicBeacon = initLoanTokenLogic[1]; - - doc = await TestToken.new("dollar on chain", "DOC", 18, wei("20000", "ether")); - - loanToken = await LoanToken.new(owner, loanTokenLogic.address, sovryn.address, WRBTC.address); - await loanToken.initialize(doc.address, "SUSD", "SUSD"); - - /** Initialize the loan token logic proxy */ - loanTokenV2 = await ILoanTokenLogicProxy.at(loanToken.address); - await loanTokenV2.setBeaconAddress(loanTokenLogicBeacon.address); - - // loanTokenV2 = await LoanTokenLogicStandard.at(loanToken.address); - loanTokenV2 = await ILoanTokenModulesMock.at(loanToken.address); // mocked for ad-hoc logic for isolated testing - const loanTokenAddress = await loanToken.loanTokenAddress(); - if (owner == (await sovryn.owner())) { - await sovryn.setLoanPool([loanTokenV2.address], [loanTokenAddress]); - } - - // Creating the Staking Instance. - stakingLogic = await StakingLogic.new(SUSD.address); - staking = await StakingProxy.new(SUSD.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingLogic.at(staking.address); - - // Creating the FeeSharing Instance. - feeSharingProxy = await FeeSharingProxy.new(constants.ZERO_ADDRESS, staking.address); - - // Creating the Vesting Instance. - vestingLogic = await VestingLogic.new(); - vestingFactory = await VestingFactory.new(vestingLogic.address); - vestingRegistry = await VestingRegistry.new( - vestingFactory.address, - SUSD.address, - staking.address, - feeSharingProxy.address, - owner // This should be Governance Timelock Contract. - ); - vestingFactory.transferOwnership(vestingRegistry.address); - - // Creating the instance of newLockedSOV Contract. - await sovryn.setLockedSOVAddress((await LockedSOV.new(SUSD.address, vestingRegistry.address, cliff, duration, [owner])).address); - lockedSOV = await LockedSOV.at(await sovryn.lockedSOVAddress()); - - // initialize - feeds = await PriceFeedsLocal.new(WRBTC.address, sovryn.address); - await feeds.setRates(doc.address, WRBTC.address, wei("0.01", "ether")); - swapsSovryn = await SwapsImplSovrynSwap.new(); - const sovrynSwapSimulator = await TestSovrynSwap.new(feeds.address); - await sovryn.setSovrynSwapContractRegistryAddress(sovrynSwapSimulator.address); - await sovryn.setSupportedTokens([doc.address, WRBTC.address], [true, true]); - await sovryn.setPriceFeedContract( - feeds.address // priceFeeds - ); - await sovryn.setSwapsImplContract( - swapsSovryn.address // swapsImpl - ); - await sovryn.setFeesController(owner); - await sovryn.setWrbtcToken(WRBTC.address); - await sovryn.setSOVTokenAddress(SUSD.address); - - { - /** + let loanTokenLogic; + let WRBTC; + let loanTokenLogicBeacon; + let doc; + let SUSD; + let lockedSOV; + let sovryn; + let loanTokenV2; + let feeds; + let wei = web3.utils.toWei; + + let swapsSovryn; + + async function deploymentAndInitFixture(_wallets, _provider) { + // Deploying sovrynProtocol w/ generic function from initializer.js + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + await sovryn.setSovrynProtocolAddress(sovryn.address); + + // Mock Loan Token Logic + const initLoanTokenLogic = await getLoanTokenLogic(true); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] + loanTokenLogic = initLoanTokenLogic[0]; + loanTokenLogicBeacon = initLoanTokenLogic[1]; + + doc = await TestToken.new("dollar on chain", "DOC", 18, wei("20000", "ether")); + + loanToken = await LoanToken.new( + owner, + loanTokenLogic.address, + sovryn.address, + WRBTC.address + ); + await loanToken.initialize(doc.address, "SUSD", "SUSD"); + + /** Initialize the loan token logic proxy */ + loanTokenV2 = await ILoanTokenLogicProxy.at(loanToken.address); + await loanTokenV2.setBeaconAddress(loanTokenLogicBeacon.address); + + // loanTokenV2 = await LoanTokenLogicStandard.at(loanToken.address); + loanTokenV2 = await ILoanTokenModulesMock.at(loanToken.address); // mocked for ad-hoc logic for isolated testing + const loanTokenAddress = await loanToken.loanTokenAddress(); + if (owner == (await sovryn.owner())) { + await sovryn.setLoanPool([loanTokenV2.address], [loanTokenAddress]); + } + + // Creating the Staking Instance. + stakingLogic = await StakingLogic.new(SUSD.address); + staking = await StakingProxy.new(SUSD.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); + + // Creating the FeeSharing Instance. + feeSharingProxy = await FeeSharingProxy.new(constants.ZERO_ADDRESS, staking.address); + + // Creating the Vesting Instance. + vestingLogic = await VestingLogic.new(); + vestingFactory = await VestingFactory.new(vestingLogic.address); + vestingRegistry = await VestingRegistry.new( + vestingFactory.address, + SUSD.address, + staking.address, + feeSharingProxy.address, + owner // This should be Governance Timelock Contract. + ); + vestingFactory.transferOwnership(vestingRegistry.address); + + // Creating the instance of newLockedSOV Contract. + await sovryn.setLockedSOVAddress( + ( + await LockedSOV.new(SUSD.address, vestingRegistry.address, cliff, duration, [ + owner, + ]) + ).address + ); + lockedSOV = await LockedSOV.at(await sovryn.lockedSOVAddress()); + + // initialize + feeds = await PriceFeedsLocal.new(WRBTC.address, sovryn.address); + await feeds.setRates(doc.address, WRBTC.address, wei("0.01", "ether")); + swapsSovryn = await SwapsImplSovrynSwap.new(); + const sovrynSwapSimulator = await TestSovrynSwap.new(feeds.address); + await sovryn.setSovrynSwapContractRegistryAddress(sovrynSwapSimulator.address); + await sovryn.setSupportedTokens([doc.address, WRBTC.address], [true, true]); + await sovryn.setPriceFeedContract( + feeds.address // priceFeeds + ); + await sovryn.setSwapsImplContract( + swapsSovryn.address // swapsImpl + ); + await sovryn.setFeesController(owner); + await sovryn.setWrbtcToken(WRBTC.address); + await sovryn.setSOVTokenAddress(SUSD.address); + + { + /** struct LoanParams { bytes32 id; // id of loan params object bool active; // if false, this object has been disabled by the owner and can't be used for future loans @@ -168,487 +179,607 @@ contract("Affiliates", (accounts) => { uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) } */ - } - params = [ - "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object - false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans - owner, // address owner; // owner of this object - doc.address, // address loanToken; // the token being loaned - WRBTC.address, // address collateralToken; // the required collateral token - wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin - wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value - 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) - ]; - - // await loanTokenV2.setupLoanParams([params], true); - await loanTokenV2.setupLoanParams([params], false); - - // setting up interest rates - const baseRate = wei("1", "ether"); - const rateMultiplier = wei("20.25", "ether"); - const targetLevel = wei("80", "ether"); - const kinkLevel = wei("90", "ether"); - const maxScaleRate = wei("100", "ether"); - await loanTokenV2.setDemandCurve(baseRate, rateMultiplier, baseRate, rateMultiplier, targetLevel, kinkLevel, maxScaleRate); - - // GIVING SOME DOC tokens to loanToken so that we can borrow from loanToken - await doc.transfer(loanTokenV2.address, wei("500", "ether")); - await doc.transfer(trader, wei("100", "ether")); - // trader approves to LoanToken loan amount for trading - await doc.approve(loanToken.address, web3.utils.toWei("100", "ether"), { from: trader }); - // Giving some testRbtc to sovrynAddress (by minting some testRbtc),so that it can open position in wRBTC. - await WRBTC.mint(sovryn.address, wei("500", "ether")); - - // Giving some SOV Token to sovrynAddress (For affiliates rewards purposes) - await SUSD.mint(sovryn.address, wei("500", "ether")); - } - - before(async () => { - [owner, trader, referrer, account1, account2, ...accounts] = accounts; - }); - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - it("Should not be able to set the affiliateFeePercent more than 100%", async () => { - const valueExperiment = wei("101", "ether"); - await expectRevert(sovryn.setAffiliateFeePercent(0, { from: referrer }), "unauthorized"); - await expectRevert(sovryn.setAffiliateFeePercent(valueExperiment), "value too high"); - }); - - it("Test coverage: call marginTradeAffiliate w/ affiliateReferrer = address(0)", async () => { - // expected in x * 10**18 where x is the actual leverage (2, 3, 4, or 5) - const leverageAmount = web3.utils.toWei("3", "ether"); - // loan tokens sent to iToken contract to start Margin Trading - const loanTokenSent = web3.utils.toWei("20", "ether"); - // AUDIT: should the call be allowed from arbitrary address to set an affiliate in - // LoanTokenLogicStandard.marginTradeAffiliate? - const tx = await loanTokenV2.marginTradeAffiliate( - constants.ZERO_BYTES32, // loanId (0 for new loans) - leverageAmount, // leverageAmount - loanTokenSent, // loanTokenSent - 0, // no collateral token sent - WRBTC.address, // collateralTokenAddress - trader, - 0, // max slippage - constants.ZERO_ADDRESS, // affiliates referrer - "0x", // loanDataBytes (only required with ether) - { from: trader } - ); - }); - - it("User Margin Trade with Affiliate runs correctly", async () => { - // expected in x * 10**18 where x is the actual leverage (2, 3, 4, or 5) - const leverageAmount = web3.utils.toWei("3", "ether"); - // loan tokens sent to iToken contract to start Margin Trading - const loanTokenSent = web3.utils.toWei("20", "ether"); - // AUDIT: should the call be allowed from arbitrary address to set an affiliate in - // LoanTokenLogicStandard.marginTradeAffiliate? - const tx = await loanTokenV2.marginTradeAffiliate( - constants.ZERO_BYTES32, // loanId (0 for new loans) - leverageAmount, // leverageAmount - loanTokenSent, // loanTokenSent - 0, // no collateral token sent - WRBTC.address, // collateralTokenAddress - trader, - 0, // max slippage - referrer, // affiliates referrer - "0x", // loanDataBytes (only required with ether) - { from: trader } - ); - - expect(await sovryn.getUserNotFirstTradeFlag(trader), "userNotFirstTradeFlag has not been set to true").to.be.true; - expect(await sovryn.getAffiliatesUserReferrer(trader), "Incorrect User Affiliate Referrer set").to.be.equal(referrer); - - let event_name = "PayTradingFeeToAffiliate"; - let decode = decodeLogs(tx.receipt.rawLogs, Affiliates, event_name); - const referrerFee = await sovryn.affiliatesReferrerBalances(referrer, doc.address); - if (!decode.length) { - throw "Event PayTradingFeeToAffiliate is not fired properly"; - } - - tradingFeeAmount = decode[0].args["tradingFeeTokenAmount"]; - submittedToken = decode[0].args["token"]; - submittedReferrer = decode[0].args["referrer"]; - submittedTrader = decode[0].args["trader"]; - isHeld = decode[0].args["isHeld"]; - affiliatesFeePercentage = await sovryn.affiliateFeePercent(); - sovBonusAmountShouldBePaid = await feeds.queryReturn( - doc.address, - SUSD.address, - ((affiliatesFeePercentage * tradingFeeAmount) / Math.pow(10, 20)).toString() - ); - submittedSovBonusAmount = decode[0].args["sovBonusAmount"]; - submittedTokenBonusAmount = decode[0].args["tokenBonusAmount"]; - affiliateRewardsHeld = await sovryn.getAffiliateRewardsHeld(referrer); - expect(sovBonusAmountShouldBePaid.toString(), "Incorrect sov bonus amount calculation").to.be.equal(submittedSovBonusAmount); - expect(submittedToken).to.eql(doc.address); - expect(submittedReferrer).to.eql(referrer); - expect(submittedTrader).to.eql(trader); - expect(referrerFee.toString()).to.be.equal(submittedTokenBonusAmount.toString()); - // Since the minimum referrals to payout is set to 3, make sure the affiliateRewardsHeld is correct - expect(affiliateRewardsHeld.toString(), "SOV Bonus amount that stored in the affiliateRewardsHeld is incorrect").to.be.equal( - sovBonusAmountShouldBePaid.toString() - ); - expect(isHeld, "Token should not be sent since the minimum referrals to payout has not been fullfilled").to.eql(true); - - lockedSOVBalance = await lockedSOV.getLockedBalance(referrer); - expect(lockedSOVBalance.toString(), "Locked sov balance should be 0").to.eql(new BN(0).toString()); - - /*----------------------------------- Do a trade once again, and set the min referrals to payout to 1 -----------------------------------*/ - previousAffiliateRewardsHeld = affiliateRewardsHeld; - await sovryn.setMinReferralsToPayoutAffiliates(1); - const tx2 = await loanTokenV2.marginTradeAffiliate( - constants.ZERO_BYTES32, // loanId (0 for new loans) - leverageAmount, // leverageAmount - loanTokenSent, // loanTokenSent - 0, // no collateral token sent - WRBTC.address, // collateralTokenAddress - trader, - 0, // max slippage - referrer, // affiliates referrer - "0x", // loanDataBytes (only required with ether) - { from: trader } - ); - - decode = decodeLogs(tx2.receipt.rawLogs, Affiliates, event_name); - if (!decode.length) { - throw "Event PayTradingFeeToAffiliate is not fired properly"; - } - - // At this point, the remaining affiliate rewards in protocol + current rewards will be sent to the locked sov - tradingFeeAmount = decode[0].args["tradingFeeTokenAmount"]; - submittedToken = decode[0].args["token"]; - submittedReferrer = decode[0].args["referrer"]; - submittedTrader = decode[0].args["trader"]; - isHeld = decode[0].args["isHeld"]; - sovBonusAmountShouldBePaid = await feeds.queryReturn( - doc.address, - SUSD.address, - ((affiliatesFeePercentage * tradingFeeAmount) / Math.pow(10, 20)).toString() - ); - submittedSovBonusAmount = decode[0].args["sovBonusAmount"]; - paidSovBonusAmount = decode[0].args["sovBonusAmountPaid"]; - affiliateRewardsHeld = await sovryn.getAffiliateRewardsHeld(referrer); - - expect(sovBonusAmountShouldBePaid.toString(), "Incorrect sov bonus amount calculation").to.be.equal( - submittedSovBonusAmount.toString() - ); - expect(submittedToken).to.eql(doc.address); - expect(submittedReferrer).to.eql(referrer); - expect(submittedTrader).to.eql(trader); - - expect( - new BN(parseInt(previousAffiliateRewardsHeld)).add(new BN(parseInt(sovBonusAmountShouldBePaid))).toString(), - "Incorrect sov bonus amount calculation" - ).to.be.equal(paidSovBonusAmount.toString()); - expect(affiliateRewardsHeld.toString(), "Affiliates rewards should be 0 after rewards is sent").to.eql(new BN(0).toString()); - expect(isHeld, "Token should be sent since the minimum referrals to payout has not been fullfilled").to.eql(false); - - lockedSOVBalance = await lockedSOV.getLockedBalance(referrer); - expect(lockedSOVBalance.toString(), "Locked sov balance should be 0").to.eql(paidSovBonusAmount.toString()); - }); - - it("User Margin Trade with Affiliate runs correctly when minimum referrals set to 1", async () => { - await sovryn.setMinReferralsToPayoutAffiliates(1); - // expected in x * 10**18 where x is the actual leverage (2, 3, 4, or 5) - const leverageAmount = web3.utils.toWei("3", "ether"); - // loan tokens sent to iToken contract to start Margin Trading - const loanTokenSent = web3.utils.toWei("20", "ether"); - // AUDIT: should the call be allowed from arbitrary address to set an affiliate in - // LoanTokenLogicStandard.marginTradeAffiliate? - const tx = await loanTokenV2.marginTradeAffiliate( - constants.ZERO_BYTES32, // loanId (0 for new loans) - leverageAmount, // leverageAmount - loanTokenSent, // loanTokenSent - 0, // no collateral token sent - WRBTC.address, // collateralTokenAddress - trader, - 0, // max slippage - referrer, // affiliates referrer - "0x", // loanDataBytes (only required with ether) - { from: trader } - ); - - expect(await sovryn.getUserNotFirstTradeFlag(trader), "userNotFirstTradeFlag has not been set to true").to.be.true; - expect(await sovryn.getAffiliatesUserReferrer(trader), "Incorrect User Affiliate Referrer set").to.be.equal(referrer); - - const event_name = "PayTradingFeeToAffiliate"; - const decode = decodeLogs(tx.receipt.rawLogs, Affiliates, event_name); - const referrerFee = await sovryn.affiliatesReferrerBalances(referrer, doc.address); - if (!decode.length) { - throw "Event PayTradingFeeToAffiliate is not fired properly"; - } - - tradingFeeAmount = decode[0].args["tradingFeeTokenAmount"]; - submittedToken = decode[0].args["token"]; - submittedReferrer = decode[0].args["referrer"]; - submittedTrader = decode[0].args["trader"]; - isHeld = decode[0].args["isHeld"]; - affiliatesFeePercentage = await sovryn.affiliateFeePercent(); - sovBonusAmountShouldBePaid = await feeds.queryReturn( - doc.address, - SUSD.address, - ((affiliatesFeePercentage * tradingFeeAmount) / Math.pow(10, 20)).toString() - ); - submittedSovBonusAmount = decode[0].args["sovBonusAmountPaid"]; - submittedTokenBonusAmount = decode[0].args["tokenBonusAmount"]; - affiliateRewardsHeld = await sovryn.getAffiliateRewardsHeld(referrer); - expect(sovBonusAmountShouldBePaid.toString(), "Incorrect sov bonus amount calculation").to.be.equal(submittedSovBonusAmount); - expect(submittedToken).to.eql(doc.address); - expect(submittedReferrer).to.eql(referrer); - expect(submittedTrader).to.eql(trader); - expect(referrerFee.toString()).to.be.equal(submittedTokenBonusAmount.toString()); - // Since the minimum referrals to payout is set to 1, make sure the affiliateRewardsHeld is correct - expect(affiliateRewardsHeld.toString(), "SOV Bonus amount that stored in the affiliateRewardsHeld is incorrect").to.be.equal( - new BN(0).toString() - ); - expect(isHeld, "Token should be sent since the minimum referrals to payout has not been fullfilled").to.eql(false); - - lockedSOVBalance = await lockedSOV.getLockedBalance(referrer); - expect(lockedSOVBalance.toString(), "Locked sov balance is not matched").to.eql(sovBonusAmountShouldBePaid.toString()); - }); - - it("PayTradingFeeToAffiliateFail event should be fired in case lock sov reverted", async () => { - // deploy lockedSOVFailedMockup and set to protocol - await sovryn.setLockedSOVAddress((await LockedSOVFailedMockup.new(SUSD.address, [owner])).address); - - await sovryn.setMinReferralsToPayoutAffiliates(1); - // expected in x * 10**18 where x is the actual leverage (2, 3, 4, or 5) - const leverageAmount = web3.utils.toWei("3", "ether"); - // loan tokens sent to iToken contract to start Margin Trading - const loanTokenSent = web3.utils.toWei("20", "ether"); - // AUDIT: should the call be allowed from arbitrary address to set an affiliate in - // LoanTokenLogicStandard.marginTradeAffiliate? - const tx = await loanTokenV2.marginTradeAffiliate( - constants.ZERO_BYTES32, // loanId (0 for new loans) - leverageAmount, // leverageAmount - loanTokenSent, // loanTokenSent - 0, // no collateral token sent - WRBTC.address, // collateralTokenAddress - trader, - 0, // max slippage - referrer, // affiliates referrer - "0x", // loanDataBytes (only required with ether) - { from: trader } - ); - - const event_failed_name = "PayTradingFeeToAffiliateFail"; - const decodeFailed = decodeLogs(tx.receipt.rawLogs, Affiliates, event_failed_name); - if (!decodeFailed.length) { - throw "Event PayTradingFeeToAffiliateFail is not fired properly"; - } - - // We need to make sure that the fail log event is fired - tradingFeeAmount = decodeFailed[0].args["tradingFeeTokenAmount"]; - submittedToken = decodeFailed[0].args["token"]; - submittedReferrer = decodeFailed[0].args["referrer"]; - submittedTrader = decodeFailed[0].args["trader"]; - sovBonusAmount = decodeFailed[0].args["sovBonusAmount"]; - affiliatesFeePercentage = await sovryn.affiliateFeePercent(); - sovBonusAmountShouldBePaid = await feeds.queryReturn( - doc.address, - SUSD.address, - ((affiliatesFeePercentage * tradingFeeAmount) / Math.pow(10, 20)).toString() - ); - expect(await sovryn.getUserNotFirstTradeFlag(trader), "userNotFirstTradeFlag has not been set to true").to.be.true; - expect(await sovryn.getAffiliatesUserReferrer(trader), "Incorrect User Affiliate Referrer set").to.be.equal(referrer); - expect(sovBonusAmountShouldBePaid.toString(), "Incorrect sov bonus amount calculation").to.be.equal(sovBonusAmount); - expect(submittedToken).to.eql(doc.address); - expect(submittedReferrer).to.eql(referrer); - expect(submittedTrader).to.eql(trader); - }); - - it("Only the first trade users can be assigned Affiliates Referrer", async () => { - let tx = await loanTokenV2.setUserNotFirstTradeFlag(trader); // can be called only from loan tokens pool addresses - await expectEvent.inTransaction(tx.receipt.rawLogs[0].transactionHash, Affiliates, "SetUserNotFirstTradeFlag", { - user: trader, - }); - tx = await loanTokenV2.setAffiliatesReferrer(trader, referrer); // can be called only from loan tokens pool addresses - expect(await sovryn.getAffiliatesUserReferrer(trader), "Referrer cannot be set for non first trade users").to.be.equal( - constants.ZERO_ADDRESS - ); - - await expectEvent.inTransaction(tx.receipt.rawLogs[0].transactionHash, Affiliates, "SetAffiliatesReferrerFail", { - user: trader, - referrer: referrer, - }); - }); - - it("Affiliates Referrer cannot be changed once set", async () => { - const tx = await loanTokenV2.setAffiliatesReferrer(trader, referrer); // can be called only from loan tokens pool addresses - await expectEvent.inTransaction(tx.receipt.rawLogs[0].transactionHash, Affiliates, "SetAffiliatesReferrer", { - user: trader, - referrer: referrer, - }); - await loanTokenV2.setAffiliatesReferrer(trader, account1); // try to replace referrer - expect(await sovryn.getAffiliatesUserReferrer(trader), "Affiliates Referrer is set once and cannot be changed").to.be.equal( - referrer - ); - }); - - it("Users cannot be self-referrers", async () => { - // try to replace referrer - await loanTokenV2.setAffiliatesReferrer(trader, trader); // can be called only from loan tokens pool addresses - expect(await sovryn.getAffiliatesUserReferrer(trader), "Affiliates Referrer is set once and cannot be changed").to.be.equal( - constants.ZERO_ADDRESS - ); - }); - - it("First users Margin trading without affiliate referrer sets userNotFirstTradingFlag = true", async () => { - const leverageAmount = web3.utils.toWei("3", "ether"); - const loanTokenSent = web3.utils.toWei("20", "ether"); - await loanTokenV2.marginTrade( - constants.ZERO_BYTES32, // loanId (0 for new loans) - leverageAmount, // leverageAmount - loanTokenSent, // loanTokenSent - 0, // no collateral token sent - WRBTC.address, // collateralTokenAddress - trader, // trader, - 0, // max slippage - // referrer, // affiliates referrer - "0x", // loanDataBytes (only required with ether) - { from: trader } - ); - expect(await sovryn.getUserNotFirstTradeFlag(trader), "sovryn.getUserNotFirstTradeFlag(trader) should be true").to.be.true; - }); - - it("Doesn't allow fallback function calls", async () => { - const affiliates = await Affiliates.new(); - await expectRevert( - affiliates.send(wei("0.0000000000000001", "ether")), - "fallback function is not payable and was called with value 100" - ); - await expectRevert(affiliates.sendTransaction({}), "Affiliates - fallback not allowed"); - }); - - it("payTradingFeeToAffiliatesReferrer() Should revert if not called by protocol", async () => { - await expectRevert( - sovryn.payTradingFeeToAffiliatesReferrer(referrer, trader, SUSD.address, wei("1", "gwei")), - "Affiliates: not authorized" - ); - }); - - it("setAffiliatesReferrer() should revert if not called by loanPool", async () => { - await expectRevert(sovryn.setAffiliatesReferrer(accounts[0], referrer), "Affiliates: not authorized"); - }); - - it("setUserNotFirstTradeFlag() should revert if not called by loanPool", async () => { - await expectRevert(sovryn.setUserNotFirstTradeFlag(referrer), "Affiliates: not authorized"); - }); - - it("Test referralList when set Affiliates referrer", async () => { - let tx = await loanTokenV2.setAffiliatesReferrer(trader, referrer); // can be called only from loan tokens pool addresses - await expectEvent.inTransaction(tx.receipt.rawLogs[0].transactionHash, Affiliates, "SetAffiliatesReferrer", { - user: trader, - referrer: referrer, - }); - let refList = await sovryn.getReferralsList(referrer); - expect(refList[0], "Referrals list is not matched").to.be.equal(trader); - - tx = await loanTokenV2.setAffiliatesReferrer(accounts[9], referrer); - refList = await sovryn.getReferralsList(referrer); - expect(refList.length).to.be.equal(2); - }); - - it("Test referralList won't be duplicate when set Affiliates referrer twice", async () => { - let tx = await loanTokenV2.setAffiliatesReferrer(trader, referrer); // can be called only from loan tokens pool addresses - await expectEvent.inTransaction(tx.receipt.rawLogs[0].transactionHash, Affiliates, "SetAffiliatesReferrer", { - user: trader, - referrer: referrer, - }); - let refList = await sovryn.getReferralsList(referrer); - expect(refList[0], "Referrals list is not matched").to.be.equal(trader); - - tx = await loanTokenV2.setAffiliatesReferrer(trader, referrer); // can be called only from loan tokens pool addresses - refList = await sovryn.getReferralsList(referrer); - expect(refList.length).to.be.equal(1); - }); - - it("Withdraw all token will revert if receiver is zero address", async () => { - await expectRevert( - sovryn.withdrawAllAffiliatesReferrerTokenFees(constants.ZERO_ADDRESS, { from: referrer }), - "Affiliates: cannot withdraw to zero address" - ); - }); - - it("Withdraw all token will revert if minimum affiliates to payout is not fulfilled", async () => { - await expectRevert( - sovryn.withdrawAllAffiliatesReferrerTokenFees(referrer, { from: referrer }), - "Your referrals has not reached the minimum request" - ); - }); - - it("Affiliates referrer withdraw fees should revert because of the minimum request is not fullfilled", async () => { - const leverageAmount = web3.utils.toWei("3", "ether"); - // loan tokens sent to iToken contract to start Margin Trading - const loanTokenSent = web3.utils.toWei("20", "ether"); - // AUDIT: should the call be allowed from arbitrary address to set an affiliate in - // LoanTokenLogicStandard.marginTradeAffiliate? - await loanTokenV2.marginTradeAffiliate( - constants.ZERO_BYTES32, // loanId (0 for new loans) - leverageAmount, // leverageAmount - loanTokenSent, // loanTokenSent - 0, // no collateral token sent - WRBTC.address, // collateralTokenAddress - trader, - 0, // max slippage - referrer, // affiliates referrer - "0x", // loanDataBytes (only required with ether) - { from: trader } - ); - - // WITHDRAW AFFILIATE FEES - // FAIL - const referrerFee = await sovryn.affiliatesReferrerBalances(referrer, doc.address); - await expectRevert( - sovryn.withdrawAffiliatesReferrerTokenFees(doc.address, constants.ZERO_ADDRESS, referrerFee, { from: referrer }), - "Affiliates: cannot withdraw to zero address" - ); - await expectRevert( - sovryn.withdrawAffiliatesReferrerTokenFees(doc.address, referrer, 0, { from: referrer }), - "Affiliates: cannot withdraw zero amount" - ); - - await expectRevert( - sovryn.withdrawAffiliatesReferrerTokenFees(doc.address, referrer, referrerFee, { from: referrer }), - "Your referrals has not reached the minimum request" - ); - }); - - it("Affiliates Referrer withdraw fees in two tokens works correctly with min referrals to payout is 0", async () => { - await sovryn.setMinReferralsToPayoutAffiliates(1); - const leverageAmount = web3.utils.toWei("3", "ether"); - // loan tokens sent to iToken contract to start Margin Trading - const loanTokenSent = web3.utils.toWei("20", "ether"); - - const leverageAmount2 = web3.utils.toWei("3", "ether"); - // loan tokens sent to iToken contract to start Margin Trading - const loanTokenSent2 = web3.utils.toWei("20", "ether"); - - // add another pair - // loanTokenLogic = await MockLoanTokenLogic.new(); - // WRBTC = await WRBTC.new(); - eur = await TestToken.new("euro on chain 2", "EUR", 18, wei("20000", "ether")); - const loanToken2 = await LoanToken.new(owner, loanTokenLogic.address, sovryn.address, WRBTC.address); - await loanToken2.initialize(eur.address, "SEUR", "SEUR"); - - /** Initialize the loan token logic proxy */ - loanToken2V2 = await ILoanTokenLogicProxy.at(loanToken2.address); - await loanToken2V2.setBeaconAddress(loanTokenLogicBeacon.address); - - // loanToken2V2 = await MockLoanTokenLogic.at(loanToken2.address); //mocked for ad-hoc logic for isolated testing - loanToken2V2 = await ILoanTokenModulesMock.at(loanToken2.address); //mocked for ad-hoc logic for isolated testing - const loanTokenAddress2 = await loanToken2.loanTokenAddress(); - if (owner == (await sovryn.owner())) { - await sovryn.setLoanPool([loanToken2V2.address], [loanTokenAddress2]); - } - await feeds.setRates(eur.address, WRBTC.address, wei("0.01", "ether")); - await sovryn.setSupportedTokens([eur.address], [true]); - - { - /** + } + params = [ + "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object + false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans + owner, // address owner; // owner of this object + doc.address, // address loanToken; // the token being loaned + WRBTC.address, // address collateralToken; // the required collateral token + wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin + wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value + 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) + ]; + + // await loanTokenV2.setupLoanParams([params], true); + await loanTokenV2.setupLoanParams([params], false); + + // setting up interest rates + const baseRate = wei("1", "ether"); + const rateMultiplier = wei("20.25", "ether"); + const targetLevel = wei("80", "ether"); + const kinkLevel = wei("90", "ether"); + const maxScaleRate = wei("100", "ether"); + await loanTokenV2.setDemandCurve( + baseRate, + rateMultiplier, + baseRate, + rateMultiplier, + targetLevel, + kinkLevel, + maxScaleRate + ); + + // GIVING SOME DOC tokens to loanToken so that we can borrow from loanToken + await doc.transfer(loanTokenV2.address, wei("500", "ether")); + await doc.transfer(trader, wei("100", "ether")); + // trader approves to LoanToken loan amount for trading + await doc.approve(loanToken.address, web3.utils.toWei("100", "ether"), { from: trader }); + // Giving some testRbtc to sovrynAddress (by minting some testRbtc),so that it can open position in wRBTC. + await WRBTC.mint(sovryn.address, wei("500", "ether")); + + // Giving some SOV Token to sovrynAddress (For affiliates rewards purposes) + await SUSD.mint(sovryn.address, wei("500", "ether")); + } + + before(async () => { + [owner, trader, referrer, account1, account2, ...accounts] = accounts; + }); + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + it("Should not be able to set the affiliateFeePercent more than 100%", async () => { + const valueExperiment = wei("101", "ether"); + await expectRevert(sovryn.setAffiliateFeePercent(0, { from: referrer }), "unauthorized"); + await expectRevert(sovryn.setAffiliateFeePercent(valueExperiment), "value too high"); + }); + + it("Test coverage: call marginTradeAffiliate w/ affiliateReferrer = address(0)", async () => { + // expected in x * 10**18 where x is the actual leverage (2, 3, 4, or 5) + const leverageAmount = web3.utils.toWei("3", "ether"); + // loan tokens sent to iToken contract to start Margin Trading + const loanTokenSent = web3.utils.toWei("20", "ether"); + // AUDIT: should the call be allowed from arbitrary address to set an affiliate in + // LoanTokenLogicStandard.marginTradeAffiliate? + const tx = await loanTokenV2.marginTradeAffiliate( + constants.ZERO_BYTES32, // loanId (0 for new loans) + leverageAmount, // leverageAmount + loanTokenSent, // loanTokenSent + 0, // no collateral token sent + WRBTC.address, // collateralTokenAddress + trader, + 0, // max slippage + constants.ZERO_ADDRESS, // affiliates referrer + "0x", // loanDataBytes (only required with ether) + { from: trader } + ); + }); + + it("User Margin Trade with Affiliate runs correctly", async () => { + // expected in x * 10**18 where x is the actual leverage (2, 3, 4, or 5) + const leverageAmount = web3.utils.toWei("3", "ether"); + // loan tokens sent to iToken contract to start Margin Trading + const loanTokenSent = web3.utils.toWei("20", "ether"); + // AUDIT: should the call be allowed from arbitrary address to set an affiliate in + // LoanTokenLogicStandard.marginTradeAffiliate? + const tx = await loanTokenV2.marginTradeAffiliate( + constants.ZERO_BYTES32, // loanId (0 for new loans) + leverageAmount, // leverageAmount + loanTokenSent, // loanTokenSent + 0, // no collateral token sent + WRBTC.address, // collateralTokenAddress + trader, + 0, // max slippage + referrer, // affiliates referrer + "0x", // loanDataBytes (only required with ether) + { from: trader } + ); + + expect( + await sovryn.getUserNotFirstTradeFlag(trader), + "userNotFirstTradeFlag has not been set to true" + ).to.be.true; + expect( + await sovryn.getAffiliatesUserReferrer(trader), + "Incorrect User Affiliate Referrer set" + ).to.be.equal(referrer); + + let event_name = "PayTradingFeeToAffiliate"; + let decode = decodeLogs(tx.receipt.rawLogs, Affiliates, event_name); + const referrerFee = await sovryn.affiliatesReferrerBalances(referrer, doc.address); + if (!decode.length) { + throw "Event PayTradingFeeToAffiliate is not fired properly"; + } + + tradingFeeAmount = decode[0].args["tradingFeeTokenAmount"]; + submittedToken = decode[0].args["token"]; + submittedReferrer = decode[0].args["referrer"]; + submittedTrader = decode[0].args["trader"]; + isHeld = decode[0].args["isHeld"]; + affiliatesFeePercentage = await sovryn.affiliateFeePercent(); + sovBonusAmountShouldBePaid = await feeds.queryReturn( + doc.address, + SUSD.address, + ((affiliatesFeePercentage * tradingFeeAmount) / Math.pow(10, 20)).toString() + ); + submittedSovBonusAmount = decode[0].args["sovBonusAmount"]; + submittedTokenBonusAmount = decode[0].args["tokenBonusAmount"]; + affiliateRewardsHeld = await sovryn.getAffiliateRewardsHeld(referrer); + expect( + sovBonusAmountShouldBePaid.toString(), + "Incorrect sov bonus amount calculation" + ).to.be.equal(submittedSovBonusAmount); + expect(submittedToken).to.eql(doc.address); + expect(submittedReferrer).to.eql(referrer); + expect(submittedTrader).to.eql(trader); + expect(referrerFee.toString()).to.be.equal(submittedTokenBonusAmount.toString()); + // Since the minimum referrals to payout is set to 3, make sure the affiliateRewardsHeld is correct + expect( + affiliateRewardsHeld.toString(), + "SOV Bonus amount that stored in the affiliateRewardsHeld is incorrect" + ).to.be.equal(sovBonusAmountShouldBePaid.toString()); + expect( + isHeld, + "Token should not be sent since the minimum referrals to payout has not been fullfilled" + ).to.eql(true); + + lockedSOVBalance = await lockedSOV.getLockedBalance(referrer); + expect(lockedSOVBalance.toString(), "Locked sov balance should be 0").to.eql( + new BN(0).toString() + ); + + /*----------------------------------- Do a trade once again, and set the min referrals to payout to 1 -----------------------------------*/ + previousAffiliateRewardsHeld = affiliateRewardsHeld; + await sovryn.setMinReferralsToPayoutAffiliates(1); + const tx2 = await loanTokenV2.marginTradeAffiliate( + constants.ZERO_BYTES32, // loanId (0 for new loans) + leverageAmount, // leverageAmount + loanTokenSent, // loanTokenSent + 0, // no collateral token sent + WRBTC.address, // collateralTokenAddress + trader, + 0, // max slippage + referrer, // affiliates referrer + "0x", // loanDataBytes (only required with ether) + { from: trader } + ); + + decode = decodeLogs(tx2.receipt.rawLogs, Affiliates, event_name); + if (!decode.length) { + throw "Event PayTradingFeeToAffiliate is not fired properly"; + } + + // At this point, the remaining affiliate rewards in protocol + current rewards will be sent to the locked sov + tradingFeeAmount = decode[0].args["tradingFeeTokenAmount"]; + submittedToken = decode[0].args["token"]; + submittedReferrer = decode[0].args["referrer"]; + submittedTrader = decode[0].args["trader"]; + isHeld = decode[0].args["isHeld"]; + sovBonusAmountShouldBePaid = await feeds.queryReturn( + doc.address, + SUSD.address, + ((affiliatesFeePercentage * tradingFeeAmount) / Math.pow(10, 20)).toString() + ); + submittedSovBonusAmount = decode[0].args["sovBonusAmount"]; + paidSovBonusAmount = decode[0].args["sovBonusAmountPaid"]; + affiliateRewardsHeld = await sovryn.getAffiliateRewardsHeld(referrer); + + expect( + sovBonusAmountShouldBePaid.toString(), + "Incorrect sov bonus amount calculation" + ).to.be.equal(submittedSovBonusAmount.toString()); + expect(submittedToken).to.eql(doc.address); + expect(submittedReferrer).to.eql(referrer); + expect(submittedTrader).to.eql(trader); + + expect( + new BN(parseInt(previousAffiliateRewardsHeld)) + .add(new BN(parseInt(sovBonusAmountShouldBePaid))) + .toString(), + "Incorrect sov bonus amount calculation" + ).to.be.equal(paidSovBonusAmount.toString()); + expect( + affiliateRewardsHeld.toString(), + "Affiliates rewards should be 0 after rewards is sent" + ).to.eql(new BN(0).toString()); + expect( + isHeld, + "Token should be sent since the minimum referrals to payout has not been fullfilled" + ).to.eql(false); + + lockedSOVBalance = await lockedSOV.getLockedBalance(referrer); + expect(lockedSOVBalance.toString(), "Locked sov balance should be 0").to.eql( + paidSovBonusAmount.toString() + ); + }); + + it("User Margin Trade with Affiliate runs correctly when minimum referrals set to 1", async () => { + await sovryn.setMinReferralsToPayoutAffiliates(1); + // expected in x * 10**18 where x is the actual leverage (2, 3, 4, or 5) + const leverageAmount = web3.utils.toWei("3", "ether"); + // loan tokens sent to iToken contract to start Margin Trading + const loanTokenSent = web3.utils.toWei("20", "ether"); + // AUDIT: should the call be allowed from arbitrary address to set an affiliate in + // LoanTokenLogicStandard.marginTradeAffiliate? + const tx = await loanTokenV2.marginTradeAffiliate( + constants.ZERO_BYTES32, // loanId (0 for new loans) + leverageAmount, // leverageAmount + loanTokenSent, // loanTokenSent + 0, // no collateral token sent + WRBTC.address, // collateralTokenAddress + trader, + 0, // max slippage + referrer, // affiliates referrer + "0x", // loanDataBytes (only required with ether) + { from: trader } + ); + + expect( + await sovryn.getUserNotFirstTradeFlag(trader), + "userNotFirstTradeFlag has not been set to true" + ).to.be.true; + expect( + await sovryn.getAffiliatesUserReferrer(trader), + "Incorrect User Affiliate Referrer set" + ).to.be.equal(referrer); + + const event_name = "PayTradingFeeToAffiliate"; + const decode = decodeLogs(tx.receipt.rawLogs, Affiliates, event_name); + const referrerFee = await sovryn.affiliatesReferrerBalances(referrer, doc.address); + if (!decode.length) { + throw "Event PayTradingFeeToAffiliate is not fired properly"; + } + + tradingFeeAmount = decode[0].args["tradingFeeTokenAmount"]; + submittedToken = decode[0].args["token"]; + submittedReferrer = decode[0].args["referrer"]; + submittedTrader = decode[0].args["trader"]; + isHeld = decode[0].args["isHeld"]; + affiliatesFeePercentage = await sovryn.affiliateFeePercent(); + sovBonusAmountShouldBePaid = await feeds.queryReturn( + doc.address, + SUSD.address, + ((affiliatesFeePercentage * tradingFeeAmount) / Math.pow(10, 20)).toString() + ); + submittedSovBonusAmount = decode[0].args["sovBonusAmountPaid"]; + submittedTokenBonusAmount = decode[0].args["tokenBonusAmount"]; + affiliateRewardsHeld = await sovryn.getAffiliateRewardsHeld(referrer); + expect( + sovBonusAmountShouldBePaid.toString(), + "Incorrect sov bonus amount calculation" + ).to.be.equal(submittedSovBonusAmount); + expect(submittedToken).to.eql(doc.address); + expect(submittedReferrer).to.eql(referrer); + expect(submittedTrader).to.eql(trader); + expect(referrerFee.toString()).to.be.equal(submittedTokenBonusAmount.toString()); + // Since the minimum referrals to payout is set to 1, make sure the affiliateRewardsHeld is correct + expect( + affiliateRewardsHeld.toString(), + "SOV Bonus amount that stored in the affiliateRewardsHeld is incorrect" + ).to.be.equal(new BN(0).toString()); + expect( + isHeld, + "Token should be sent since the minimum referrals to payout has not been fullfilled" + ).to.eql(false); + + lockedSOVBalance = await lockedSOV.getLockedBalance(referrer); + expect(lockedSOVBalance.toString(), "Locked sov balance is not matched").to.eql( + sovBonusAmountShouldBePaid.toString() + ); + }); + + it("PayTradingFeeToAffiliateFail event should be fired in case lock sov reverted", async () => { + // deploy lockedSOVFailedMockup and set to protocol + await sovryn.setLockedSOVAddress( + ( + await LockedSOVFailedMockup.new(SUSD.address, [owner]) + ).address + ); + + await sovryn.setMinReferralsToPayoutAffiliates(1); + // expected in x * 10**18 where x is the actual leverage (2, 3, 4, or 5) + const leverageAmount = web3.utils.toWei("3", "ether"); + // loan tokens sent to iToken contract to start Margin Trading + const loanTokenSent = web3.utils.toWei("20", "ether"); + // AUDIT: should the call be allowed from arbitrary address to set an affiliate in + // LoanTokenLogicStandard.marginTradeAffiliate? + const tx = await loanTokenV2.marginTradeAffiliate( + constants.ZERO_BYTES32, // loanId (0 for new loans) + leverageAmount, // leverageAmount + loanTokenSent, // loanTokenSent + 0, // no collateral token sent + WRBTC.address, // collateralTokenAddress + trader, + 0, // max slippage + referrer, // affiliates referrer + "0x", // loanDataBytes (only required with ether) + { from: trader } + ); + + const event_failed_name = "PayTradingFeeToAffiliateFail"; + const decodeFailed = decodeLogs(tx.receipt.rawLogs, Affiliates, event_failed_name); + if (!decodeFailed.length) { + throw "Event PayTradingFeeToAffiliateFail is not fired properly"; + } + + // We need to make sure that the fail log event is fired + tradingFeeAmount = decodeFailed[0].args["tradingFeeTokenAmount"]; + submittedToken = decodeFailed[0].args["token"]; + submittedReferrer = decodeFailed[0].args["referrer"]; + submittedTrader = decodeFailed[0].args["trader"]; + sovBonusAmount = decodeFailed[0].args["sovBonusAmount"]; + affiliatesFeePercentage = await sovryn.affiliateFeePercent(); + sovBonusAmountShouldBePaid = await feeds.queryReturn( + doc.address, + SUSD.address, + ((affiliatesFeePercentage * tradingFeeAmount) / Math.pow(10, 20)).toString() + ); + expect( + await sovryn.getUserNotFirstTradeFlag(trader), + "userNotFirstTradeFlag has not been set to true" + ).to.be.true; + expect( + await sovryn.getAffiliatesUserReferrer(trader), + "Incorrect User Affiliate Referrer set" + ).to.be.equal(referrer); + expect( + sovBonusAmountShouldBePaid.toString(), + "Incorrect sov bonus amount calculation" + ).to.be.equal(sovBonusAmount); + expect(submittedToken).to.eql(doc.address); + expect(submittedReferrer).to.eql(referrer); + expect(submittedTrader).to.eql(trader); + }); + + it("Only the first trade users can be assigned Affiliates Referrer", async () => { + let tx = await loanTokenV2.setUserNotFirstTradeFlag(trader); // can be called only from loan tokens pool addresses + await expectEvent.inTransaction( + tx.receipt.rawLogs[0].transactionHash, + Affiliates, + "SetUserNotFirstTradeFlag", + { + user: trader, + } + ); + tx = await loanTokenV2.setAffiliatesReferrer(trader, referrer); // can be called only from loan tokens pool addresses + expect( + await sovryn.getAffiliatesUserReferrer(trader), + "Referrer cannot be set for non first trade users" + ).to.be.equal(constants.ZERO_ADDRESS); + + await expectEvent.inTransaction( + tx.receipt.rawLogs[0].transactionHash, + Affiliates, + "SetAffiliatesReferrerFail", + { + user: trader, + referrer: referrer, + } + ); + }); + + it("Affiliates Referrer cannot be changed once set", async () => { + const tx = await loanTokenV2.setAffiliatesReferrer(trader, referrer); // can be called only from loan tokens pool addresses + await expectEvent.inTransaction( + tx.receipt.rawLogs[0].transactionHash, + Affiliates, + "SetAffiliatesReferrer", + { + user: trader, + referrer: referrer, + } + ); + await loanTokenV2.setAffiliatesReferrer(trader, account1); // try to replace referrer + expect( + await sovryn.getAffiliatesUserReferrer(trader), + "Affiliates Referrer is set once and cannot be changed" + ).to.be.equal(referrer); + }); + + it("Users cannot be self-referrers", async () => { + // try to replace referrer + await loanTokenV2.setAffiliatesReferrer(trader, trader); // can be called only from loan tokens pool addresses + expect( + await sovryn.getAffiliatesUserReferrer(trader), + "Affiliates Referrer is set once and cannot be changed" + ).to.be.equal(constants.ZERO_ADDRESS); + }); + + it("First users Margin trading without affiliate referrer sets userNotFirstTradingFlag = true", async () => { + const leverageAmount = web3.utils.toWei("3", "ether"); + const loanTokenSent = web3.utils.toWei("20", "ether"); + await loanTokenV2.marginTrade( + constants.ZERO_BYTES32, // loanId (0 for new loans) + leverageAmount, // leverageAmount + loanTokenSent, // loanTokenSent + 0, // no collateral token sent + WRBTC.address, // collateralTokenAddress + trader, // trader, + 0, // max slippage + // referrer, // affiliates referrer + "0x", // loanDataBytes (only required with ether) + { from: trader } + ); + expect( + await sovryn.getUserNotFirstTradeFlag(trader), + "sovryn.getUserNotFirstTradeFlag(trader) should be true" + ).to.be.true; + }); + + it("Doesn't allow fallback function calls", async () => { + const affiliates = await Affiliates.new(); + await expectRevert( + affiliates.send(wei("0.0000000000000001", "ether")), + "fallback function is not payable and was called with value 100" + ); + await expectRevert(affiliates.sendTransaction({}), "Affiliates - fallback not allowed"); + }); + + it("payTradingFeeToAffiliatesReferrer() Should revert if not called by protocol", async () => { + await expectRevert( + sovryn.payTradingFeeToAffiliatesReferrer( + referrer, + trader, + SUSD.address, + wei("1", "gwei") + ), + "Affiliates: not authorized" + ); + }); + + it("setAffiliatesReferrer() should revert if not called by loanPool", async () => { + await expectRevert( + sovryn.setAffiliatesReferrer(accounts[0], referrer), + "Affiliates: not authorized" + ); + }); + + it("setUserNotFirstTradeFlag() should revert if not called by loanPool", async () => { + await expectRevert( + sovryn.setUserNotFirstTradeFlag(referrer), + "Affiliates: not authorized" + ); + }); + + it("Test referralList when set Affiliates referrer", async () => { + let tx = await loanTokenV2.setAffiliatesReferrer(trader, referrer); // can be called only from loan tokens pool addresses + await expectEvent.inTransaction( + tx.receipt.rawLogs[0].transactionHash, + Affiliates, + "SetAffiliatesReferrer", + { + user: trader, + referrer: referrer, + } + ); + let refList = await sovryn.getReferralsList(referrer); + expect(refList[0], "Referrals list is not matched").to.be.equal(trader); + + tx = await loanTokenV2.setAffiliatesReferrer(accounts[9], referrer); + refList = await sovryn.getReferralsList(referrer); + expect(refList.length).to.be.equal(2); + }); + + it("Test referralList won't be duplicate when set Affiliates referrer twice", async () => { + let tx = await loanTokenV2.setAffiliatesReferrer(trader, referrer); // can be called only from loan tokens pool addresses + await expectEvent.inTransaction( + tx.receipt.rawLogs[0].transactionHash, + Affiliates, + "SetAffiliatesReferrer", + { + user: trader, + referrer: referrer, + } + ); + let refList = await sovryn.getReferralsList(referrer); + expect(refList[0], "Referrals list is not matched").to.be.equal(trader); + + tx = await loanTokenV2.setAffiliatesReferrer(trader, referrer); // can be called only from loan tokens pool addresses + refList = await sovryn.getReferralsList(referrer); + expect(refList.length).to.be.equal(1); + }); + + it("Withdraw all token will revert if receiver is zero address", async () => { + await expectRevert( + sovryn.withdrawAllAffiliatesReferrerTokenFees(constants.ZERO_ADDRESS, { + from: referrer, + }), + "Affiliates: cannot withdraw to zero address" + ); + }); + + it("Withdraw all token will revert if minimum affiliates to payout is not fulfilled", async () => { + await expectRevert( + sovryn.withdrawAllAffiliatesReferrerTokenFees(referrer, { from: referrer }), + "Your referrals has not reached the minimum request" + ); + }); + + it("Affiliates referrer withdraw fees should revert because of the minimum request is not fullfilled", async () => { + const leverageAmount = web3.utils.toWei("3", "ether"); + // loan tokens sent to iToken contract to start Margin Trading + const loanTokenSent = web3.utils.toWei("20", "ether"); + // AUDIT: should the call be allowed from arbitrary address to set an affiliate in + // LoanTokenLogicStandard.marginTradeAffiliate? + await loanTokenV2.marginTradeAffiliate( + constants.ZERO_BYTES32, // loanId (0 for new loans) + leverageAmount, // leverageAmount + loanTokenSent, // loanTokenSent + 0, // no collateral token sent + WRBTC.address, // collateralTokenAddress + trader, + 0, // max slippage + referrer, // affiliates referrer + "0x", // loanDataBytes (only required with ether) + { from: trader } + ); + + // WITHDRAW AFFILIATE FEES + // FAIL + const referrerFee = await sovryn.affiliatesReferrerBalances(referrer, doc.address); + await expectRevert( + sovryn.withdrawAffiliatesReferrerTokenFees( + doc.address, + constants.ZERO_ADDRESS, + referrerFee, + { from: referrer } + ), + "Affiliates: cannot withdraw to zero address" + ); + await expectRevert( + sovryn.withdrawAffiliatesReferrerTokenFees(doc.address, referrer, 0, { + from: referrer, + }), + "Affiliates: cannot withdraw zero amount" + ); + + await expectRevert( + sovryn.withdrawAffiliatesReferrerTokenFees(doc.address, referrer, referrerFee, { + from: referrer, + }), + "Your referrals has not reached the minimum request" + ); + }); + + it("Affiliates Referrer withdraw fees in two tokens works correctly with min referrals to payout is 0", async () => { + await sovryn.setMinReferralsToPayoutAffiliates(1); + const leverageAmount = web3.utils.toWei("3", "ether"); + // loan tokens sent to iToken contract to start Margin Trading + const loanTokenSent = web3.utils.toWei("20", "ether"); + + const leverageAmount2 = web3.utils.toWei("3", "ether"); + // loan tokens sent to iToken contract to start Margin Trading + const loanTokenSent2 = web3.utils.toWei("20", "ether"); + + // add another pair + // loanTokenLogic = await MockLoanTokenLogic.new(); + // WRBTC = await WRBTC.new(); + eur = await TestToken.new("euro on chain 2", "EUR", 18, wei("20000", "ether")); + const loanToken2 = await LoanToken.new( + owner, + loanTokenLogic.address, + sovryn.address, + WRBTC.address + ); + await loanToken2.initialize(eur.address, "SEUR", "SEUR"); + + /** Initialize the loan token logic proxy */ + loanToken2V2 = await ILoanTokenLogicProxy.at(loanToken2.address); + await loanToken2V2.setBeaconAddress(loanTokenLogicBeacon.address); + + // loanToken2V2 = await MockLoanTokenLogic.at(loanToken2.address); //mocked for ad-hoc logic for isolated testing + loanToken2V2 = await ILoanTokenModulesMock.at(loanToken2.address); //mocked for ad-hoc logic for isolated testing + const loanTokenAddress2 = await loanToken2.loanTokenAddress(); + if (owner == (await sovryn.owner())) { + await sovryn.setLoanPool([loanToken2V2.address], [loanTokenAddress2]); + } + await feeds.setRates(eur.address, WRBTC.address, wei("0.01", "ether")); + await sovryn.setSupportedTokens([eur.address], [true]); + + { + /** struct LoanParams { bytes32 id; // id of loan params object bool active; // if false, this object has been disabled by the owner and can't be used for future loans @@ -660,273 +791,377 @@ contract("Affiliates", (accounts) => { uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) } */ - } - params = [ - "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object - false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans - owner, // address owner; // owner of this object - eur.address, // address loanToken; // the token being loaned - WRBTC.address, // address collateralToken; // the required collateral token - wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin - wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value - 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) - ]; - - // await loanTokenV2.setupLoanParams([params], true); - await loanToken2V2.setupLoanParams([params], false); - - // setting up interest rates - const baseRate = wei("1", "ether"); - const rateMultiplier = wei("20.25", "ether"); - const targetLevel = wei("80", "ether"); - const kinkLevel = wei("90", "ether"); - const maxScaleRate = wei("100", "ether"); - await loanToken2V2.setDemandCurve(baseRate, rateMultiplier, baseRate, rateMultiplier, targetLevel, kinkLevel, maxScaleRate); - - // GIVING SOME DOC tokens to loanToken so that we can borrow from loanToken - await eur.transfer(loanToken2V2.address, wei("500", "ether")); - await eur.transfer(trader, wei("20", "ether")); - // trader approves to LoanToken loan amount for trading - await eur.approve(loanToken2.address, web3.utils.toWei("20", "ether"), { from: trader }); - // Giving some more testRbtc to sovrynAddress (by minting some testRbtc),so that it can open position in wRBTC. - await WRBTC.mint(sovryn.address, wei("500", "ether")); - - // margin trade afiliate 2 tokens - await loanTokenV2.marginTradeAffiliate( - constants.ZERO_BYTES32, // loanId (0 for new loans) - leverageAmount, // leverageAmount - loanTokenSent, // loanTokenSent - 0, // no collateral token sent - WRBTC.address, // collateralTokenAddress - trader, - 0, // max slippage - referrer, // affiliates referrer - "0x", // loanDataBytes (only required with ether) - { from: trader } - ); - const referrerFee = await sovryn.affiliatesReferrerBalances(referrer, doc.address); - - await loanToken2V2.marginTradeAffiliate( - constants.ZERO_BYTES32, // loanId (0 for new loans) - leverageAmount2, // leverageAmount - loanTokenSent2, // loanTokenSent - 0, // no collateral token sent - WRBTC.address, // collateralTokenAddress - trader, - 0, // max slippage - referrer, // affiliates referrer - "0x", // loanDataBytes (only required with ether) - { from: trader } - ); - const referrerFee2 = await sovryn.affiliatesReferrerBalances(referrer, eur.address); - - // WITHDRAW AFFILIATE FEES - // FAIL - await expectRevert( - sovryn.withdrawAffiliatesReferrerTokenFees(doc.address, constants.ZERO_ADDRESS, referrerFee, { from: referrer }), - "Affiliates: cannot withdraw to zero address" - ); - await expectRevert( - sovryn.withdrawAffiliatesReferrerTokenFees(eur.address, constants.ZERO_ADDRESS, referrerFee2, { from: referrer }), - "Affiliates: cannot withdraw to zero address" - ); - await expectRevert( - sovryn.withdrawAffiliatesReferrerTokenFees(doc.address, referrer, 0, { from: referrer }), - "Affiliates: cannot withdraw zero amount" - ); - await expectRevert( - sovryn.withdrawAffiliatesReferrerTokenFees(eur.address, referrer, 0, { from: referrer }), - "Affiliates: cannot withdraw zero amount" - ); - - // SUCCESS - // partial withdraw - let tx; - tx = await sovryn.withdrawAffiliatesReferrerTokenFees(doc.address, referrer, referrerFee.divn(2), { from: referrer }); - const newReferrerFee = await sovryn.affiliatesReferrerBalances(referrer, doc.address); - expect(newReferrerFee, "Incorrect partial balance after half DoC withdraw").to.be.bignumber.equal(referrerFee.divn(2)); - - await expectEvent.inTransaction(tx.receipt.rawLogs[0].transactionHash, Affiliates, "WithdrawAffiliatesReferrerTokenFees", { - referrer: referrer, - receiver: referrer, - tokenAddress: doc.address, - amount: referrerFee.divn(2).toString(), - }); - - await sovryn.withdrawAffiliatesReferrerTokenFees(eur.address, referrer, referrerFee2.divn(4), { from: referrer }); - const newReferrerFee2 = await sovryn.affiliatesReferrerBalances(referrer, eur.address); - - expect(newReferrerFee2, "Incorrect partial balance after a quarter EUR withdraw").to.be.bignumber.equal( - referrerFee2.muln(3).divn(4) - ); - - let refBalances = await sovryn.getAffiliatesReferrerBalances(referrer); - - // complete withdraw - await sovryn.withdrawAffiliatesReferrerTokenFees(doc.address, referrer, newReferrerFee, { from: referrer }); - expect(await doc.balanceOf(referrer), "Incorrect DoC withdraw amount").to.be.bignumber.equal(referrerFee); - expect( - await sovryn.affiliatesReferrerBalances(referrer, doc.address), - "Affiliate Referrer's balance of DoC should be zero after withdrawal" - ).to.be.bignumber.equal(new BN(0)); - - // now the DoC token and it's balance for the referrer should be removed from the list - refBalances = await sovryn.getAffiliatesReferrerBalances(referrer); - expect(refBalances["referrerTokensList"][0]).to.eql(eur.address); - expect(refBalances["referrerTokensList"]).to.have.length(1); - expect(refBalances["referrerTokensBalances"][0]).to.be.bignumber.equal(new BN(Math.pow(10, 15)).muln(18)); - expect(refBalances["referrerTokensBalances"]).to.have.length(1); - - await sovryn.withdrawAffiliatesReferrerTokenFees(eur.address, referrer, newReferrerFee2, { from: referrer }); - expect(await eur.balanceOf(referrer), "Incorrect EUR withdraw amount").to.be.bignumber.equal(referrerFee2); - expect( - await sovryn.affiliatesReferrerBalances(referrer, eur.address), - "Affiliate Referrer's balance of EUR should be zero after withdrawal" - ).to.be.bignumber.equal(new BN(0)); - - refBalances = await sovryn.getAffiliatesReferrerBalances(referrer); - expect(refBalances["referrerTokensList"][0], "After withdrawal the token should be deleted from the referrers list").to.be - .undefined; - expect(refBalances["referrerTokensBalances"][0], "After withdrawal the token balances should be deleted from the referrers list").to - .be.undefined; - }); - - it("Affiliates Referrer withdraw all fees for two tokens works correctly with min referrals to payout is 0", async () => { - await sovryn.setMinReferralsToPayoutAffiliates(1); - const leverageAmount = web3.utils.toWei("3", "ether"); - // loan tokens sent to iToken contract to start Margin Trading - const loanTokenSent = web3.utils.toWei("20", "ether"); - - const leverageAmount2 = web3.utils.toWei("3", "ether"); - // loan tokens sent to iToken contract to start Margin Trading - const loanTokenSent2 = web3.utils.toWei("10", "ether"); - - // add another pair - // loanTokenLogic = await MockLoanTokenLogic.new(); - // WRBTC = await WRBTC.new(); - eur = await TestToken.new("euro on chain 2", "EUR", 18, wei("20000", "ether")); - const loanToken2 = await LoanToken.new(owner, loanTokenLogic.address, sovryn.address, WRBTC.address); - await loanToken2.initialize(eur.address, "SEUR", "SEUR"); - - /** Initialize the loan token logic proxy */ - loanToken2V2 = await ILoanTokenLogicProxy.at(loanToken2.address); - await loanToken2V2.setBeaconAddress(loanTokenLogicBeacon.address); - - // loanToken2V2 = await MockLoanTokenLogic.at(loanToken2.address); //mocked for ad-hoc logic for isolated testing - loanToken2V2 = await ILoanTokenModulesMock.at(loanToken2.address); //mocked for ad-hoc logic for isolated testing - const loanTokenAddress2 = await loanToken2.loanTokenAddress(); - if (owner == (await sovryn.owner())) { - await sovryn.setLoanPool([loanToken2V2.address], [loanTokenAddress2]); - } - await feeds.setRates(eur.address, WRBTC.address, wei("0.01", "ether")); - await sovryn.setSupportedTokens([eur.address], [true]); - - params = [ - "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object - false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans - owner, // address owner; // owner of this object - eur.address, // address loanToken; // the token being loaned - WRBTC.address, // address collateralToken; // the required collateral token - wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin - wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value - 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) - ]; - - // await loanTokenV2.setupLoanParams([params], true); - await loanToken2V2.setupLoanParams([params], false); - - // setting up interest rates - const baseRate = wei("1", "ether"); - const rateMultiplier = wei("20.25", "ether"); - const targetLevel = wei("80", "ether"); - const kinkLevel = wei("90", "ether"); - const maxScaleRate = wei("100", "ether"); - await loanToken2V2.setDemandCurve(baseRate, rateMultiplier, baseRate, rateMultiplier, targetLevel, kinkLevel, maxScaleRate); - - // GIVING SOME DOC tokens to loanToken so that we can borrow from loanToken - await eur.transfer(loanToken2V2.address, wei("500", "ether")); - await eur.transfer(trader, wei("20", "ether")); - // trader approves to LoanToken loan amount for trading - await eur.approve(loanToken2.address, web3.utils.toWei("20", "ether"), { from: trader }); - // Giving some more testRbtc to sovrynAddress (by minting some testRbtc),so that it can open position in wRBTC. - await WRBTC.mint(sovryn.address, wei("500", "ether")); - - // margin trade afiliate 2 tokens - await loanTokenV2.marginTradeAffiliate( - constants.ZERO_BYTES32, // loanId (0 for new loans) - leverageAmount, // leverageAmount - loanTokenSent, // loanTokenSent - 0, // no collateral token sent - WRBTC.address, // collateralTokenAddress - trader, - 0, // max slippage - referrer, // affiliates referrer - "0x", // loanDataBytes (only required with ether) - { from: trader } - ); - const referrerFee = await sovryn.affiliatesReferrerBalances(referrer, doc.address); - - await loanToken2V2.marginTradeAffiliate( - constants.ZERO_BYTES32, // loanId (0 for new loans) - leverageAmount2, // leverageAmount - loanTokenSent2, // loanTokenSent - 0, // no collateral token sent - WRBTC.address, // collateralTokenAddress - trader, - 0, // max slippage - referrer, // affiliates referrer - "0x", // loanDataBytes (only required with ether) - { from: trader } - ); - const referrerFee2 = await sovryn.affiliatesReferrerBalances(referrer, eur.address); - - // WITHDRAW AFFILIATE FEES - // FAIL - await expectRevert( - sovryn.withdrawAffiliatesReferrerTokenFees(doc.address, constants.ZERO_ADDRESS, referrerFee, { from: referrer }), - "Affiliates: cannot withdraw to zero address" - ); - await expectRevert( - sovryn.withdrawAffiliatesReferrerTokenFees(eur.address, constants.ZERO_ADDRESS, referrerFee2, { from: referrer }), - "Affiliates: cannot withdraw to zero address" - ); - await expectRevert( - sovryn.withdrawAffiliatesReferrerTokenFees(doc.address, referrer, 0, { from: referrer }), - "Affiliates: cannot withdraw zero amount" - ); - await expectRevert( - sovryn.withdrawAffiliatesReferrerTokenFees(eur.address, referrer, 0, { from: referrer }), - "Affiliates: cannot withdraw zero amount" - ); - - // SUCCESS - // withdraw all - let tx; - tx = await sovryn.withdrawAllAffiliatesReferrerTokenFees(referrer, { from: referrer }); - const newReferrerFee = await sovryn.affiliatesReferrerBalances(referrer, doc.address); - expect(newReferrerFee, "Incorrect all balance after all DoC withdraw").to.be.bignumber.equal(new BN(0)); - - await expectEvent.inTransaction(tx.receipt.rawLogs[0].transactionHash, Affiliates, "WithdrawAffiliatesReferrerTokenFees", { - referrer: referrer, - receiver: referrer, - tokenAddress: doc.address, - amount: referrerFee.toString(), - }); - - const newReferrerFee2 = await sovryn.affiliatesReferrerBalances(referrer, eur.address); - expect(newReferrerFee2, "Incorrect all balance after all EUR withdraw").to.be.bignumber.equal(new BN(0)); - - await expectEvent.inTransaction(tx.receipt.rawLogs[1].transactionHash, Affiliates, "WithdrawAffiliatesReferrerTokenFees", { - referrer: referrer, - receiver: referrer, - tokenAddress: eur.address, - amount: referrerFee2.toString(), - }); - - let refBalances = await sovryn.getAffiliatesReferrerBalances(referrer); - expect(refBalances["referrerTokensList"][0], "After withdrawal the token should be deleted from the referrers list").to.be - .undefined; - expect(refBalances["referrerTokensBalances"][0], "After withdrawal the token balances should be deleted from the referrers list").to - .be.undefined; - }); + } + params = [ + "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object + false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans + owner, // address owner; // owner of this object + eur.address, // address loanToken; // the token being loaned + WRBTC.address, // address collateralToken; // the required collateral token + wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin + wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value + 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) + ]; + + // await loanTokenV2.setupLoanParams([params], true); + await loanToken2V2.setupLoanParams([params], false); + + // setting up interest rates + const baseRate = wei("1", "ether"); + const rateMultiplier = wei("20.25", "ether"); + const targetLevel = wei("80", "ether"); + const kinkLevel = wei("90", "ether"); + const maxScaleRate = wei("100", "ether"); + await loanToken2V2.setDemandCurve( + baseRate, + rateMultiplier, + baseRate, + rateMultiplier, + targetLevel, + kinkLevel, + maxScaleRate + ); + + // GIVING SOME DOC tokens to loanToken so that we can borrow from loanToken + await eur.transfer(loanToken2V2.address, wei("500", "ether")); + await eur.transfer(trader, wei("20", "ether")); + // trader approves to LoanToken loan amount for trading + await eur.approve(loanToken2.address, web3.utils.toWei("20", "ether"), { from: trader }); + // Giving some more testRbtc to sovrynAddress (by minting some testRbtc),so that it can open position in wRBTC. + await WRBTC.mint(sovryn.address, wei("500", "ether")); + + // margin trade afiliate 2 tokens + await loanTokenV2.marginTradeAffiliate( + constants.ZERO_BYTES32, // loanId (0 for new loans) + leverageAmount, // leverageAmount + loanTokenSent, // loanTokenSent + 0, // no collateral token sent + WRBTC.address, // collateralTokenAddress + trader, + 0, // max slippage + referrer, // affiliates referrer + "0x", // loanDataBytes (only required with ether) + { from: trader } + ); + const referrerFee = await sovryn.affiliatesReferrerBalances(referrer, doc.address); + + await loanToken2V2.marginTradeAffiliate( + constants.ZERO_BYTES32, // loanId (0 for new loans) + leverageAmount2, // leverageAmount + loanTokenSent2, // loanTokenSent + 0, // no collateral token sent + WRBTC.address, // collateralTokenAddress + trader, + 0, // max slippage + referrer, // affiliates referrer + "0x", // loanDataBytes (only required with ether) + { from: trader } + ); + const referrerFee2 = await sovryn.affiliatesReferrerBalances(referrer, eur.address); + + // WITHDRAW AFFILIATE FEES + // FAIL + await expectRevert( + sovryn.withdrawAffiliatesReferrerTokenFees( + doc.address, + constants.ZERO_ADDRESS, + referrerFee, + { from: referrer } + ), + "Affiliates: cannot withdraw to zero address" + ); + await expectRevert( + sovryn.withdrawAffiliatesReferrerTokenFees( + eur.address, + constants.ZERO_ADDRESS, + referrerFee2, + { from: referrer } + ), + "Affiliates: cannot withdraw to zero address" + ); + await expectRevert( + sovryn.withdrawAffiliatesReferrerTokenFees(doc.address, referrer, 0, { + from: referrer, + }), + "Affiliates: cannot withdraw zero amount" + ); + await expectRevert( + sovryn.withdrawAffiliatesReferrerTokenFees(eur.address, referrer, 0, { + from: referrer, + }), + "Affiliates: cannot withdraw zero amount" + ); + + // SUCCESS + // partial withdraw + let tx; + tx = await sovryn.withdrawAffiliatesReferrerTokenFees( + doc.address, + referrer, + referrerFee.divn(2), + { from: referrer } + ); + const newReferrerFee = await sovryn.affiliatesReferrerBalances(referrer, doc.address); + expect( + newReferrerFee, + "Incorrect partial balance after half DoC withdraw" + ).to.be.bignumber.equal(referrerFee.divn(2)); + + await expectEvent.inTransaction( + tx.receipt.rawLogs[0].transactionHash, + Affiliates, + "WithdrawAffiliatesReferrerTokenFees", + { + referrer: referrer, + receiver: referrer, + tokenAddress: doc.address, + amount: referrerFee.divn(2).toString(), + } + ); + + await sovryn.withdrawAffiliatesReferrerTokenFees( + eur.address, + referrer, + referrerFee2.divn(4), + { from: referrer } + ); + const newReferrerFee2 = await sovryn.affiliatesReferrerBalances(referrer, eur.address); + + expect( + newReferrerFee2, + "Incorrect partial balance after a quarter EUR withdraw" + ).to.be.bignumber.equal(referrerFee2.muln(3).divn(4)); + + let refBalances = await sovryn.getAffiliatesReferrerBalances(referrer); + + // complete withdraw + await sovryn.withdrawAffiliatesReferrerTokenFees(doc.address, referrer, newReferrerFee, { + from: referrer, + }); + expect( + await doc.balanceOf(referrer), + "Incorrect DoC withdraw amount" + ).to.be.bignumber.equal(referrerFee); + expect( + await sovryn.affiliatesReferrerBalances(referrer, doc.address), + "Affiliate Referrer's balance of DoC should be zero after withdrawal" + ).to.be.bignumber.equal(new BN(0)); + + // now the DoC token and it's balance for the referrer should be removed from the list + refBalances = await sovryn.getAffiliatesReferrerBalances(referrer); + expect(refBalances["referrerTokensList"][0]).to.eql(eur.address); + expect(refBalances["referrerTokensList"]).to.have.length(1); + expect(refBalances["referrerTokensBalances"][0]).to.be.bignumber.equal( + new BN(Math.pow(10, 15)).muln(18) + ); + expect(refBalances["referrerTokensBalances"]).to.have.length(1); + + await sovryn.withdrawAffiliatesReferrerTokenFees(eur.address, referrer, newReferrerFee2, { + from: referrer, + }); + expect( + await eur.balanceOf(referrer), + "Incorrect EUR withdraw amount" + ).to.be.bignumber.equal(referrerFee2); + expect( + await sovryn.affiliatesReferrerBalances(referrer, eur.address), + "Affiliate Referrer's balance of EUR should be zero after withdrawal" + ).to.be.bignumber.equal(new BN(0)); + + refBalances = await sovryn.getAffiliatesReferrerBalances(referrer); + expect( + refBalances["referrerTokensList"][0], + "After withdrawal the token should be deleted from the referrers list" + ).to.be.undefined; + expect( + refBalances["referrerTokensBalances"][0], + "After withdrawal the token balances should be deleted from the referrers list" + ).to.be.undefined; + }); + + it("Affiliates Referrer withdraw all fees for two tokens works correctly with min referrals to payout is 0", async () => { + await sovryn.setMinReferralsToPayoutAffiliates(1); + const leverageAmount = web3.utils.toWei("3", "ether"); + // loan tokens sent to iToken contract to start Margin Trading + const loanTokenSent = web3.utils.toWei("20", "ether"); + + const leverageAmount2 = web3.utils.toWei("3", "ether"); + // loan tokens sent to iToken contract to start Margin Trading + const loanTokenSent2 = web3.utils.toWei("10", "ether"); + + // add another pair + // loanTokenLogic = await MockLoanTokenLogic.new(); + // WRBTC = await WRBTC.new(); + eur = await TestToken.new("euro on chain 2", "EUR", 18, wei("20000", "ether")); + const loanToken2 = await LoanToken.new( + owner, + loanTokenLogic.address, + sovryn.address, + WRBTC.address + ); + await loanToken2.initialize(eur.address, "SEUR", "SEUR"); + + /** Initialize the loan token logic proxy */ + loanToken2V2 = await ILoanTokenLogicProxy.at(loanToken2.address); + await loanToken2V2.setBeaconAddress(loanTokenLogicBeacon.address); + + // loanToken2V2 = await MockLoanTokenLogic.at(loanToken2.address); //mocked for ad-hoc logic for isolated testing + loanToken2V2 = await ILoanTokenModulesMock.at(loanToken2.address); //mocked for ad-hoc logic for isolated testing + const loanTokenAddress2 = await loanToken2.loanTokenAddress(); + if (owner == (await sovryn.owner())) { + await sovryn.setLoanPool([loanToken2V2.address], [loanTokenAddress2]); + } + await feeds.setRates(eur.address, WRBTC.address, wei("0.01", "ether")); + await sovryn.setSupportedTokens([eur.address], [true]); + + params = [ + "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object + false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans + owner, // address owner; // owner of this object + eur.address, // address loanToken; // the token being loaned + WRBTC.address, // address collateralToken; // the required collateral token + wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin + wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value + 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) + ]; + + // await loanTokenV2.setupLoanParams([params], true); + await loanToken2V2.setupLoanParams([params], false); + + // setting up interest rates + const baseRate = wei("1", "ether"); + const rateMultiplier = wei("20.25", "ether"); + const targetLevel = wei("80", "ether"); + const kinkLevel = wei("90", "ether"); + const maxScaleRate = wei("100", "ether"); + await loanToken2V2.setDemandCurve( + baseRate, + rateMultiplier, + baseRate, + rateMultiplier, + targetLevel, + kinkLevel, + maxScaleRate + ); + + // GIVING SOME DOC tokens to loanToken so that we can borrow from loanToken + await eur.transfer(loanToken2V2.address, wei("500", "ether")); + await eur.transfer(trader, wei("20", "ether")); + // trader approves to LoanToken loan amount for trading + await eur.approve(loanToken2.address, web3.utils.toWei("20", "ether"), { from: trader }); + // Giving some more testRbtc to sovrynAddress (by minting some testRbtc),so that it can open position in wRBTC. + await WRBTC.mint(sovryn.address, wei("500", "ether")); + + // margin trade afiliate 2 tokens + await loanTokenV2.marginTradeAffiliate( + constants.ZERO_BYTES32, // loanId (0 for new loans) + leverageAmount, // leverageAmount + loanTokenSent, // loanTokenSent + 0, // no collateral token sent + WRBTC.address, // collateralTokenAddress + trader, + 0, // max slippage + referrer, // affiliates referrer + "0x", // loanDataBytes (only required with ether) + { from: trader } + ); + const referrerFee = await sovryn.affiliatesReferrerBalances(referrer, doc.address); + + await loanToken2V2.marginTradeAffiliate( + constants.ZERO_BYTES32, // loanId (0 for new loans) + leverageAmount2, // leverageAmount + loanTokenSent2, // loanTokenSent + 0, // no collateral token sent + WRBTC.address, // collateralTokenAddress + trader, + 0, // max slippage + referrer, // affiliates referrer + "0x", // loanDataBytes (only required with ether) + { from: trader } + ); + const referrerFee2 = await sovryn.affiliatesReferrerBalances(referrer, eur.address); + + // WITHDRAW AFFILIATE FEES + // FAIL + await expectRevert( + sovryn.withdrawAffiliatesReferrerTokenFees( + doc.address, + constants.ZERO_ADDRESS, + referrerFee, + { from: referrer } + ), + "Affiliates: cannot withdraw to zero address" + ); + await expectRevert( + sovryn.withdrawAffiliatesReferrerTokenFees( + eur.address, + constants.ZERO_ADDRESS, + referrerFee2, + { from: referrer } + ), + "Affiliates: cannot withdraw to zero address" + ); + await expectRevert( + sovryn.withdrawAffiliatesReferrerTokenFees(doc.address, referrer, 0, { + from: referrer, + }), + "Affiliates: cannot withdraw zero amount" + ); + await expectRevert( + sovryn.withdrawAffiliatesReferrerTokenFees(eur.address, referrer, 0, { + from: referrer, + }), + "Affiliates: cannot withdraw zero amount" + ); + + // SUCCESS + // withdraw all + let tx; + tx = await sovryn.withdrawAllAffiliatesReferrerTokenFees(referrer, { from: referrer }); + const newReferrerFee = await sovryn.affiliatesReferrerBalances(referrer, doc.address); + expect( + newReferrerFee, + "Incorrect all balance after all DoC withdraw" + ).to.be.bignumber.equal(new BN(0)); + + await expectEvent.inTransaction( + tx.receipt.rawLogs[0].transactionHash, + Affiliates, + "WithdrawAffiliatesReferrerTokenFees", + { + referrer: referrer, + receiver: referrer, + tokenAddress: doc.address, + amount: referrerFee.toString(), + } + ); + + const newReferrerFee2 = await sovryn.affiliatesReferrerBalances(referrer, eur.address); + expect( + newReferrerFee2, + "Incorrect all balance after all EUR withdraw" + ).to.be.bignumber.equal(new BN(0)); + + await expectEvent.inTransaction( + tx.receipt.rawLogs[1].transactionHash, + Affiliates, + "WithdrawAffiliatesReferrerTokenFees", + { + referrer: referrer, + receiver: referrer, + tokenAddress: eur.address, + amount: referrerFee2.toString(), + } + ); + + let refBalances = await sovryn.getAffiliatesReferrerBalances(referrer); + expect( + refBalances["referrerTokensList"][0], + "After withdrawal the token should be deleted from the referrers list" + ).to.be.undefined; + expect( + refBalances["referrerTokensBalances"][0], + "After withdrawal the token balances should be deleted from the referrers list" + ).to.be.undefined; + }); }); diff --git a/tests/affiliates/affiliates_integration.test.js b/tests/affiliates/affiliates_integration.test.js index 7a0ad3ad3..735a80bfc 100644 --- a/tests/affiliates/affiliates_integration.test.js +++ b/tests/affiliates/affiliates_integration.test.js @@ -44,229 +44,279 @@ const Affiliates = artifacts.require("Affiliates"); const IV1PoolOracle = artifacts.require("IV1PoolOracle"); const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getLoanTokenLogic, - getLoanToken, - getLoanTokenLogicWrbtc, - getLoanTokenWRBTC, - loan_pool_setup, - set_demand_curve, - getPriceFeeds, - getSovryn, - decodeLogs, - getSOV, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getLoanTokenLogic, + getLoanToken, + getLoanTokenLogicWrbtc, + getLoanTokenWRBTC, + loan_pool_setup, + set_demand_curve, + getPriceFeeds, + getSovryn, + decodeLogs, + getSOV, } = require("../Utils/initializer.js"); let cliff = 1; // This is in 4 weeks. i.e. 1 * 4 weeks. let duration = 11; // This is in 4 weeks. i.e. 11 * 4 weeks. contract("Affiliates", (accounts) => { - let loanTokenLogic; - let WRBTC; - let doc; - let SUSD; - let lockedSOV; - let sovryn; - let loanTokenV2; - let feeds; - let wei = web3.utils.toWei; - let senderMock; - let swapsSovryn; - - let loanTokenSent; - let leverageAmount; - let referrerFee; - - async function deploymentAndInitFixture(_wallets, _provider) { - const provider = waffle.provider; - [senderMock] = provider.getWallets(); - - // Deploying sovrynProtocol w/ generic function from initializer.js - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - await sovryn.setSovrynProtocolAddress(sovryn.address); - - // loanTokenLogic = await LoanTokenLogicLM.new(); - const initLoanTokenLogic = await getLoanTokenLogic(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] - loanTokenLogic = initLoanTokenLogic[0]; - loanTokenLogicBeacon = initLoanTokenLogic[1]; - doc = await TestToken.new("dollar on chain", "DOC", 18, wei("20000", "ether")); - loanToken = await LoanToken.new(owner, loanTokenLogic.address, sovryn.address, WRBTC.address); - await loanToken.initialize(doc.address, "SUSD", "SUSD"); - - /** Initialize the loan token logic proxy */ - loanTokenV2 = await ILoanTokenLogicProxy.at(loanToken.address); - await loanTokenV2.setBeaconAddress(loanTokenLogicBeacon.address); - - /** Use interface of LoanTokenModules */ - loanTokenV2 = await ILoanTokenModules.at(loanToken.address); - - const loanTokenAddress = await loanToken.loanTokenAddress(); - if (owner == (await sovryn.owner())) { - await sovryn.setLoanPool([loanTokenV2.address], [loanTokenAddress]); - } - - // Creating the Staking Instance. - stakingLogic = await StakingLogic.new(SUSD.address); - staking = await StakingProxy.new(SUSD.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingLogic.at(staking.address); - - // Creating the FeeSharing Instance. - feeSharingProxy = await FeeSharingProxy.new(constants.ZERO_ADDRESS, staking.address); - - // Creating the Vesting Instance. - vestingLogic = await VestingLogic.new(); - vestingFactory = await VestingFactory.new(vestingLogic.address); - vestingRegistry = await VestingRegistry.new( - vestingFactory.address, - SUSD.address, - staking.address, - feeSharingProxy.address, - owner // This should be Governance Timelock Contract. - ); - vestingFactory.transferOwnership(vestingRegistry.address); - - // Creating the instance of newLockedSOV Contract. - await sovryn.setLockedSOVAddress((await LockedSOV.new(SUSD.address, vestingRegistry.address, cliff, duration, [owner])).address); - lockedSOV = await LockedSOV.at(await sovryn.lockedSOVAddress()); - - // initialize - /// @dev Optimization: Init same feeds for all tests - - // feeds = await PriceFeedsLocal.new(WRBTC.address, sovryn.address); - // await feeds.setRates(doc.address, WRBTC.address, wei("0.01", "ether")); - - feeds = await PriceFeeds.new(WRBTC.address, SUSD.address, doc.address); - testToken1Precision = 18; - testToken2Precision = 18; - btcPrecision = 18; - testToken1 = await TestToken.new("test token 1", "TEST1", testToken1Precision, wei("20000", "ether")); - testToken2 = await TestToken.new("test token 2", "TEST2", testToken2Precision, wei("20000", "ether")); - testToken1Price = wei("2", "ether"); - testToken2Price = wei("2", "ether"); - wrBTCPrice = wei("8", "ether"); - docPrice = wei("7", "ether"); - - // Set tetToken1 feed - price 1Z BTC - // Set v1 convert mockup - liquidityV1ConverterMockupTestToken1 = await LiquidityPoolV1ConverterMockup.new(testToken1.address, WRBTC.address); - - priceFeedsV1PoolOracleMockupTestToken1 = await deployMockContract(senderMock, IV1PoolOracle.abi); - await priceFeedsV1PoolOracleMockupTestToken1.mock.latestAnswer.returns(testToken1Price); - await priceFeedsV1PoolOracleMockupTestToken1.mock.latestPrice.returns(testToken1Price); - await priceFeedsV1PoolOracleMockupTestToken1.mock.liquidityPool.returns(liquidityV1ConverterMockupTestToken1.address); - - priceFeedsV1PoolOracleTestToken1 = await PriceFeedV1PoolOracle.new( - priceFeedsV1PoolOracleMockupTestToken1.address, - WRBTC.address, - doc.address, - testToken1.address - ); - - liquidityV1ConverterMockupTestToken2 = await LiquidityPoolV1ConverterMockup.new(testToken2.address, WRBTC.address); - priceFeedsV1PoolOracleMockupTestToken2 = await deployMockContract(senderMock, IV1PoolOracle.abi); - await priceFeedsV1PoolOracleMockupTestToken2.mock.latestAnswer.returns(testToken2Price); - await priceFeedsV1PoolOracleMockupTestToken2.mock.latestPrice.returns(testToken2Price); - await priceFeedsV1PoolOracleMockupTestToken2.mock.liquidityPool.returns(liquidityV1ConverterMockupTestToken2.address); - - priceFeedsV1PoolOracleTestToken2 = await PriceFeedV1PoolOracle.new( - priceFeedsV1PoolOracleMockupTestToken2.address, - WRBTC.address, - doc.address, - testToken2.address - ); - - // Set rBTC feed - using rsk oracle - priceFeedsV1PoolOracleMockupBTC = await PriceFeedRSKOracleMockup.new(); - await priceFeedsV1PoolOracleMockupBTC.setValue(wrBTCPrice); - priceFeedsV1PoolOracleBTC = await PriceFeedRSKOracle.new(priceFeedsV1PoolOracleMockupBTC.address); - - // Set DOC feed -- price 1 BTC - liquidityV1ConverterMockupDOC = await LiquidityPoolV1ConverterMockup.new(doc.address, WRBTC.address); - - priceFeedsV1PoolOracleMockupDOC = await deployMockContract(senderMock, IV1PoolOracle.abi); - await priceFeedsV1PoolOracleMockupDOC.mock.latestAnswer.returns(docPrice); - await priceFeedsV1PoolOracleMockupDOC.mock.latestPrice.returns(docPrice); - await priceFeedsV1PoolOracleMockupDOC.mock.liquidityPool.returns(liquidityV1ConverterMockupDOC.address); - - priceFeedsV1PoolOracleDOC = await PriceFeedV1PoolOracle.new( - priceFeedsV1PoolOracleMockupDOC.address, - WRBTC.address, - doc.address, - doc.address - ); - - // await feeds.setPriceFeed([WRBTC.address, doc.address], [priceFeedsV1PoolOracle.address, priceFeedsV1PoolOracle.address]) - await feeds.setPriceFeed( - [testToken1.address, testToken2.address, doc.address, WRBTC.address], - [ - priceFeedsV1PoolOracleTestToken1.address, - priceFeedsV1PoolOracleTestToken2.address, - priceFeedsV1PoolOracleDOC.address, - priceFeedsV1PoolOracleBTC.address, - ] - ); - - test1 = await feeds.queryRate(testToken1.address, doc.address); - expect(test1[0].toString()).to.be.equal( - new BN(testToken1Price) - .mul(new BN(wrBTCPrice)) - .mul(new BN(10 ** (testToken1Precision - btcPrecision))) - .div(new BN(wei("1", "ether"))) - .toString() - ); - - test = await feeds.queryReturn(testToken1.address, doc.address, wei("2", "ether")); - expect(test.toString()).to.be.equal( - new BN(2) - .mul( - new BN(testToken1Price) - .mul(new BN(wrBTCPrice)) - .mul(new BN(10 ** (testToken1Precision - btcPrecision))) - .div(new BN(wei("1", "ether"))) - ) - .toString() - ); - - test1 = await feeds.queryRate(testToken1.address, testToken2.address); - - expect(test1[0].toString()).to.be.equal( - new BN(testToken1Price) - .div(new BN(testToken2Price)) - .mul(new BN(wei("1", "ether"))) - .toString() - ); - test = await feeds.queryReturn(testToken1.address, testToken2.address, wei("2", "ether")); - expect(test.toString()).to.be.equal( - new BN(2).mul(new BN(testToken1Price).div(new BN(testToken2Price)).mul(new BN(wei("1", "ether")))).toString() - ); - - /// @dev Optimization: Init same swap pool for all tests - - swapsSovryn = await SwapsImplSovrynSwap.new(); - const sovrynSwapSimulator = await TestSovrynSwap.new(feeds.address); - await sovryn.setSovrynSwapContractRegistryAddress(sovrynSwapSimulator.address); - await sovryn.setSupportedTokens([doc.address, WRBTC.address], [true, true]); - await sovryn.setPriceFeedContract( - feeds.address // priceFeeds - ); - await sovryn.setSwapsImplContract( - swapsSovryn.address // swapsImpl - ); - await sovryn.setFeesController(owner); - await sovryn.setWrbtcToken(WRBTC.address); - await sovryn.setSOVTokenAddress(SUSD.address); - - { - /** + let loanTokenLogic; + let WRBTC; + let doc; + let SUSD; + let lockedSOV; + let sovryn; + let loanTokenV2; + let feeds; + let wei = web3.utils.toWei; + let senderMock; + let swapsSovryn; + + let loanTokenSent; + let leverageAmount; + let referrerFee; + + async function deploymentAndInitFixture(_wallets, _provider) { + const provider = waffle.provider; + [senderMock] = provider.getWallets(); + + // Deploying sovrynProtocol w/ generic function from initializer.js + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + await sovryn.setSovrynProtocolAddress(sovryn.address); + + // loanTokenLogic = await LoanTokenLogicLM.new(); + const initLoanTokenLogic = await getLoanTokenLogic(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] + loanTokenLogic = initLoanTokenLogic[0]; + loanTokenLogicBeacon = initLoanTokenLogic[1]; + doc = await TestToken.new("dollar on chain", "DOC", 18, wei("20000", "ether")); + loanToken = await LoanToken.new( + owner, + loanTokenLogic.address, + sovryn.address, + WRBTC.address + ); + await loanToken.initialize(doc.address, "SUSD", "SUSD"); + + /** Initialize the loan token logic proxy */ + loanTokenV2 = await ILoanTokenLogicProxy.at(loanToken.address); + await loanTokenV2.setBeaconAddress(loanTokenLogicBeacon.address); + + /** Use interface of LoanTokenModules */ + loanTokenV2 = await ILoanTokenModules.at(loanToken.address); + + const loanTokenAddress = await loanToken.loanTokenAddress(); + if (owner == (await sovryn.owner())) { + await sovryn.setLoanPool([loanTokenV2.address], [loanTokenAddress]); + } + + // Creating the Staking Instance. + stakingLogic = await StakingLogic.new(SUSD.address); + staking = await StakingProxy.new(SUSD.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); + + // Creating the FeeSharing Instance. + feeSharingProxy = await FeeSharingProxy.new(constants.ZERO_ADDRESS, staking.address); + + // Creating the Vesting Instance. + vestingLogic = await VestingLogic.new(); + vestingFactory = await VestingFactory.new(vestingLogic.address); + vestingRegistry = await VestingRegistry.new( + vestingFactory.address, + SUSD.address, + staking.address, + feeSharingProxy.address, + owner // This should be Governance Timelock Contract. + ); + vestingFactory.transferOwnership(vestingRegistry.address); + + // Creating the instance of newLockedSOV Contract. + await sovryn.setLockedSOVAddress( + ( + await LockedSOV.new(SUSD.address, vestingRegistry.address, cliff, duration, [ + owner, + ]) + ).address + ); + lockedSOV = await LockedSOV.at(await sovryn.lockedSOVAddress()); + + // initialize + /// @dev Optimization: Init same feeds for all tests + + // feeds = await PriceFeedsLocal.new(WRBTC.address, sovryn.address); + // await feeds.setRates(doc.address, WRBTC.address, wei("0.01", "ether")); + + feeds = await PriceFeeds.new(WRBTC.address, SUSD.address, doc.address); + testToken1Precision = 18; + testToken2Precision = 18; + btcPrecision = 18; + testToken1 = await TestToken.new( + "test token 1", + "TEST1", + testToken1Precision, + wei("20000", "ether") + ); + testToken2 = await TestToken.new( + "test token 2", + "TEST2", + testToken2Precision, + wei("20000", "ether") + ); + testToken1Price = wei("2", "ether"); + testToken2Price = wei("2", "ether"); + wrBTCPrice = wei("8", "ether"); + docPrice = wei("7", "ether"); + + // Set tetToken1 feed - price 1Z BTC + // Set v1 convert mockup + liquidityV1ConverterMockupTestToken1 = await LiquidityPoolV1ConverterMockup.new( + testToken1.address, + WRBTC.address + ); + + priceFeedsV1PoolOracleMockupTestToken1 = await deployMockContract( + senderMock, + IV1PoolOracle.abi + ); + await priceFeedsV1PoolOracleMockupTestToken1.mock.latestAnswer.returns(testToken1Price); + await priceFeedsV1PoolOracleMockupTestToken1.mock.latestPrice.returns(testToken1Price); + await priceFeedsV1PoolOracleMockupTestToken1.mock.liquidityPool.returns( + liquidityV1ConverterMockupTestToken1.address + ); + + priceFeedsV1PoolOracleTestToken1 = await PriceFeedV1PoolOracle.new( + priceFeedsV1PoolOracleMockupTestToken1.address, + WRBTC.address, + doc.address, + testToken1.address + ); + + liquidityV1ConverterMockupTestToken2 = await LiquidityPoolV1ConverterMockup.new( + testToken2.address, + WRBTC.address + ); + priceFeedsV1PoolOracleMockupTestToken2 = await deployMockContract( + senderMock, + IV1PoolOracle.abi + ); + await priceFeedsV1PoolOracleMockupTestToken2.mock.latestAnswer.returns(testToken2Price); + await priceFeedsV1PoolOracleMockupTestToken2.mock.latestPrice.returns(testToken2Price); + await priceFeedsV1PoolOracleMockupTestToken2.mock.liquidityPool.returns( + liquidityV1ConverterMockupTestToken2.address + ); + + priceFeedsV1PoolOracleTestToken2 = await PriceFeedV1PoolOracle.new( + priceFeedsV1PoolOracleMockupTestToken2.address, + WRBTC.address, + doc.address, + testToken2.address + ); + + // Set rBTC feed - using rsk oracle + priceFeedsV1PoolOracleMockupBTC = await PriceFeedRSKOracleMockup.new(); + await priceFeedsV1PoolOracleMockupBTC.setValue(wrBTCPrice); + priceFeedsV1PoolOracleBTC = await PriceFeedRSKOracle.new( + priceFeedsV1PoolOracleMockupBTC.address + ); + + // Set DOC feed -- price 1 BTC + liquidityV1ConverterMockupDOC = await LiquidityPoolV1ConverterMockup.new( + doc.address, + WRBTC.address + ); + + priceFeedsV1PoolOracleMockupDOC = await deployMockContract(senderMock, IV1PoolOracle.abi); + await priceFeedsV1PoolOracleMockupDOC.mock.latestAnswer.returns(docPrice); + await priceFeedsV1PoolOracleMockupDOC.mock.latestPrice.returns(docPrice); + await priceFeedsV1PoolOracleMockupDOC.mock.liquidityPool.returns( + liquidityV1ConverterMockupDOC.address + ); + + priceFeedsV1PoolOracleDOC = await PriceFeedV1PoolOracle.new( + priceFeedsV1PoolOracleMockupDOC.address, + WRBTC.address, + doc.address, + doc.address + ); + + // await feeds.setPriceFeed([WRBTC.address, doc.address], [priceFeedsV1PoolOracle.address, priceFeedsV1PoolOracle.address]) + await feeds.setPriceFeed( + [testToken1.address, testToken2.address, doc.address, WRBTC.address], + [ + priceFeedsV1PoolOracleTestToken1.address, + priceFeedsV1PoolOracleTestToken2.address, + priceFeedsV1PoolOracleDOC.address, + priceFeedsV1PoolOracleBTC.address, + ] + ); + + test1 = await feeds.queryRate(testToken1.address, doc.address); + expect(test1[0].toString()).to.be.equal( + new BN(testToken1Price) + .mul(new BN(wrBTCPrice)) + .mul(new BN(10 ** (testToken1Precision - btcPrecision))) + .div(new BN(wei("1", "ether"))) + .toString() + ); + + test = await feeds.queryReturn(testToken1.address, doc.address, wei("2", "ether")); + expect(test.toString()).to.be.equal( + new BN(2) + .mul( + new BN(testToken1Price) + .mul(new BN(wrBTCPrice)) + .mul(new BN(10 ** (testToken1Precision - btcPrecision))) + .div(new BN(wei("1", "ether"))) + ) + .toString() + ); + + test1 = await feeds.queryRate(testToken1.address, testToken2.address); + + expect(test1[0].toString()).to.be.equal( + new BN(testToken1Price) + .div(new BN(testToken2Price)) + .mul(new BN(wei("1", "ether"))) + .toString() + ); + test = await feeds.queryReturn(testToken1.address, testToken2.address, wei("2", "ether")); + expect(test.toString()).to.be.equal( + new BN(2) + .mul( + new BN(testToken1Price) + .div(new BN(testToken2Price)) + .mul(new BN(wei("1", "ether"))) + ) + .toString() + ); + + /// @dev Optimization: Init same swap pool for all tests + + swapsSovryn = await SwapsImplSovrynSwap.new(); + const sovrynSwapSimulator = await TestSovrynSwap.new(feeds.address); + await sovryn.setSovrynSwapContractRegistryAddress(sovrynSwapSimulator.address); + await sovryn.setSupportedTokens([doc.address, WRBTC.address], [true, true]); + await sovryn.setPriceFeedContract( + feeds.address // priceFeeds + ); + await sovryn.setSwapsImplContract( + swapsSovryn.address // swapsImpl + ); + await sovryn.setFeesController(owner); + await sovryn.setWrbtcToken(WRBTC.address); + await sovryn.setSOVTokenAddress(SUSD.address); + + { + /** struct LoanParams { bytes32 id; // id of loan params object bool active; // if false, this object has been disabled by the owner and can't be used for future loans @@ -278,113 +328,132 @@ contract("Affiliates", (accounts) => { uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) } */ - } - params = [ - "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object - false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans - owner, // address owner; // owner of this object - doc.address, // address loanToken; // the token being loaned - WRBTC.address, // address collateralToken; // the required collateral token - wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin - wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value - 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) - ]; - - // await loanTokenV2.setupLoanParams([params], true); - await loanTokenV2.setupLoanParams([params], false); - - // setting up interest rates - const baseRate = wei("1", "ether"); - const rateMultiplier = wei("20.25", "ether"); - const targetLevel = wei("80", "ether"); - const kinkLevel = wei("90", "ether"); - const maxScaleRate = wei("100", "ether"); - await loanTokenV2.setDemandCurve(baseRate, rateMultiplier, baseRate, rateMultiplier, targetLevel, kinkLevel, maxScaleRate); - - // GIVING SOME DOC tokens to loanToken so that we can borrow from loanToken - await doc.transfer(loanTokenV2.address, wei("500", "ether")); - await doc.transfer(trader, wei("100", "ether")); - // trader approves to LoanToken loan amount for trading - await doc.approve(loanToken.address, web3.utils.toWei("100", "ether"), { from: trader }); - // Giving some testRbtc to sovrynAddress (by minting some testRbtc),so that it can open position in wRBTC. - await WRBTC.mint(sovryn.address, wei("500", "ether")); - - // Giving some SOV Token to sovrynAddress (For affiliates rewards purposes) - await SUSD.mint(sovryn.address, wei("500", "ether")); - - /// @dev Optimization: Init default affiliate for all tests - - // Change the min referrals to payout to 3 for testing purposes - await sovryn.setMinReferralsToPayoutAffiliates(3); - loanTokenLogic = await LoanTokenLogicLM.new(); - - loanTokenSent = wei("21", "ether"); - leverageAmount = web3.utils.toWei("2", "ether"); - // underlyingToken.approve(loanTokenV2.address, loanTokenSent*2) - - let previousAffiliateRewardsHeld = await sovryn.affiliateRewardsHeld(referrer); - let tx = await loanTokenV2.marginTradeAffiliate( - constants.ZERO_BYTES32, // loanId (0 for new loans) - leverageAmount, // Leverage - loanTokenSent, // loanTokenSent - 0, // - WRBTC.address, // collateralTokenAddress - trader, // trader - 0, // max slippage - referrer, // referrer address - "0x", // loanDataBytes (only required with ether) - { from: trader } - ); - - await expectEvent.inTransaction(tx.receipt.rawLogs[0].transactionHash, Affiliates, "SetAffiliatesReferrer", { - user: trader, - referrer: referrer, - }); - - let referrerOnChain = await sovryn.affiliatesUserReferrer(trader); - expect(referrerOnChain, "Incorrect User Affiliate").to.be.equal(referrer); - - notFirstTradeFlagOnChain = await sovryn.getUserNotFirstTradeFlag(trader); - expect(notFirstTradeFlagOnChain, "First trade flag is not updated").to.be.true; - - let decode = decodeLogs(tx.receipt.rawLogs, Affiliates, "PayTradingFeeToAffiliate"); - referrerFee = await sovryn.affiliatesReferrerBalances(referrer, doc.address); - if (!decode.length) { - throw "Event PayTradingFeeToAffiliate is not fired properly"; - } - - let isHeld = decode[0].args["isHeld"]; - let affiliateRewardsHeld = await sovryn.affiliateRewardsHeld(referrer); - let submittedAffiliatesReward = decode[0].args["sovBonusAmount"]; - let submittedTokenBonusAmount = decode[0].args["tokenBonusAmount"]; - let submittedReferrer = decode[0].args["referrer"]; - let submittedTrader = decode[0].args["trader"]; - expect(isHeld, "First trade affiliates reward must be in held").to.be.true; - expect(referrerFee.toString(), "Token bonus rewards is not matched").to.be.equal(submittedTokenBonusAmount.toString()); - - let checkedValueShouldBe = affiliateRewardsHeld - previousAffiliateRewardsHeld; - expect(checkedValueShouldBe.toString(), "Affiliates bonus rewards is not matched").to.be.equal( - submittedAffiliatesReward.toString() - ); - - // Check lockedSOV Balance of the referrer - let referrerBalanceInLockedSOV = await lockedSOV.getLockedBalance(referrer); - expect(referrerBalanceInLockedSOV.toString(), "Referrer balance in lockedSOV is not matched").to.be.equal(new BN(0).toString()); - - expect(submittedReferrer).to.eql(referrer); - expect(submittedTrader).to.eql(trader); - } - - before(async () => { - [owner, trader, referrer, account1, account2, ...accounts] = accounts; - }); - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - it("Test affiliates integration with underlying token", async () => { - /* + } + params = [ + "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object + false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans + owner, // address owner; // owner of this object + doc.address, // address loanToken; // the token being loaned + WRBTC.address, // address collateralToken; // the required collateral token + wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin + wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value + 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) + ]; + + // await loanTokenV2.setupLoanParams([params], true); + await loanTokenV2.setupLoanParams([params], false); + + // setting up interest rates + const baseRate = wei("1", "ether"); + const rateMultiplier = wei("20.25", "ether"); + const targetLevel = wei("80", "ether"); + const kinkLevel = wei("90", "ether"); + const maxScaleRate = wei("100", "ether"); + await loanTokenV2.setDemandCurve( + baseRate, + rateMultiplier, + baseRate, + rateMultiplier, + targetLevel, + kinkLevel, + maxScaleRate + ); + + // GIVING SOME DOC tokens to loanToken so that we can borrow from loanToken + await doc.transfer(loanTokenV2.address, wei("500", "ether")); + await doc.transfer(trader, wei("100", "ether")); + // trader approves to LoanToken loan amount for trading + await doc.approve(loanToken.address, web3.utils.toWei("100", "ether"), { from: trader }); + // Giving some testRbtc to sovrynAddress (by minting some testRbtc),so that it can open position in wRBTC. + await WRBTC.mint(sovryn.address, wei("500", "ether")); + + // Giving some SOV Token to sovrynAddress (For affiliates rewards purposes) + await SUSD.mint(sovryn.address, wei("500", "ether")); + + /// @dev Optimization: Init default affiliate for all tests + + // Change the min referrals to payout to 3 for testing purposes + await sovryn.setMinReferralsToPayoutAffiliates(3); + loanTokenLogic = await LoanTokenLogicLM.new(); + + loanTokenSent = wei("21", "ether"); + leverageAmount = web3.utils.toWei("2", "ether"); + // underlyingToken.approve(loanTokenV2.address, loanTokenSent*2) + + let previousAffiliateRewardsHeld = await sovryn.affiliateRewardsHeld(referrer); + let tx = await loanTokenV2.marginTradeAffiliate( + constants.ZERO_BYTES32, // loanId (0 for new loans) + leverageAmount, // Leverage + loanTokenSent, // loanTokenSent + 0, // + WRBTC.address, // collateralTokenAddress + trader, // trader + 0, // max slippage + referrer, // referrer address + "0x", // loanDataBytes (only required with ether) + { from: trader } + ); + + await expectEvent.inTransaction( + tx.receipt.rawLogs[0].transactionHash, + Affiliates, + "SetAffiliatesReferrer", + { + user: trader, + referrer: referrer, + } + ); + + let referrerOnChain = await sovryn.affiliatesUserReferrer(trader); + expect(referrerOnChain, "Incorrect User Affiliate").to.be.equal(referrer); + + notFirstTradeFlagOnChain = await sovryn.getUserNotFirstTradeFlag(trader); + expect(notFirstTradeFlagOnChain, "First trade flag is not updated").to.be.true; + + let decode = decodeLogs(tx.receipt.rawLogs, Affiliates, "PayTradingFeeToAffiliate"); + referrerFee = await sovryn.affiliatesReferrerBalances(referrer, doc.address); + if (!decode.length) { + throw "Event PayTradingFeeToAffiliate is not fired properly"; + } + + let isHeld = decode[0].args["isHeld"]; + let affiliateRewardsHeld = await sovryn.affiliateRewardsHeld(referrer); + let submittedAffiliatesReward = decode[0].args["sovBonusAmount"]; + let submittedTokenBonusAmount = decode[0].args["tokenBonusAmount"]; + let submittedReferrer = decode[0].args["referrer"]; + let submittedTrader = decode[0].args["trader"]; + expect(isHeld, "First trade affiliates reward must be in held").to.be.true; + expect(referrerFee.toString(), "Token bonus rewards is not matched").to.be.equal( + submittedTokenBonusAmount.toString() + ); + + let checkedValueShouldBe = affiliateRewardsHeld - previousAffiliateRewardsHeld; + expect( + checkedValueShouldBe.toString(), + "Affiliates bonus rewards is not matched" + ).to.be.equal(submittedAffiliatesReward.toString()); + + // Check lockedSOV Balance of the referrer + let referrerBalanceInLockedSOV = await lockedSOV.getLockedBalance(referrer); + expect( + referrerBalanceInLockedSOV.toString(), + "Referrer balance in lockedSOV is not matched" + ).to.be.equal(new BN(0).toString()); + + expect(submittedReferrer).to.eql(referrer); + expect(submittedTrader).to.eql(trader); + } + + before(async () => { + [owner, trader, referrer, account1, account2, ...accounts] = accounts; + }); + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + it("Test affiliates integration with underlying token", async () => { + /* /// @dev Optimization: Removed default affiliate init for specific test, relayed to beforeEach hook // Change the min referrals to payout to 3 for testing purposes @@ -449,79 +518,104 @@ contract("Affiliates", (accounts) => { expect(submittedTrader).to.eql(trader); */ - // Change the min referrals to payout to 1 - await sovryn.setMinReferralsToPayoutAffiliates(1); - - previousAffiliateRewardsHeld = await sovryn.affiliateRewardsHeld(referrer); - tx = await loanTokenV2.marginTradeAffiliate( - constants.ZERO_BYTES32, // loanId (0 for new loans) - leverageAmount, // Leverage - loanTokenSent, // loanTokenSent - 0, // - WRBTC.address, // collateralTokenAddress - trader, // trader - 0, // max slippage - referrer, // referrer address - "0x", // loanDataBytes (only required with ether) - { from: trader } - ); - - decode = decodeLogs(tx.receipt.rawLogs, Affiliates, "PayTradingFeeToAffiliate"); - if (!decode.length) { - throw "Event PayTradingFeeToAffiliate is not fired properly"; - } - - isHeld = decode[0].args["isHeld"]; - expect(isHeld, "First trade affiliates reward must not be in held").to.be.false; - - affiliateRewardsHeld = await sovryn.affiliateRewardsHeld(referrer); - submittedAffiliatesReward = decode[0].args["sovBonusAmount"]; - submittedTokenBonusAmount = decode[0].args["tokenBonusAmount"]; - sovBonusAmountPaid = decode[0].args["sovBonusAmountPaid"]; - submittedReferrer = decode[0].args["referrer"]; - submittedTrader = decode[0].args["trader"]; - - expect(affiliateRewardsHeld.toString(), "affiliateRewardHeld should be zero at this point").to.be.equal(new BN(0).toString()); - expect(referrerFee.toString(), "Token bonus rewards is not matched").to.be.equal(submittedTokenBonusAmount.toString()); - - checkSovBonusAmountPaid = new BN(submittedAffiliatesReward).add(new BN(previousAffiliateRewardsHeld)); - expect(checkSovBonusAmountPaid.toString(), "Affiliates bonus rewards paid is not matched").to.be.equal( - sovBonusAmountPaid.toString() - ); - - checkedValueShouldBe = new BN(affiliateRewardsHeld).add(new BN(previousAffiliateRewardsHeld)); - expect(checkedValueShouldBe.toString(), "Affiliates bonus rewards is not matched").to.be.equal( - submittedAffiliatesReward.toString() - ); - - // Check lockedSOV Balance of the referrer - referrerBalanceInLockedSOV = await lockedSOV.getLockedBalance(referrer); - expect(referrerBalanceInLockedSOV.toString(), "Referrer balance in lockedSOV is not matched").to.be.equal( - checkSovBonusAmountPaid.toString() - ); - - expect(submittedReferrer).to.eql(referrer); - expect(submittedTrader).to.eql(trader); - - // Do withdrawal - let referrerTokenBalance = await doc.balanceOf(referrer); - let referrerFee2 = new BN(referrerFee).add(new BN(submittedTokenBonusAmount)); - tx = await sovryn.withdrawAllAffiliatesReferrerTokenFees(referrer, { from: referrer }); - const referrerFeeAfterWithdrawal = await sovryn.affiliatesReferrerBalances(referrer, doc.address); - let referrerTokenBalanceAfterWithdrawal = await doc.balanceOf(referrer); - expect(referrerFeeAfterWithdrawal, "Incorrect all balance after all DoC withdraw").to.be.bignumber.equal(new BN(0)); - expect(referrerTokenBalanceAfterWithdrawal.sub(referrerTokenBalance).toString()).to.be.equal(referrerFee2.toString()); - - await expectEvent.inTransaction(tx.receipt.rawLogs[0].transactionHash, Affiliates, "WithdrawAffiliatesReferrerTokenFees", { - referrer: referrer, - receiver: referrer, - tokenAddress: doc.address, - amount: referrerFee2.toString(), - }); - }); - - it("Test affiliates integration with underlying token with oracle v1Pool", async () => { - /* + // Change the min referrals to payout to 1 + await sovryn.setMinReferralsToPayoutAffiliates(1); + + previousAffiliateRewardsHeld = await sovryn.affiliateRewardsHeld(referrer); + tx = await loanTokenV2.marginTradeAffiliate( + constants.ZERO_BYTES32, // loanId (0 for new loans) + leverageAmount, // Leverage + loanTokenSent, // loanTokenSent + 0, // + WRBTC.address, // collateralTokenAddress + trader, // trader + 0, // max slippage + referrer, // referrer address + "0x", // loanDataBytes (only required with ether) + { from: trader } + ); + + decode = decodeLogs(tx.receipt.rawLogs, Affiliates, "PayTradingFeeToAffiliate"); + if (!decode.length) { + throw "Event PayTradingFeeToAffiliate is not fired properly"; + } + + isHeld = decode[0].args["isHeld"]; + expect(isHeld, "First trade affiliates reward must not be in held").to.be.false; + + affiliateRewardsHeld = await sovryn.affiliateRewardsHeld(referrer); + submittedAffiliatesReward = decode[0].args["sovBonusAmount"]; + submittedTokenBonusAmount = decode[0].args["tokenBonusAmount"]; + sovBonusAmountPaid = decode[0].args["sovBonusAmountPaid"]; + submittedReferrer = decode[0].args["referrer"]; + submittedTrader = decode[0].args["trader"]; + + expect( + affiliateRewardsHeld.toString(), + "affiliateRewardHeld should be zero at this point" + ).to.be.equal(new BN(0).toString()); + expect(referrerFee.toString(), "Token bonus rewards is not matched").to.be.equal( + submittedTokenBonusAmount.toString() + ); + + checkSovBonusAmountPaid = new BN(submittedAffiliatesReward).add( + new BN(previousAffiliateRewardsHeld) + ); + expect( + checkSovBonusAmountPaid.toString(), + "Affiliates bonus rewards paid is not matched" + ).to.be.equal(sovBonusAmountPaid.toString()); + + checkedValueShouldBe = new BN(affiliateRewardsHeld).add( + new BN(previousAffiliateRewardsHeld) + ); + expect( + checkedValueShouldBe.toString(), + "Affiliates bonus rewards is not matched" + ).to.be.equal(submittedAffiliatesReward.toString()); + + // Check lockedSOV Balance of the referrer + referrerBalanceInLockedSOV = await lockedSOV.getLockedBalance(referrer); + expect( + referrerBalanceInLockedSOV.toString(), + "Referrer balance in lockedSOV is not matched" + ).to.be.equal(checkSovBonusAmountPaid.toString()); + + expect(submittedReferrer).to.eql(referrer); + expect(submittedTrader).to.eql(trader); + + // Do withdrawal + let referrerTokenBalance = await doc.balanceOf(referrer); + let referrerFee2 = new BN(referrerFee).add(new BN(submittedTokenBonusAmount)); + tx = await sovryn.withdrawAllAffiliatesReferrerTokenFees(referrer, { from: referrer }); + const referrerFeeAfterWithdrawal = await sovryn.affiliatesReferrerBalances( + referrer, + doc.address + ); + let referrerTokenBalanceAfterWithdrawal = await doc.balanceOf(referrer); + expect( + referrerFeeAfterWithdrawal, + "Incorrect all balance after all DoC withdraw" + ).to.be.bignumber.equal(new BN(0)); + expect( + referrerTokenBalanceAfterWithdrawal.sub(referrerTokenBalance).toString() + ).to.be.equal(referrerFee2.toString()); + + await expectEvent.inTransaction( + tx.receipt.rawLogs[0].transactionHash, + Affiliates, + "WithdrawAffiliatesReferrerTokenFees", + { + referrer: referrer, + receiver: referrer, + tokenAddress: doc.address, + amount: referrerFee2.toString(), + } + ); + }); + + it("Test affiliates integration with underlying token with oracle v1Pool", async () => { + /* /// @dev Optimization: Removed feeds init for specific test, relayed to beforeEach hook feeds = await PriceFeeds.new(WRBTC.address, SUSD.address, doc.address); @@ -630,7 +724,7 @@ contract("Affiliates", (accounts) => { ); */ - /* + /* /// @dev Optimization: Removed swap pool init for specific test, relayed to beforeEach hook swapsSovryn = await SwapsImplSovrynSwap.new(); @@ -648,7 +742,7 @@ contract("Affiliates", (accounts) => { await sovryn.setSOVTokenAddress(SUSD.address); */ - /* + /* /// @dev Optimization: Removed default affiliate init for specific test, relayed to beforeEach hook // Change the min referrals to payout to 3 for testing purposes @@ -708,74 +802,99 @@ contract("Affiliates", (accounts) => { expect(referrerBalanceInLockedSOV.toString(), "Referrer balance in lockedSOV is not matched").to.be.equal(new BN(0).toString()); */ - // Change the min referrals to payout to 1 - await sovryn.setMinReferralsToPayoutAffiliates(1); - - previousAffiliateRewardsHeld = await sovryn.affiliateRewardsHeld(referrer); - tx = await loanTokenV2.marginTradeAffiliate( - constants.ZERO_BYTES32, // loanId (0 for new loans) - leverageAmount, // Leverage - loanTokenSent, // loanTokenSent - 0, // - WRBTC.address, // collateralTokenAddress - trader, // trader - 0, // max slippage - referrer, // referrer address, - "0x", // loanDataBytes (only required with ether) - { from: trader } - ); - - decode = decodeLogs(tx.receipt.rawLogs, Affiliates, "PayTradingFeeToAffiliate"); - if (!decode.length) { - throw "Event PayTradingFeeToAffiliate is not fired properly"; - } - - isHeld = decode[0].args["isHeld"]; - expect(isHeld, "First trade affiliates reward must not be in held").to.be.false; - - affiliateRewardsHeld = await sovryn.affiliateRewardsHeld(referrer); - submittedAffiliatesReward = decode[0].args["sovBonusAmount"]; - submittedTokenBonusAmount = decode[0].args["tokenBonusAmount"]; - sovBonusAmountPaid = decode[0].args["sovBonusAmountPaid"]; - - expect(affiliateRewardsHeld.toString(), "affiliateRewardHeld should be zero at this point").to.be.equal(new BN(0).toString()); - expect(referrerFee.toString(), "Token bonus rewards is not matched").to.be.equal(submittedTokenBonusAmount.toString()); - - checkSovBonusAmountPaid = new BN(submittedAffiliatesReward).add(new BN(previousAffiliateRewardsHeld)); - expect(checkSovBonusAmountPaid.toString(), "Affiliates bonus rewards paid is not matched").to.be.equal( - sovBonusAmountPaid.toString() - ); - - checkedValueShouldBe = new BN(affiliateRewardsHeld).add(new BN(previousAffiliateRewardsHeld)); - expect(checkedValueShouldBe.toString(), "Affiliates bonus rewards is not matched").to.be.equal( - submittedAffiliatesReward.toString() - ); - - // Check lockedSOV Balance of the referrer - referrerBalanceInLockedSOV = await lockedSOV.getLockedBalance(referrer); - expect(referrerBalanceInLockedSOV.toString(), "Referrer balance in lockedSOV is not matched").to.be.equal( - checkSovBonusAmountPaid.toString() - ); - - // Do withdrawal - let referrerTokenBalance = await doc.balanceOf(referrer); - let referrerFee2 = new BN(referrerFee).add(new BN(submittedTokenBonusAmount)); - tx = await sovryn.withdrawAllAffiliatesReferrerTokenFees(referrer, { from: referrer }); - const referrerFeeAfterWithdrawal = await sovryn.affiliatesReferrerBalances(referrer, doc.address); - let referrerTokenBalanceAfterWithdrawal = await doc.balanceOf(referrer); - expect(referrerFeeAfterWithdrawal, "Incorrect all balance after all DoC withdraw").to.be.bignumber.equal(new BN(0)); - expect(referrerTokenBalanceAfterWithdrawal.sub(referrerTokenBalance).toString()).to.be.equal(referrerFee2.toString()); - - await expectEvent.inTransaction(tx.receipt.rawLogs[0].transactionHash, Affiliates, "WithdrawAffiliatesReferrerTokenFees", { - referrer: referrer, - receiver: referrer, - tokenAddress: doc.address, - amount: referrerFee2.toString(), - }); - }); - - it("Check get estimation token value in rBTC", async () => { - /* + // Change the min referrals to payout to 1 + await sovryn.setMinReferralsToPayoutAffiliates(1); + + previousAffiliateRewardsHeld = await sovryn.affiliateRewardsHeld(referrer); + tx = await loanTokenV2.marginTradeAffiliate( + constants.ZERO_BYTES32, // loanId (0 for new loans) + leverageAmount, // Leverage + loanTokenSent, // loanTokenSent + 0, // + WRBTC.address, // collateralTokenAddress + trader, // trader + 0, // max slippage + referrer, // referrer address, + "0x", // loanDataBytes (only required with ether) + { from: trader } + ); + + decode = decodeLogs(tx.receipt.rawLogs, Affiliates, "PayTradingFeeToAffiliate"); + if (!decode.length) { + throw "Event PayTradingFeeToAffiliate is not fired properly"; + } + + isHeld = decode[0].args["isHeld"]; + expect(isHeld, "First trade affiliates reward must not be in held").to.be.false; + + affiliateRewardsHeld = await sovryn.affiliateRewardsHeld(referrer); + submittedAffiliatesReward = decode[0].args["sovBonusAmount"]; + submittedTokenBonusAmount = decode[0].args["tokenBonusAmount"]; + sovBonusAmountPaid = decode[0].args["sovBonusAmountPaid"]; + + expect( + affiliateRewardsHeld.toString(), + "affiliateRewardHeld should be zero at this point" + ).to.be.equal(new BN(0).toString()); + expect(referrerFee.toString(), "Token bonus rewards is not matched").to.be.equal( + submittedTokenBonusAmount.toString() + ); + + checkSovBonusAmountPaid = new BN(submittedAffiliatesReward).add( + new BN(previousAffiliateRewardsHeld) + ); + expect( + checkSovBonusAmountPaid.toString(), + "Affiliates bonus rewards paid is not matched" + ).to.be.equal(sovBonusAmountPaid.toString()); + + checkedValueShouldBe = new BN(affiliateRewardsHeld).add( + new BN(previousAffiliateRewardsHeld) + ); + expect( + checkedValueShouldBe.toString(), + "Affiliates bonus rewards is not matched" + ).to.be.equal(submittedAffiliatesReward.toString()); + + // Check lockedSOV Balance of the referrer + referrerBalanceInLockedSOV = await lockedSOV.getLockedBalance(referrer); + expect( + referrerBalanceInLockedSOV.toString(), + "Referrer balance in lockedSOV is not matched" + ).to.be.equal(checkSovBonusAmountPaid.toString()); + + // Do withdrawal + let referrerTokenBalance = await doc.balanceOf(referrer); + let referrerFee2 = new BN(referrerFee).add(new BN(submittedTokenBonusAmount)); + tx = await sovryn.withdrawAllAffiliatesReferrerTokenFees(referrer, { from: referrer }); + const referrerFeeAfterWithdrawal = await sovryn.affiliatesReferrerBalances( + referrer, + doc.address + ); + let referrerTokenBalanceAfterWithdrawal = await doc.balanceOf(referrer); + expect( + referrerFeeAfterWithdrawal, + "Incorrect all balance after all DoC withdraw" + ).to.be.bignumber.equal(new BN(0)); + expect( + referrerTokenBalanceAfterWithdrawal.sub(referrerTokenBalance).toString() + ).to.be.equal(referrerFee2.toString()); + + await expectEvent.inTransaction( + tx.receipt.rawLogs[0].transactionHash, + Affiliates, + "WithdrawAffiliatesReferrerTokenFees", + { + referrer: referrer, + receiver: referrer, + tokenAddress: doc.address, + amount: referrerFee2.toString(), + } + ); + }); + + it("Check get estimation token value in rBTC", async () => { + /* /// @dev Optimization: Removed feeds init for specific test, relayed to beforeEach hook feeds = await PriceFeeds.new(WRBTC.address, SUSD.address, doc.address); @@ -884,7 +1003,7 @@ contract("Affiliates", (accounts) => { ); */ - /* + /* /// @dev Optimization: Removed swap pool init for specific test, relayed to beforeEach hook swapsSovryn = await SwapsImplSovrynSwap.new(); @@ -902,7 +1021,7 @@ contract("Affiliates", (accounts) => { await sovryn.setSOVTokenAddress(SUSD.address); */ - /* + /* /// @dev Optimization: Removed default affiliate init for specific test, relayed to beforeEach hook // Change the min referrals to payout to 3 for testing purposes @@ -962,16 +1081,16 @@ contract("Affiliates", (accounts) => { expect(referrerBalanceInLockedSOV.toString(), "Referrer balance in lockedSOV is not matched").to.be.equal(new BN(0).toString()); */ - // Check est token rewards balance in rbtc - const tokenRewards = await sovryn.getAffiliatesReferrerTokenBalance(referrer, doc.address); + // Check est token rewards balance in rbtc + const tokenRewards = await sovryn.getAffiliatesReferrerTokenBalance(referrer, doc.address); - const tokenRewardsInRBTC = await sovryn.getAffiliatesTokenRewardsValueInRbtc(referrer); + const tokenRewardsInRBTC = await sovryn.getAffiliatesTokenRewardsValueInRbtc(referrer); - expect(tokenRewardsInRBTC.toString()).to.be.equal( - tokenRewards - .mul(new BN(wei("1", "ether"))) - .div(new BN(wrBTCPrice)) - .toString() - ); - }); + expect(tokenRewardsInRBTC.toString()).to.be.equal( + tokenRewards + .mul(new BN(wei("1", "ether"))) + .div(new BN(wrBTCPrice)) + .toString() + ); + }); }); diff --git a/tests/farm/LiquidityMining.js b/tests/farm/LiquidityMining.js index 536fcd204..06f3771ce 100644 --- a/tests/farm/LiquidityMining.js +++ b/tests/farm/LiquidityMining.js @@ -34,1837 +34,2244 @@ const TestLockedSOV = artifacts.require("LockedSOVMockup"); const Wrapper = artifacts.require("RBTCWrapperProxyMockup"); contract("LiquidityMining", (accounts) => { - const name = "Test SOV Token"; - const symbol = "TST"; - - const PRECISION = 1e12; - - const rewardTokensPerBlock = new BN(3); - const startDelayBlocks = new BN(1); - const numberOfBonusBlocks = new BN(50); - - // The % which determines how much will be unlocked immediately. - /// @dev 10000 is 100% - const unlockedImmediatelyPercent = new BN(1000); // 10% - - let root, account1, account2, account3, account4; - let SOVToken, token1, token2, token3, liquidityMiningConfigToken; - let liquidityMining, wrapper; - let lockedSOVAdmins, lockedSOV; - - async function deploymentAndInit() { - SOVToken = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); - token1 = await TestToken.new("Test token 1", "TST-1", 18, TOTAL_SUPPLY); - token2 = await TestToken.new("Test token 2", "TST-2", 18, TOTAL_SUPPLY); - token3 = await TestToken.new("Test token 3", "TST-3", 18, TOTAL_SUPPLY); - liquidityMiningConfigToken = await LiquidityMiningConfigToken.new(); - lockedSOVAdmins = [account1, account2]; - - lockedSOV = await TestLockedSOV.new(SOVToken.address, lockedSOVAdmins); - - await deployLiquidityMining(); - await liquidityMining.initialize( - SOVToken.address, - rewardTokensPerBlock, - startDelayBlocks, - numberOfBonusBlocks, - wrapper.address, - lockedSOV.address, - unlockedImmediatelyPercent - ); - } - - async function deploymentAndInitFixture(_wallets, _provider) { - await deploymentAndInit(); - } - - before(async () => { - accounts = await web3.eth.getAccounts(); - [root, account1, account2, account3, account4, ...accounts] = accounts; - }); - - /// @dev Test dummy liquidityMiningConfigToken methods for coverage - describe("liquidityMiningConfigToken", () => { - it("Test liquidityMiningConfigToken methods", async () => { - await loadFixture(deploymentAndInitFixture); - let totalSupply = await liquidityMiningConfigToken.totalSupply(); - // console.log("totalSupply = ", totalSupply.toString()); - expect(totalSupply).to.be.bignumber.equal(new BN(0)); - - let transferReturn = await liquidityMiningConfigToken.transfer.call(account1, 0); - // console.log("transferReturn = ", transferReturn); - expect(transferReturn).equal(false); - - let allowance = await liquidityMiningConfigToken.allowance(account1, account2); - // console.log("allowance = ", allowance.toString()); - expect(allowance).to.be.bignumber.equal(new BN(0)); - - let approveReturn = await liquidityMiningConfigToken.approve.call(account1, 0); - // console.log("approveReturn = ", approveReturn); - expect(approveReturn).equal(false); - - let transferFromReturn = await liquidityMiningConfigToken.transferFrom.call(account1, account2, 0); - // console.log("transferFromReturn = ", transferFromReturn); - expect(transferFromReturn).equal(false); - }); - }); - - describe("initialize", () => { - it("sets the expected values", async () => { - await loadFixture(deploymentAndInitFixture); - await deployLiquidityMining(); - let tx = await liquidityMining.initialize( - SOVToken.address, - rewardTokensPerBlock, - startDelayBlocks, - numberOfBonusBlocks, - wrapper.address, - lockedSOV.address, - unlockedImmediatelyPercent - ); - - let _SOV = await liquidityMining.SOV(); - let _rewardTokensPerBlock = await liquidityMining.rewardTokensPerBlock(); - let _startBlock = await liquidityMining.startBlock(); - let _bonusEndBlock = await liquidityMining.bonusEndBlock(); - let _wrapper = await liquidityMining.wrapper(); - - let blockNumber = new BN(tx.receipt.blockNumber); - - expect(_SOV).equal(SOVToken.address); - expect(_rewardTokensPerBlock).bignumber.equal(rewardTokensPerBlock); - expect(_startBlock).bignumber.equal(startDelayBlocks.add(blockNumber)); - expect(_bonusEndBlock).bignumber.equal(startDelayBlocks.add(blockNumber).add(numberOfBonusBlocks)); - expect(_wrapper).equal(wrapper.address); - }); - - it("fails if not an owner or an admin", async () => { - await deployLiquidityMining(); - await expectRevert( - liquidityMining.initialize( - SOVToken.address, - rewardTokensPerBlock, - startDelayBlocks, - numberOfBonusBlocks, - wrapper.address, - lockedSOV.address, - unlockedImmediatelyPercent, - { from: account1 } - ), - "unauthorized" - ); - - await liquidityMining.addAdmin(account1); - await liquidityMining.initialize( - SOVToken.address, - rewardTokensPerBlock, - startDelayBlocks, - numberOfBonusBlocks, - wrapper.address, - lockedSOV.address, - unlockedImmediatelyPercent, - { from: account1 } - ); - }); - - it("fails if _startBlock = 0", async () => { - await deployLiquidityMining(); - await expectRevert( - liquidityMining.initialize( - SOVToken.address, - rewardTokensPerBlock, - 0, - numberOfBonusBlocks, - wrapper.address, - lockedSOV.address, - unlockedImmediatelyPercent - ), - "Invalid start block" - ); - }); - - it("fails if already initialized", async () => { - await deploymentAndInit(); - await expectRevert( - liquidityMining.initialize( - SOVToken.address, - rewardTokensPerBlock, - startDelayBlocks, - numberOfBonusBlocks, - wrapper.address, - lockedSOV.address, - unlockedImmediatelyPercent - ), - "Already initialized" - ); - }); - - it("fails if the 0 address is passed as token address", async () => { - await deployLiquidityMining(); - await expectRevert( - liquidityMining.initialize( - ZERO_ADDRESS, - rewardTokensPerBlock, - startDelayBlocks, - numberOfBonusBlocks, - wrapper.address, - lockedSOV.address, - unlockedImmediatelyPercent - ), - "Invalid token address" - ); - }); - - it("fails if unlockedImmediatelyPercent >= 10000", async () => { - await deployLiquidityMining(); - await expectRevert( - liquidityMining.initialize( - SOVToken.address, - rewardTokensPerBlock, - startDelayBlocks, - numberOfBonusBlocks, - wrapper.address, - lockedSOV.address, - 12345 - ), - "Unlocked immediately percent has to be less than 10000." - ); - }); - }); - - describe("addAdmin", () => { - it("adds admin", async () => { - let tx = await liquidityMining.addAdmin(account1); - - expectEvent(tx, "AdminAdded", { - admin: account1, - }); - - let isAdmin = await liquidityMining.admins(account1); - expect(isAdmin).equal(true); - }); - - it("fails sender isn't an owner", async () => { - await expectRevert(liquidityMining.addAdmin(account1, { from: account1 }), "unauthorized"); - }); - }); - - describe("removeAdmin", () => { - it("adds admin", async () => { - await liquidityMining.addAdmin(account1); - let tx = await liquidityMining.removeAdmin(account1); - - expectEvent(tx, "AdminRemoved", { - admin: account1, - }); - - let isAdmin = await liquidityMining.admins(account1); - expect(isAdmin).equal(false); - }); - - it("fails sender isn't an owner", async () => { - await expectRevert(liquidityMining.removeAdmin(account1, { from: account1 }), "unauthorized"); - }); - }); - - describe("setLockedSOV", () => { - it("sets the expected values", async () => { - let newLockedSOV = account2; - await liquidityMining.setLockedSOV(newLockedSOV); - - let _lockedSOV = await liquidityMining.lockedSOV(); - expect(_lockedSOV).equal(newLockedSOV); - }); - - it("fails if not an owner and an admin", async () => { - await expectRevert(liquidityMining.setLockedSOV(account2, { from: account1 }), "unauthorized"); - - await liquidityMining.addAdmin(account1); - await liquidityMining.setLockedSOV(account2, { from: account1 }); - }); - - it("fails if zero address passed", async () => { - await expectRevert(liquidityMining.setLockedSOV(ZERO_ADDRESS), "Invalid lockedSOV Address."); - }); - }); - - describe("setUnlockedImmediatelyPercent", () => { - it("sets the expected values", async () => { - let newUnlockedImmediatelyPercent = new BN(2000); - await liquidityMining.setUnlockedImmediatelyPercent(newUnlockedImmediatelyPercent); - - let _unlockedImmediatelyPercent = await liquidityMining.unlockedImmediatelyPercent(); - expect(_unlockedImmediatelyPercent).bignumber.equal(newUnlockedImmediatelyPercent); - }); - - it("fails if not an owner or an admin", async () => { - await deploymentAndInit(); - await expectRevert(liquidityMining.setUnlockedImmediatelyPercent(1000, { from: account1 }), "unauthorized"); - - await liquidityMining.addAdmin(account1); - await liquidityMining.setUnlockedImmediatelyPercent(1000, { from: account1 }); - }); - - it("fails if unlockedImmediatelyPercent >= 10000", async () => { - await expectRevert( - liquidityMining.setUnlockedImmediatelyPercent(100000), - "Unlocked immediately percent has to be less than 10000." - ); - }); - }); - - describe("setWrapper", () => { - it("sets the expected values", async () => { - let newWrapper = account2; - await liquidityMining.setWrapper(newWrapper); - - let _wrapper = await liquidityMining.wrapper(); - expect(_wrapper).equal(newWrapper); - }); - - it("fails if not an owner or an admin", async () => { - await deploymentAndInit(); - await expectRevert(liquidityMining.setWrapper(account2, { from: account1 }), "unauthorized"); - - await liquidityMining.addAdmin(account1); - await liquidityMining.setWrapper(account2, { from: account1 }); - }); - }); - - describe("stopMining", () => { - it("should set end block", async () => { - let tx = await liquidityMining.stopMining(); - - let blockNumber = new BN(tx.receipt.blockNumber); - let _endBlock = await liquidityMining.endBlock(); - expect(_endBlock).bignumber.equal(blockNumber); - }); - - it("fails if not an owner or an admin", async () => { - await deploymentAndInit(); - await expectRevert(liquidityMining.stopMining({ from: account1 }), "unauthorized"); - - await liquidityMining.addAdmin(account1); - await liquidityMining.stopMining({ from: account1 }); - }); - - it("fails if already stopped", async () => { - await deploymentAndInit(); - await liquidityMining.stopMining(); - await expectRevert(liquidityMining.stopMining(), "Already stopped"); - }); - }); - - describe("transferSOV", () => { - it("should be able to transfer SOV", async () => { - let amount = new BN(1000); - await SOVToken.transfer(liquidityMining.address, amount); - - let balanceBefore = await SOVToken.balanceOf(account1); - await liquidityMining.transferSOV(account1, amount); - let balanceAfter = await SOVToken.balanceOf(account1); - - expect(amount).bignumber.equal(balanceAfter.sub(balanceBefore)); - }); - - it("only owner or admin should be able to transfer", async () => { - await expectRevert(liquidityMining.transferSOV(account1, 1000, { from: account1 }), "unauthorized"); - - await liquidityMining.addAdmin(account1); - await liquidityMining.transferSOV(account1, 1000, { from: account1 }); - }); - - it("fails if the 0 address is passed as receiver address", async () => { - await expectRevert(liquidityMining.transferSOV(ZERO_ADDRESS, 1000), "Receiver address invalid"); - }); - - it("fails if the 0 is passed as an amount", async () => { - await expectRevert(liquidityMining.transferSOV(account1, 0), "Amount invalid"); - }); - }); - - describe("add", () => { - it("should be able to add pool token", async () => { - let allocationPoint = new BN(1); - let tx = await liquidityMining.add(token1.address, allocationPoint, false); - - expect(await liquidityMining.totalAllocationPoint()).bignumber.equal(allocationPoint); - - let poolInfo = await liquidityMining.poolInfoList(0); - expect(poolInfo.poolToken).equal(token1.address); - expect(poolInfo.allocationPoint).bignumber.equal(allocationPoint); - let blockNumber = new BN(tx.receipt.blockNumber); - expect(poolInfo.lastRewardBlock).bignumber.equal(blockNumber); - expect(poolInfo.accumulatedRewardPerShare).bignumber.equal(new BN(0)); - - expect(await liquidityMining.getPoolLength()).bignumber.equal(new BN(1)); - - expectEvent(tx, "PoolTokenAdded", { - user: root, - poolToken: token1.address, - allocationPoint: allocationPoint, - }); - }); - - it("should be able to add 2 pool tokens and update pools", async () => { - await deploymentAndInit(); - let allocationPoint1 = new BN(1); - let tx1 = await liquidityMining.add(token1.address, allocationPoint1, false); - - expect(await liquidityMining.totalAllocationPoint()).bignumber.equal(allocationPoint1); - - expectEvent(tx1, "PoolTokenAdded", { - user: root, - poolToken: token1.address, - allocationPoint: allocationPoint1, - }); - - let allocationPoint2 = new BN(2); - let tx2 = await liquidityMining.add(token2.address, allocationPoint2, true); - - expect(await liquidityMining.totalAllocationPoint()).bignumber.equal(allocationPoint1.add(allocationPoint2)); - - expectEvent(tx2, "PoolTokenAdded", { - user: root, - poolToken: token2.address, - allocationPoint: allocationPoint2, - }); - - let poolInfo1 = await liquidityMining.getPoolInfo(token1.address); - let poolInfo2 = await liquidityMining.getPoolInfo(token2.address); - expect(poolInfo1.lastRewardBlock).bignumber.equal(poolInfo2.lastRewardBlock); - }); - - it("fails if the 0 allocation point is passed", async () => { - await expectRevert(liquidityMining.add(token1.address, new BN(0), false), "Invalid allocation point"); - }); - - it("fails if the 0 address is passed as token address", async () => { - await expectRevert(liquidityMining.add(ZERO_ADDRESS, new BN(1), false), "Invalid token address"); - }); - - it("fails if token already added", async () => { - await deploymentAndInit(); - await liquidityMining.add(token1.address, new BN(1), false); - await expectRevert(liquidityMining.add(token1.address, new BN(1), false), "Token already added"); - }); - - it("only owner or admin should be able to add pool token", async () => { - await deploymentAndInit(); - await expectRevert(liquidityMining.add(token2.address, new BN(1), false, { from: account1 }), "unauthorized"); - - await liquidityMining.addAdmin(account1); - await liquidityMining.add(token2.address, new BN(1), false, { from: account1 }); - }); - }); - - describe("update", () => { - it("should be able to update pool token", async () => { - await deploymentAndInit(); - let oldAllocationPoint = new BN(1); - await liquidityMining.add(token1.address, oldAllocationPoint, false); - - let newAllocationPoint = new BN(2); - let tx = await liquidityMining.update(token1.address, newAllocationPoint, false); - - expect(await liquidityMining.totalAllocationPoint()).bignumber.equal(newAllocationPoint); - - let poolInfo = await liquidityMining.getPoolInfo(token1.address); - let blockNumber = new BN(tx.receipt.blockNumber); - checkPoolInfo(poolInfo, token1.address, newAllocationPoint, blockNumber, new BN(0)); - - expect(await liquidityMining.getPoolLength()).bignumber.equal(new BN(1)); - - expectEvent(tx, "PoolTokenUpdated", { - user: root, - poolToken: token1.address, - newAllocationPoint: newAllocationPoint, - oldAllocationPoint: oldAllocationPoint, - }); - }); - - it("should be able to update pool token and update pools", async () => { - await deploymentAndInit(); - let oldAllocationPoint = new BN(1); - await liquidityMining.add(token1.address, oldAllocationPoint, false); - - await liquidityMining.add(token2.address, oldAllocationPoint, false); - - let newAllocationPoint = new BN(2); - let tx = await liquidityMining.update(token1.address, newAllocationPoint, true); - - expect(await liquidityMining.totalAllocationPoint()).bignumber.equal(oldAllocationPoint.add(newAllocationPoint)); - - let poolInfo = await liquidityMining.getPoolInfo(token2.address); - expect(poolInfo.lastRewardBlock).bignumber.equal(new BN(tx.receipt.blockNumber)); - }); - - it("fails if token wasn't added", async () => { - await deploymentAndInit(); - await expectRevert(liquidityMining.update(token1.address, new BN(1), false), "Pool token not found"); - }); - - it("only owner or admin should be able to update pool token", async () => { - await liquidityMining.add(token2.address, new BN(1), false); - await expectRevert(liquidityMining.update(token2.address, new BN(1), false, { from: account1 }), "unauthorized"); - - await liquidityMining.addAdmin(account1); - await liquidityMining.update(token2.address, new BN(1), false, { from: account1 }); - }); - }); - - describe("updateTokens", () => { - it("should be able to update 2 pool tokens", async () => { - await deploymentAndInit(); - let poolTokens = [token1.address, token2.address, token3.address]; - let oldAllocationPoints = [new BN(1), new BN(2), new BN(3)]; - - for (let i = 0; i < poolTokens.length; i++) { - await liquidityMining.add(poolTokens[i], oldAllocationPoints[i], false); - } - - let newAllocationPoints = [new BN(101), new BN(102), new BN(3)]; - let tx = await liquidityMining.updateTokens(poolTokens, newAllocationPoints, true); - - let totalAllocationPoint = new BN(0); - for (let i = 0; i < newAllocationPoints.length; i++) { - totalAllocationPoint = totalAllocationPoint.add(newAllocationPoints[i]); - } - expect(await liquidityMining.totalAllocationPoint()).bignumber.equal(totalAllocationPoint); - - let blockNumber = new BN(tx.receipt.blockNumber); - for (let i = 0; i < poolTokens.length - 1; i++) { - let poolInfo = await liquidityMining.getPoolInfo(poolTokens[i]); - checkPoolInfo(poolInfo, poolTokens[i], newAllocationPoints[i], blockNumber, new BN(0)); - - expectEvent(tx, "PoolTokenUpdated", { - user: root, - poolToken: poolTokens[i], - newAllocationPoint: newAllocationPoints[i], - oldAllocationPoint: oldAllocationPoints[i], - }); - } - - expect(await liquidityMining.getPoolLength()).bignumber.equal(new BN(3)); - - let poolInfo = await liquidityMining.getPoolInfo(poolTokens[poolTokens.length - 1]); - expect(poolInfo.lastRewardBlock).bignumber.equal(blockNumber); - }); - - it("fails if token wasn't added", async () => { - await deploymentAndInit(); - await expectRevert(liquidityMining.updateTokens([token1.address], [new BN(1)], false), "Pool token not found"); - }); - - it("fails if arrays have different length", async () => { - await liquidityMining.add(token2.address, new BN(1), false); - await expectRevert(liquidityMining.updateTokens([token1.address, token2.address], [new BN(1)], false), "Arrays mismatch"); - }); - - it("only owner or admin should be able to update pool token", async () => { - await deploymentAndInit(); - await liquidityMining.add(token2.address, new BN(1), false); - await expectRevert(liquidityMining.updateTokens([token2.address], [new BN(1)], false, { from: account1 }), "unauthorized"); - - await liquidityMining.addAdmin(account1); - await liquidityMining.updateTokens([token2.address], [new BN(1)], false, { from: account1 }); - }); - }); - - describe("deposit", () => { - let allocationPoint = new BN(1); - let amount = new BN(1000); - - beforeEach(async () => { - await deploymentAndInit(); - await liquidityMining.add(token1.address, allocationPoint, false); - await mineBlocks(1); - - await token1.mint(account1, amount); - await token1.approve(liquidityMining.address, amount, { from: account1 }); - }); - - it("should be able to deposit", async () => { - let tx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - let poolInfo = await liquidityMining.getPoolInfo(token1.address); - let blockNumber = new BN(tx.receipt.blockNumber); - checkPoolInfo(poolInfo, token1.address, allocationPoint, blockNumber, new BN(0)); - - await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); - - expectEvent(tx, "Deposit", { - user: account1, - poolToken: token1.address, - amount: amount, - }); - }); - - it("should be able to deposit using wrapper", async () => { - let tx = await liquidityMining.deposit(token1.address, amount, account2, { from: account1 }); - - let poolInfo = await liquidityMining.getPoolInfo(token1.address); - let blockNumber = new BN(tx.receipt.blockNumber); - checkPoolInfo(poolInfo, token1.address, allocationPoint, blockNumber, new BN(0)); - - await checkUserPoolTokens(account2, token1, amount, amount, new BN(0)); - - expectEvent(tx, "Deposit", { - user: account2, - poolToken: token1.address, - amount: amount, - }); - }); - - it("fails if token pool token not found", async () => { - await expectRevert(liquidityMining.deposit(account1, amount, ZERO_ADDRESS, { from: account1 }), "Pool token not found"); - }); - }); - - describe("claimReward", () => { - let allocationPoint = new BN(1); - let amount = new BN(1000); - - beforeEach(async () => { - await deploymentAndInit(); - await liquidityMining.add(token1.address, allocationPoint, false); - await mineBlocks(1); - - await token1.mint(account1, amount); - await token1.approve(liquidityMining.address, amount, { from: account1 }); - }); - - it("shouldn't be able to claim reward (will not be claimed without SOV tokens)", async () => { - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - await expectRevert(liquidityMining.claimReward(token1.address, ZERO_ADDRESS, { from: account1 }), "Claiming reward failed"); - }); - - it("should be able to claim reward (will be claimed with SOV tokens)", async () => { - let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - let depositBlockNumber = new BN(depositTx.receipt.blockNumber); - await SOVToken.transfer(liquidityMining.address, new BN(1000)); - - let tx = await liquidityMining.claimReward(token1.address, ZERO_ADDRESS, { from: account1 }); - - let totalUsersBalance = await liquidityMining.totalUsersBalance(); - expect(totalUsersBalance).bignumber.equal(new BN(0)); - - let poolInfo = await liquidityMining.getPoolInfo(token1.address); - let latestBlockNumber = new BN(tx.receipt.blockNumber); - checkPoolInfo(poolInfo, token1.address, allocationPoint, latestBlockNumber, new BN(-1)); - - await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); - let userReward = await checkUserReward(account1, token1, depositBlockNumber, latestBlockNumber); - - // withdrawAndStakeTokensFrom was invoked - let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); - let lockedBalance = await lockedSOV.getLockedBalance(account1); - expect(unlockedBalance).bignumber.equal(new BN(0)); - expect(lockedBalance).bignumber.equal(new BN(0)); - - expectEvent(tx, "RewardClaimed", { - user: account1, - poolToken: token1.address, - amount: userReward, - }); - }); - - it("should be able to claim reward using wrapper", async () => { - let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - let depositBlockNumber = new BN(depositTx.receipt.blockNumber); - await SOVToken.transfer(liquidityMining.address, new BN(1000)); - - let tx = await wrapper.claimReward(token1.address, { from: account1 }); - - let poolInfo = await liquidityMining.getPoolInfo(token1.address); - let latestBlockNumber = new BN(tx.receipt.blockNumber); - checkPoolInfo(poolInfo, token1.address, allocationPoint, latestBlockNumber, new BN(-1)); - - await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); - await checkUserReward(account1, token1, depositBlockNumber, latestBlockNumber); - - // withdrawAndStakeTokensFrom was invoked - let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); - let lockedBalance = await lockedSOV.getLockedBalance(account1); - expect(unlockedBalance).bignumber.equal(new BN(0)); - expect(lockedBalance).bignumber.equal(new BN(0)); - }); - - it("fails if token pool token not found", async () => { - await expectRevert(liquidityMining.claimReward(account1, ZERO_ADDRESS, { from: account1 }), "Pool token not found"); - }); - }); - - describe("claimRewardFromAllPools", () => { - let allocationPoint = new BN(1); - let amount = new BN(1000); - - beforeEach(async () => { - await deploymentAndInit(); - await liquidityMining.add(token1.address, allocationPoint, false); - await liquidityMining.add(token2.address, allocationPoint, false); - await mineBlocks(1); - - await token1.mint(account1, amount); - await token1.approve(liquidityMining.address, amount, { from: account1 }); - await token2.mint(account1, amount); - await token2.approve(liquidityMining.address, amount, { from: account1 }); - }); - - it("shouldn't be able to claim reward (will not be claimed without SOV tokens)", async () => { - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - await expectRevert(liquidityMining.claimRewardFromAllPools(ZERO_ADDRESS, { from: account1 }), "Claiming reward failed"); - }); - - it("should be able to claim reward (will be claimed with SOV tokens)", async () => { - let depositTx1 = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - let depositBlockNumber1 = new BN(depositTx1.receipt.blockNumber); - let depositTx2 = await liquidityMining.deposit(token2.address, amount, ZERO_ADDRESS, { from: account1 }); - let depositBlockNumber2 = new BN(depositTx2.receipt.blockNumber); - await SOVToken.transfer(liquidityMining.address, amount.mul(new BN(2))); - - let tx = await liquidityMining.claimRewardFromAllPools(ZERO_ADDRESS, { from: account1 }); - - let totalUsersBalance = await liquidityMining.totalUsersBalance(); - expect(totalUsersBalance).bignumber.equal(new BN(0)); - - let poolInfo = await liquidityMining.getPoolInfo(token1.address); - let latestBlockNumber = new BN(tx.receipt.blockNumber); - checkPoolInfo(poolInfo, token1.address, allocationPoint, latestBlockNumber, new BN(-1)); - - await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); - let userReward1 = await checkUserReward(account1, token1, depositBlockNumber1, latestBlockNumber); - // we have 2 pools with the same allocation points - userReward1 = userReward1.div(new BN(2)); - - await checkUserPoolTokens(account1, token2, amount, amount, new BN(0)); - let userReward2 = await checkUserReward(account1, token2, depositBlockNumber2, latestBlockNumber); - // we have 2 pools with the same allocation points - userReward2 = userReward2.div(new BN(2)); - - // withdrawAndStakeTokensFrom was invoked - let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); - let lockedBalance = await lockedSOV.getLockedBalance(account1); - expect(unlockedBalance).bignumber.equal(new BN(0)); - expect(lockedBalance).bignumber.equal(new BN(0)); - - expectEvent(tx, "RewardClaimed", { - user: account1, - poolToken: token1.address, - amount: userReward1, - }); - - expect(userReward1, tx.logs[0].args.amount); - expect(token1.address, tx.logs[0].args.poolToken); - expect(userReward2, tx.logs[1].args.amount); - expect(token2.address, tx.logs[1].args.poolToken); - }); - - it("should be able to claim reward using wrapper", async () => { - let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - let depositBlockNumber = new BN(depositTx.receipt.blockNumber); - await SOVToken.transfer(liquidityMining.address, new BN(1000)); - - let tx = await wrapper.claimRewardFromAllPools({ from: account1 }); - - let poolInfo = await liquidityMining.getPoolInfo(token1.address); - let latestBlockNumber = new BN(tx.receipt.blockNumber); - checkPoolInfo(poolInfo, token1.address, allocationPoint, latestBlockNumber, new BN(-1)); - - await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); - await checkUserReward(account1, token1, depositBlockNumber, latestBlockNumber); - - // withdrawAndStakeTokensFrom was invoked - let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); - let lockedBalance = await lockedSOV.getLockedBalance(account1); - expect(unlockedBalance).bignumber.equal(new BN(0)); - expect(lockedBalance).bignumber.equal(new BN(0)); - }); - }); - - describe("withdraw", () => { - let allocationPoint = new BN(1); - let amount = new BN(1000); - - beforeEach(async () => { - await deploymentAndInit(); - await liquidityMining.add(token1.address, allocationPoint, false); - await mineBlocks(1); - - await token1.mint(account1, amount); - await token1.approve(liquidityMining.address, amount, { from: account1 }); - }); - - it("should be able to withdraw (without claiming reward)", async () => { - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - let tx = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - let poolInfo = await liquidityMining.getPoolInfo(token1.address); - let blockNumber = new BN(tx.receipt.blockNumber); - checkPoolInfo(poolInfo, token1.address, allocationPoint, blockNumber, new BN(-1)); - - await checkUserPoolTokens(account1, token1, new BN(0), new BN(0), amount); - - // User's balance on lockedSOV vault - let userRewardBalance = await lockedSOV.getLockedBalance(account1); - expect(userRewardBalance).bignumber.equal(new BN(0)); - - expectEvent(tx, "Withdraw", { - user: account1, - poolToken: token1.address, - amount: amount, - }); - }); - - it("should be able to withdraw (with claiming reward)", async () => { - let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - let depositBlockNumber = new BN(depositTx.receipt.blockNumber); - await SOVToken.transfer(liquidityMining.address, new BN(1000)); - - let tx = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - let totalUsersBalance = await liquidityMining.totalUsersBalance(); - expect(totalUsersBalance).bignumber.equal(new BN(0)); - - let poolInfo = await liquidityMining.getPoolInfo(token1.address); - let latestBlockNumber = new BN(tx.receipt.blockNumber); - checkPoolInfo(poolInfo, token1.address, allocationPoint, latestBlockNumber, new BN(-1)); - - await checkUserPoolTokens(account1, token1, new BN(0), new BN(0), amount); - let userReward = await checkUserReward(account1, token1, depositBlockNumber, latestBlockNumber); - - // withdrawAndStakeTokensFrom was not invoked - let expectedUnlockedBalance = userReward.mul(unlockedImmediatelyPercent).div(new BN(10000)); - let expectedLockedBalance = userReward.sub(expectedUnlockedBalance); - let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); - let lockedBalance = await lockedSOV.getLockedBalance(account1); - expect(unlockedBalance).bignumber.equal(expectedUnlockedBalance); - expect(lockedBalance).bignumber.equal(expectedLockedBalance); - - expectEvent(tx, "Withdraw", { - user: account1, - poolToken: token1.address, - amount: amount, - }); - - expectEvent(tx, "RewardClaimed", { - user: account1, - poolToken: token1.address, - amount: userReward, - }); - }); - - it("should be able to withdraw using wrapper", async () => { - let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - let depositBlockNumber = new BN(depositTx.receipt.blockNumber); - await SOVToken.transfer(liquidityMining.address, new BN(1000)); - - let tx = await wrapper.withdraw(token1.address, amount, { from: account1 }); - - let poolInfo = await liquidityMining.getPoolInfo(token1.address); - let latestBlockNumber = new BN(tx.receipt.blockNumber); - checkPoolInfo(poolInfo, token1.address, allocationPoint, latestBlockNumber, new BN(-1)); - - await checkUserPoolTokens(account1, token1, new BN(0), new BN(0), amount, wrapper.address); - await checkUserReward(account1, token1, depositBlockNumber, latestBlockNumber); - }); - - it("fails if token pool token not found", async () => { - await expectRevert(liquidityMining.withdraw(account1, amount, ZERO_ADDRESS, { from: account1 }), "Pool token not found"); - }); - - it("fails if token pool token not found", async () => { - await expectRevert( - liquidityMining.withdraw(token1.address, amount.mul(new BN(2)), ZERO_ADDRESS, { from: account1 }), - "Not enough balance" - ); - }); - }); - - describe("emergencyWithdraw", () => { - let allocationPoint = new BN(1); - let amount = new BN(1000); - - beforeEach(async () => { - await deploymentAndInit(); - await liquidityMining.add(token1.address, allocationPoint, false); - await mineBlocks(1); - - await token1.mint(account1, amount); - await token1.approve(liquidityMining.address, amount, { from: account1 }); - }); - - it("should be able to withdraw", async () => { - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - let tx = await liquidityMining.emergencyWithdraw(token1.address, { from: account1 }); - - let totalUsersBalance = await liquidityMining.totalUsersBalance(); - expect(totalUsersBalance).bignumber.equal(new BN(0)); - - await checkUserPoolTokens(account1, token1, new BN(0), new BN(0), amount); - - let userInfo = await liquidityMining.getUserInfo(token1.address, account1); - expect(userInfo.rewardDebt).bignumber.equal(new BN(0)); - expect(userInfo.accumulatedReward).bignumber.equal(new BN(0)); - - let bonusBlockMultiplier = await liquidityMining.BONUS_BLOCK_MULTIPLIER(); - let expectedAccumulatedReward = rewardTokensPerBlock.mul(bonusBlockMultiplier); - expectEvent(tx, "EmergencyWithdraw", { - user: account1, - poolToken: token1.address, - amount: amount, - accumulatedReward: expectedAccumulatedReward, - }); - }); - - it("fails if token pool token not found", async () => { - await expectRevert(liquidityMining.emergencyWithdraw(account1, { from: account1 }), "Pool token not found"); - }); - }); - - describe("getPassedBlocksWithBonusMultiplier", () => { - it("check calculation", async () => { - let bonusBlockMultiplier = await liquidityMining.BONUS_BLOCK_MULTIPLIER(); - let startBlock = await liquidityMining.startBlock(); - let bonusEndBlock = await liquidityMining.bonusEndBlock(); - let blocks; - - // [startBlock, bonusEndBlock] - blocks = await liquidityMining.getPassedBlocksWithBonusMultiplier(startBlock, bonusEndBlock); - expect(blocks).bignumber.equal(numberOfBonusBlocks.mul(bonusBlockMultiplier)); - - // [startBlock - 100, bonusEndBlock] - blocks = await liquidityMining.getPassedBlocksWithBonusMultiplier(startBlock.sub(new BN(100)), bonusEndBlock); - expect(blocks).bignumber.equal(numberOfBonusBlocks.mul(bonusBlockMultiplier)); - - // [startBlock, bonusEndBlock + 100] - let blocksAfterBonusPeriod = new BN(100); - blocks = await liquidityMining.getPassedBlocksWithBonusMultiplier( - startBlock, - bonusEndBlock.add(new BN(blocksAfterBonusPeriod)) - ); - expect(blocks).bignumber.equal(numberOfBonusBlocks.mul(bonusBlockMultiplier).add(blocksAfterBonusPeriod)); - - // [startBlock, stopMining, ... bonusEndBlock] - await mineBlocks(5); - await liquidityMining.stopMining(); - let endBlock = await liquidityMining.endBlock(); - blocks = await liquidityMining.getPassedBlocksWithBonusMultiplier(startBlock, bonusEndBlock); - expect(blocks).bignumber.equal(endBlock.sub(startBlock).mul(bonusBlockMultiplier)); - }); - }); - - describe("getUserAccumulatedReward", () => { - const amount1 = new BN(1000); - const amount2 = new BN(2000); - const allocationPoint1 = new BN(1); - const allocationPoint2 = new BN(2); - const totalAllocationPoint = allocationPoint1.add(allocationPoint2); - let bonusBlockMultiplier; - let bonusEndBlock; - - beforeEach(async () => { - await deploymentAndInit(); - await liquidityMining.add(token1.address, allocationPoint1, false); - await liquidityMining.add(token2.address, allocationPoint2, false); - - await token1.mint(account1, amount1); - await token2.mint(account2, amount2); - - await token1.approve(liquidityMining.address, amount1, { from: account1 }); - await token2.approve(liquidityMining.address, amount2, { from: account2 }); - - bonusBlockMultiplier = await liquidityMining.BONUS_BLOCK_MULTIPLIER(); - bonusEndBlock = await liquidityMining.bonusEndBlock(); - }); - - it("check calculation for no deposits", async () => { - const reward1 = await liquidityMining.getUserAccumulatedReward(token1.address, account1); - const reward2 = await liquidityMining.getUserAccumulatedReward(token2.address, account2); - expect(reward1).bignumber.equal("0"); - expect(reward2).bignumber.equal("0"); - }); - - it("check calculation for single user, token 1, bonus period off", async () => { - await advanceBlocks(bonusEndBlock); - await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { from: account1 }); - await mineBlock(); - let reward = await liquidityMining.getUserAccumulatedReward(token1.address, account1); - - // 1 block has passed, bonus period is off - // users are given 3 tokens per share per block. user1 owns 100% of the shares - // token 1 counts as 1/3 of the pool - // reward = 1 * 3 * 1/3 = 1 - const expectedReward = rewardTokensPerBlock.mul(allocationPoint1).div(totalAllocationPoint); - expect(expectedReward).bignumber.equal("1"); // sanity check - expect(reward).bignumber.equal(expectedReward); - - await mineBlock(); - reward = await liquidityMining.getUserAccumulatedReward(token1.address, account1); - expect(reward).bignumber.equal("2"); - }); - - it("check calculation for single user, token 2, bonus period off", async () => { - await advanceBlocks(bonusEndBlock); - await liquidityMining.deposit(token2.address, amount2, ZERO_ADDRESS, { from: account2 }); - await mineBlock(); - let reward = await liquidityMining.getUserAccumulatedReward(token2.address, account2); - - // 1 block has passed, bonus period is off - // users are given 3 tokens per share per block. user2 owns 100% of the shares - // token 2 counts as 2/3 of the pool - // reward = 1 * 3 * 2/3 = 2 - const expectedReward = rewardTokensPerBlock.mul(allocationPoint2).div(totalAllocationPoint); - expect(expectedReward).bignumber.equal("2"); // sanity check - expect(reward).bignumber.equal(expectedReward); - - await mineBlock(); - reward = await liquidityMining.getUserAccumulatedReward(token2.address, account2); - expect(reward).bignumber.equal("4"); - }); - - it("check calculation for single user, token 1, bonus period on", async () => { - await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { from: account1 }); - await mineBlock(); - const reward = await liquidityMining.getUserAccumulatedReward(token1.address, account1); - - // 1 block has passed, bonus period is on so it counts as 10 blocks, - // users are given 3 tokens per share per block. user1 owns 100% of the shares - // token 1 counts as 1/3 of the pool - // reward = 10 * 3 * 1/3 = 10 - const expectedReward = rewardTokensPerBlock.mul(bonusBlockMultiplier).mul(allocationPoint1).div(totalAllocationPoint); - expect(expectedReward).bignumber.equal("10"); // sanity check - expect(reward).bignumber.equal(expectedReward); - }); - - it("check calculation for single user, token 1, bonus period on, smaller amount", async () => { - await liquidityMining.deposit(token1.address, new BN(1), ZERO_ADDRESS, { from: account1 }); - await mineBlock(); - const reward = await liquidityMining.getUserAccumulatedReward(token1.address, account1); - - // 1 block has passed, bonus period is on so it counts as 10 blocks, - // users are given 3 tokens per share per block. user1 owns 100% of the shares - // token 1 counts as 1/3 of the pool - // reward = 10 * 3 * 1/3 = 10 - // Note that the actual amount deposited plays no role here - expect(reward).bignumber.equal("10"); - }); - - it("check calculation for single user, token 2, bonus period on", async () => { - await liquidityMining.deposit(token2.address, amount2, ZERO_ADDRESS, { from: account2 }); - await mineBlock(); - const reward = await liquidityMining.getUserAccumulatedReward(token2.address, account2); - - // 1 block has passed, bonus period is on so it counts as 10 blocks, - // users are given 3 tokens per share per block. user2 owns 100% of the shares - // token 2 counts as 2/3 of the pool - // reward = 10 * 3 * 2/3 = 20 - const expectedReward = rewardTokensPerBlock.mul(bonusBlockMultiplier).mul(allocationPoint2).div(totalAllocationPoint); - expect(expectedReward).bignumber.equal("20"); // sanity check - expect(reward).bignumber.equal(expectedReward); - }); - - it("check calculation for two users and tokens", async () => { - await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { from: account1 }); - // because automining is on, the following will advance a block - await liquidityMining.deposit(token2.address, amount2, ZERO_ADDRESS, { from: account2 }); - // sanity checks - expect(await liquidityMining.getUserAccumulatedReward(token1.address, account1)).bignumber.equal("10"); - expect(await liquidityMining.getUserAccumulatedReward(token2.address, account2)).bignumber.equal("0"); - await mineBlock(); - - const reward1 = await liquidityMining.getUserAccumulatedReward(token1.address, account1); - const reward2 = await liquidityMining.getUserAccumulatedReward(token2.address, account2); - - // for the first block, user 1 will receive the reward of 10 - // for the second block: - // - user 1 still owns 100% of the shares for token1, so same reward (total 10 + 10 = 20) - // - user 2 owns 100% of the shares for token2, so same reward as in the other cases - expect(reward1).bignumber.equal("20"); - expect(reward2).bignumber.equal("20"); - }); - - it("check calculation for two users, same token (shares taken into account)", async () => { - const token = token1; - const amount = amount1; - await token.mint(account2, amount); - await token.approve(liquidityMining.address, amount, { from: account2 }); - - await liquidityMining.deposit(token.address, amount, ZERO_ADDRESS, { from: account1 }); - // because automining is on, the following will advance a block - await liquidityMining.deposit(token.address, amount, ZERO_ADDRESS, { from: account2 }); - // sanity checks - expect(await liquidityMining.getUserAccumulatedReward(token.address, account1)).bignumber.equal("10"); - expect(await liquidityMining.getUserAccumulatedReward(token.address, account2)).bignumber.equal("0"); - await mineBlock(); - - const reward1 = await liquidityMining.getUserAccumulatedReward(token.address, account1); - const reward2 = await liquidityMining.getUserAccumulatedReward(token.address, account2); - - // for the first block, user 1 will receive the reward of 10 (reward given per block for 100% of shares) - // for the second block: - // - user 1 owns 1/2 of the shares => expected reward = 5 (total 10 + 5 = 15) - // - user 2 owns 1/2 of the shares => expected reward = 5 - expect(reward1).bignumber.equal("15"); - expect(reward2).bignumber.equal("5"); - }); - }); - - describe("getEstimatedReward", () => { - const amount1 = new BN(1000); - const amount2 = new BN(2000); - const amount3 = new BN(4000); - const allocationPoint1 = new BN(1); - const allocationPoint2 = new BN(2); - - const totalAllocationPoint = allocationPoint1.add(allocationPoint2); - let bonusBlockMultiplier; - let bonusEndBlock; - let secondsPerBlock; - - beforeEach(async () => { - await deploymentAndInit(); - await liquidityMining.add(token1.address, allocationPoint1, false); - - await token1.mint(account1, amount1); - await token1.mint(account2, amount2); - await token1.mint(account3, amount3); - - await token1.approve(liquidityMining.address, amount1, { from: account1 }); - await token1.approve(liquidityMining.address, amount2, { from: account2 }); - - bonusBlockMultiplier = await liquidityMining.BONUS_BLOCK_MULTIPLIER(); - bonusEndBlock = await liquidityMining.bonusEndBlock(); - - secondsPerBlock = await liquidityMining.SECONDS_PER_BLOCK(); - }); - - it("check calculation for 1 user, period less than 1 block", async () => { - let duration = secondsPerBlock.sub(new BN(1)); - - let estimatedReward = await liquidityMining.getEstimatedReward(token1.address, amount3, duration); - let expectedReward = "0"; - expect(estimatedReward).bignumber.equal(expectedReward); - }); - - it("check calculation for 1 user, period is 1 block", async () => { - let duration = secondsPerBlock; - - let estimatedReward = await liquidityMining.getEstimatedReward(token1.address, amount3, duration); - let expectedReward = rewardTokensPerBlock.mul(bonusBlockMultiplier); - expect(estimatedReward).bignumber.equal(expectedReward); - }); - - it("check calculation for 1 user, period is 40 blocks", async () => { - let blocks = new BN(40); - let duration = secondsPerBlock.mul(blocks); - - let estimatedReward = await liquidityMining.getEstimatedReward(token1.address, amount3, duration); - let expectedReward = rewardTokensPerBlock.mul(blocks).mul(bonusBlockMultiplier); - expect(estimatedReward).bignumber.equal(expectedReward); - }); - - it("check calculation for 2 users, period is 100 blocks", async () => { - // turn off bonus period - await advanceBlocks(bonusEndBlock); - - let blocks = new BN(100); - let duration = secondsPerBlock.mul(blocks); - - await token1.approve(liquidityMining.address, amount1, { from: account1 }); - await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { from: account1 }); - - let estimatedReward = await liquidityMining.getEstimatedReward(token1.address, amount3, duration); - let expectedReward = rewardTokensPerBlock.mul(blocks); - let totalAmount = amount1.add(amount3); - expectedReward = expectedReward.mul(amount3).div(totalAmount); - expect(estimatedReward).bignumber.equal(expectedReward); - }); - - it("check calculation for 3 users and 2 tokens, period is 1000 blocks", async () => { - await liquidityMining.add(token2.address, allocationPoint2, false); - // turn off bonus period - await advanceBlocks(bonusEndBlock); - - let blocks = new BN(1000); - let duration = secondsPerBlock.mul(blocks); - - await token1.approve(liquidityMining.address, amount1, { from: account1 }); - await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { from: account1 }); - await token1.approve(liquidityMining.address, amount2, { from: account2 }); - await liquidityMining.deposit(token1.address, amount2, ZERO_ADDRESS, { from: account2 }); - - let estimatedReward = await liquidityMining.getEstimatedReward(token1.address, amount3, duration); - let expectedReward = rewardTokensPerBlock.mul(blocks); - expectedReward = expectedReward.mul(allocationPoint1).div(totalAllocationPoint); - let totalAmount = amount1.add(amount2).add(amount3); - expectedReward = expectedReward.mul(amount3).div(totalAmount); - expect(estimatedReward).bignumber.equal(expectedReward); - }); - }); - - describe("deposit/withdraw", () => { - let allocationPoint = new BN(1); - let amount = new BN(1000); - - beforeEach(async () => { - await deploymentAndInit(); - for (let token of [token1, token2]) { - for (let account of [account1, account2]) { - await token.mint(account, amount); - await token.approve(liquidityMining.address, amount, { from: account }); - } - } - - // make sure the pool has tokens to distribute - await SOVToken.transfer(liquidityMining.address, new BN(1000)); - }); - - it("add, add, deposit, deposit", async () => { - await liquidityMining.add(token1.address, allocationPoint, false); // weight 1/1 - await liquidityMining.add(token2.address, allocationPoint, false); // weight 1/2 - - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - await liquidityMining.deposit(token2.address, amount, ZERO_ADDRESS, { from: account1 }); // 1 block passed - - // await liquidityMining.update(token1.address, allocationPoint.mul(new BN(2)), true); // weight 2/3 - await liquidityMining.updateAllPools(); // 2 blocks passed from first deposit - - const currentBlockNumber = await web3.eth.getBlockNumber(); - - // 3 tokens per share per block, times bonus multiplier (10), times precision (1e12), times weight (1/2), divided by total shares - const expectedAccumulatedRewardPerBlock = rewardTokensPerBlock.mul(new BN(10)).mul(new BN(1e12)).div(new BN(2)).div(amount); - - const poolInfo1 = await liquidityMining.getPoolInfo(token1.address); - expect(poolInfo1.poolToken).equal(token1.address); - expect(poolInfo1.allocationPoint).equal("1"); - expect(poolInfo1.lastRewardBlock).equal(currentBlockNumber.toString()); - // token1 deposit has been there for 2 blocks because of automining - expect(poolInfo1.accumulatedRewardPerShare).equal(expectedAccumulatedRewardPerBlock.mul(new BN(2)).toString()); - - const poolInfo2 = await liquidityMining.getPoolInfo(token2.address); - expect(poolInfo2.poolToken).equal(token2.address); - expect(poolInfo2.allocationPoint).equal("1"); - expect(poolInfo1.lastRewardBlock).equal(currentBlockNumber.toString()); - // token2 deposit has been there for only 1 block - expect(poolInfo2.accumulatedRewardPerShare).equal(expectedAccumulatedRewardPerBlock.toString()); - }); - - // tricky case 1 - it("add(pool1), add(pool2), deposit(user1, pool1), update(pool1), withdraw(user1, pool1)", async () => { - await liquidityMining.add(token1.address, allocationPoint, false); // weight 1/1 - await liquidityMining.add(token2.address, allocationPoint, false); // weight 1/2 - - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - await liquidityMining.update(token1.address, new BN("2"), false); // 1 block passed, new weight 2/3 - const tx = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); // 2 blocks passed - - await checkBonusPeriodHasNotEnded(); // sanity check, it's included in calculations - - const lockedAmount = await lockedSOV.getLockedBalance(account1); - const unlockedAmount = await lockedSOV.getUnlockedBalance(account1); - const rewardAmount = lockedAmount.add(unlockedAmount); - - // reward per block 30 (because of bonus period), 1 block with weight 1/2 = 15, 1 block with weight 2/3 = 20 - const expectedRewardAmount = new BN("35"); - expect(rewardAmount).bignumber.equal(expectedRewardAmount); - - await checkUserPoolTokens( - account1, - token1, - new BN(0), // user LM balance - new BN(0), // LM contract token balance - amount // user token balance - ); - - expectEvent(tx, "Withdraw", { - user: account1, - poolToken: token1.address, - amount: amount, - }); - - expectEvent(tx, "RewardClaimed", { - user: account1, - poolToken: token1.address, - amount: rewardAmount, - }); - }); - - // tricky case 2 - it("add(pool1), deposit(user1, pool1), deposit(user2, pool1), withdraw(user1, pool1), withdraw(user2, pool1)", async () => { - await liquidityMining.add(token1.address, allocationPoint, false); // weight 1/1 - - // deposit 1: 0 blocks, deposit 2: 0 blocks - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - // deposit 1: 1 blocks (100% shares), deposit 2: 0 blocks - await mineBlock(); - - // deposit 1: 2 blocks (100% shares), deposit 2: 0 blocks - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account2 }); - - // deposit 1: 3 blocks (50% shares), deposit 2: 1 blocks (50% shares) - const withdrawTx1 = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - // deposit 1: 3 blocks (withdrawn), deposit 2: 2 blocks (100% shares) - const withdrawTx2 = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account2 }); - - await checkBonusPeriodHasNotEnded(); // sanity check, it's included in calculations - - const lockedAmount1 = await lockedSOV.getLockedBalance(account1); - const unlockedAmount1 = await lockedSOV.getUnlockedBalance(account1); - const reward1 = lockedAmount1.add(unlockedAmount1); - - const lockedAmount2 = await lockedSOV.getLockedBalance(account2); - const unlockedAmount2 = await lockedSOV.getUnlockedBalance(account2); - const reward2 = lockedAmount2.add(unlockedAmount2); - - // reward per block 30 (because of bonus period), 2 block with 100% shares = 60, 1 block with 50% shares = 15 - const expectedReward1 = new BN("75"); - - // reward per block 30 (because of bonus period), 1 block with 50% shares = 15, 1 block with 100% shares = 30 - const expectedReward2 = new BN("45"); - - expect(reward1).bignumber.equal(expectedReward1); - expect(reward2).bignumber.equal(expectedReward2); - - await checkUserPoolTokens( - account1, - token1, - new BN(0), // user LM balance - new BN(0), // LM contract token balance - amount // user token balance - ); - await checkUserPoolTokens( - account2, - token1, - new BN(0), // user LM balance - new BN(0), // LM contract token balance - amount // user token balance - ); - - expectEvent(withdrawTx1, "Withdraw", { - user: account1, - poolToken: token1.address, - amount: amount, - }); - expectEvent(withdrawTx1, "RewardClaimed", { - user: account1, - poolToken: token1.address, - amount: reward1, - }); - expectEvent(withdrawTx2, "Withdraw", { - user: account2, - poolToken: token1.address, - amount: amount, - }); - expectEvent(withdrawTx2, "RewardClaimed", { - user: account2, - poolToken: token1.address, - amount: reward2, - }); - }); - - // tricky case 3a - it("add(pool1), deposit(user1, pool1), add(pool2, no update), withdraw(user1, pool1)", async () => { - await liquidityMining.add(token1.address, allocationPoint, false); // weight 1/1 - - // deposit: 0 blocks - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - // deposit: 1 blocks, note: pool1 is NOT updated - await liquidityMining.add(token2.address, new BN(2), false); // new weight: 1/3 - - // deposit: 2 blocks - await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - await checkBonusPeriodHasNotEnded(); // sanity check, it's included in calculations - - const lockedAmount = await lockedSOV.getLockedBalance(account1); - const unlockedAmount = await lockedSOV.getUnlockedBalance(account1); - const rewardAmount = lockedAmount.add(unlockedAmount); - - // reward per block 30 (because of bonus period), - // because add was called without updating the pool, the new weight is used for all blocks - // so 2 blocks with weight 1/3 = 20 - const expectedRewardAmount = new BN("20"); - expect(rewardAmount).bignumber.equal(expectedRewardAmount); - - await checkUserPoolTokens( - account1, - token1, - new BN(0), // user LM balance - new BN(0), // LM contract token balance - amount // user token balance - ); - }); - - // tricky case 3b - it("add(pool1), deposit(user1, pool1), add(pool2, update), withdraw(user1, pool1)", async () => { - await liquidityMining.add(token1.address, allocationPoint, false); // weight 1/1 - - // deposit: 0 blocks - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - // deposit: 1 blocks, note: pool1 IS updated - await liquidityMining.add(token2.address, new BN(2), true); // new weight: 1/3 - - // deposit: 2 blocks - await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - await checkBonusPeriodHasNotEnded(); // sanity check, it's included in calculations - - const lockedAmount = await lockedSOV.getLockedBalance(account1); - const unlockedAmount = await lockedSOV.getUnlockedBalance(account1); - const rewardAmount = lockedAmount.add(unlockedAmount); - - // reward per block 30 (because of bonus period), - // because add was called WITH updating the pools, old weight is for 1 block and new weight is for 1 block - // so 1 block with weight 1/1 = 30 and 1 block with weight 1/3 = 10 - const expectedRewardAmount = new BN("40"); - expect(rewardAmount).bignumber.equal(expectedRewardAmount); - - await checkUserPoolTokens( - account1, - token1, - new BN(0), // user LM balance - new BN(0), // LM contract token balance - amount // user token balance - ); - }); - - // tricky case 4 - it("add(pool1), deposit(user1, pool1), add(pool2), deposit(user2, pool2), withdraw(user1, pool1), withdraw(user2, pool2)", async () => { - await liquidityMining.add(token1.address, allocationPoint, false); // weight 1/1 - - // deposit 1: 0 blocks, deposit 2: 0 blocks - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - // deposit 1: 1 blocks (weight 1/1), deposit 2: 0 blocks. pool is updated - await liquidityMining.add(token2.address, allocationPoint, true); // weight 1/2 - - // deposit 1: 2 blocks (weight 1/2), deposit 2: 0 blocks - await liquidityMining.deposit(token2.address, amount, ZERO_ADDRESS, { from: account2 }); - - // deposit 1: 3 blocks (weight 1/2), deposit 2: 1 blocks (weight 1/2) - const withdrawTx1 = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - - // deposit 1: 3 blocks (withdrawn), deposit 2: 2 blocks (weight 1/2) - const withdrawTx2 = await liquidityMining.withdraw(token2.address, amount, ZERO_ADDRESS, { from: account2 }); - - await checkBonusPeriodHasNotEnded(); // sanity check, it's included in calculations - - const lockedAmount1 = await lockedSOV.getLockedBalance(account1); - const unlockedAmount1 = await lockedSOV.getUnlockedBalance(account1); - const reward1 = lockedAmount1.add(unlockedAmount1); - - const lockedAmount2 = await lockedSOV.getLockedBalance(account2); - const unlockedAmount2 = await lockedSOV.getUnlockedBalance(account2); - const reward2 = lockedAmount2.add(unlockedAmount2); - - // reward per block 30 (because of bonus period) - // deposit 1 has 1 block with weight 1/1 (30) and 2 blocks with weight 1/2 (15*2 = 30) - const expectedReward1 = new BN("60"); - - // deposit 2 has 2 blocks with weight 1/2 (15 * 2 = 30) - const expectedReward2 = new BN("30"); - - expect(reward1).bignumber.equal(expectedReward1); - expect(reward2).bignumber.equal(expectedReward2); - - for (let account of [account1, account2]) { - for (let token of [token1, token2]) { - await checkUserPoolTokens( - account, - token, - new BN(0), // user LM balance - new BN(0), // LM contract token balance - amount // user token balance - ); - } - } - - expectEvent(withdrawTx1, "Withdraw", { - user: account1, - poolToken: token1.address, - amount: amount, - }); - expectEvent(withdrawTx1, "RewardClaimed", { - user: account1, - poolToken: token1.address, - amount: reward1, - }); - expectEvent(withdrawTx2, "Withdraw", { - user: account2, - poolToken: token2.address, - amount: amount, - }); - expectEvent(withdrawTx2, "RewardClaimed", { - user: account2, - poolToken: token2.address, - amount: reward2, - }); - }); - }); - - describe("LM configuration", () => { - // Maximum reward per week: 100K SOV (or 100M SOV) - // Maximum reward per block: 4.9604 SOV (4.9604 * 2880 * 7 = 100001.664) - - const REWARD_TOKENS_PER_BLOCK = new BN(49604).mul(new BN(10 ** 14)).mul(new BN(1000)); - // const REWARD_TOKENS_PER_BLOCK = new BN(49604).mul(new BN(10**14)); - - // SOV/BTC pool 40K per week - // ETH/BTC pool 37.5K per week (from second week) - // Dummy pool 100K - SOV/BTC pool (- ETH/BTC pool) - - const MAX_ALLOCATION_POINT = new BN(100000).mul(new BN(1000)); - // const MAX_ALLOCATION_POINT = new BN(100000); - const ALLOCATION_POINT_SOV_BTC = new BN(40000); - const ALLOCATION_POINT_ETH_BTC = new BN(37500); - - const ALLOCATION_POINT_SOV_BTC_2 = new BN(30000); - - const amount = new BN(1000); - - beforeEach(async () => { - await deployLiquidityMining(); - await liquidityMining.initialize( - SOVToken.address, - REWARD_TOKENS_PER_BLOCK, - startDelayBlocks, - numberOfBonusBlocks, - wrapper.address, - lockedSOV.address, - 0 - ); - - for (let token of [token1, token2]) { - for (let account of [account1, account2]) { - await token.mint(account, amount); - await token.approve(liquidityMining.address, amount, { from: account }); - } - } - - // turn off bonus period - let bonusEndBlock = await liquidityMining.bonusEndBlock(); - await advanceBlocks(bonusEndBlock); - }); - - it("dummy pool + 1 pool", async () => { - let dummyPool = liquidityMiningConfigToken.address; - - let SOVBTCpool = token1.address; - - await liquidityMining.add(SOVBTCpool, ALLOCATION_POINT_SOV_BTC, false); // weight 40000 / 100000 - await liquidityMining.add(dummyPool, MAX_ALLOCATION_POINT.sub(ALLOCATION_POINT_SOV_BTC), false); // weight (100000 - 40000) / 100000 - - await liquidityMining.deposit(SOVBTCpool, amount, ZERO_ADDRESS, { from: account1 }); - - // reward won't be claimed because liquidityMining doesn't have enough SOV balance - // user reward will be updated - // 10 blocks passed since last deposit - await mineBlocks(9); - await liquidityMining.withdraw(SOVBTCpool, amount, ZERO_ADDRESS, { from: account1 }); - - const userInfo = await liquidityMining.getUserInfo(SOVBTCpool, account1); - // 10 blocks passed - let passedBlocks = 10; - let expectedUserReward = REWARD_TOKENS_PER_BLOCK.mul(new BN(passedBlocks)) - .mul(ALLOCATION_POINT_SOV_BTC) - .div(MAX_ALLOCATION_POINT); - expect(userInfo.accumulatedReward).bignumber.equal(expectedUserReward); - console.log(expectedUserReward.toString()); - }); - - it("dummy pool + 2 pools", async () => { - let dummyPool = liquidityMiningConfigToken.address; - - let SOVBTCpool = token1.address; - let ETHBTCpoll = token2.address; - - await liquidityMining.add(SOVBTCpool, ALLOCATION_POINT_SOV_BTC, false); // weight 40000 / 100000 - const DUMMY_ALLOCATION_POINT = MAX_ALLOCATION_POINT.sub(ALLOCATION_POINT_SOV_BTC); - await liquidityMining.add(dummyPool, DUMMY_ALLOCATION_POINT, false); // weight (100000 - 40000) / 100000 - - await liquidityMining.deposit(SOVBTCpool, amount, ZERO_ADDRESS, { from: account1 }); - - await mineBlocks(9); - await liquidityMining.updateAllPools(); // 10 blocks passed from first deposit - - // update config - // this method will also update pool reward using previous allocation point, - // so this block should be add to calculation with old values - await liquidityMining.update(SOVBTCpool, ALLOCATION_POINT_SOV_BTC_2, false); // weight 30000 / 100000 - - await liquidityMining.add(ETHBTCpoll, ALLOCATION_POINT_ETH_BTC, false); // weight 37500 / 100000 - const DUMMY_ALLOCATION_POINT_2 = MAX_ALLOCATION_POINT.sub(ALLOCATION_POINT_SOV_BTC_2).sub(ALLOCATION_POINT_ETH_BTC); - await liquidityMining.update(dummyPool, DUMMY_ALLOCATION_POINT_2, false); // weight (100000 - 30000 - 37500) / 100000 - await liquidityMining.updateAllPools(); - - // reward won't be claimed because liquidityMining doesn't have enough SOV balance - // user reward will be updated - // 10 blocks + 5 blocks passed - await liquidityMining.withdraw(SOVBTCpool, amount, ZERO_ADDRESS, { from: account1 }); - - const userInfo = await liquidityMining.getUserInfo(SOVBTCpool, account1); - // 10 blocks + 5 blocks passed - let passedBlocks = 10 + 1; // block should be add to calculation with old values - let expectedUserReward = REWARD_TOKENS_PER_BLOCK.mul(new BN(passedBlocks)) - .mul(ALLOCATION_POINT_SOV_BTC) - .div(MAX_ALLOCATION_POINT); - passedBlocks = 5 - 1; // block should be removed from calculation with new values - expectedUserReward = expectedUserReward.add( - REWARD_TOKENS_PER_BLOCK.mul(new BN(passedBlocks)).mul(ALLOCATION_POINT_SOV_BTC_2).div(MAX_ALLOCATION_POINT) - ); - expect(userInfo.accumulatedReward).bignumber.equal(expectedUserReward); - console.log(expectedUserReward.toString()); - }); - }); - - describe("onTokensDeposited", () => { - it("should revert if the sender is not a valid pool token", async () => { - await expectRevert(liquidityMining.onTokensDeposited(ZERO_ADDRESS, new BN(1000)), "Pool token not found"); - }); - }); - - describe("external getters", () => { - let allocationPoint = new BN(1); - let amount = new BN(1000); - - beforeEach(async () => { - await deploymentAndInit(); - await token1.mint(account1, amount); - await token1.approve(liquidityMining.address, amount, { from: account1 }); - await liquidityMining.add(token1.address, allocationPoint, false); - }); - - it("PRECISION", async () => { - expect(await liquidityMining.PRECISION()).bignumber.equal(new BN(1e12)); - }); - - it("BONUS_BLOCK_MULTIPLIER", async () => { - expect(await liquidityMining.BONUS_BLOCK_MULTIPLIER()).bignumber.equal("10"); - }); - - it("SVR", async () => { - expect(await liquidityMining.SOV()).equal(SOVToken.address); - }); - - it("rewardTokensPerBlock", async () => { - expect(await liquidityMining.rewardTokensPerBlock()).bignumber.equal(rewardTokensPerBlock); - }); - - it("startBlock", async () => { - expect(await liquidityMining.startBlock()).bignumber.gt("0"); - }); - - it("bonusEndBlock", async () => { - const startBlock = await liquidityMining.startBlock(); - expect(await liquidityMining.bonusEndBlock()).bignumber.equal(startBlock.add(numberOfBonusBlocks)); - }); - - it("endBlock", async () => { - expect(await liquidityMining.endBlock()).bignumber.equal("0"); - }); - - it("wrapper", async () => { - expect(await liquidityMining.wrapper()).equal(wrapper.address); - }); - - it("totalAllocationPoint", async () => { - expect(await liquidityMining.totalAllocationPoint()).bignumber.equal(allocationPoint); - await liquidityMining.add(token2.address, allocationPoint, false); - expect(await liquidityMining.totalAllocationPoint()).bignumber.equal(allocationPoint.mul(new BN(2))); - }); - - it("totalUsersBalance", async () => { - expect(await liquidityMining.totalUsersBalance()).bignumber.equal("0"); - - await liquidityMining.updateAllPools(); - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - expect(await liquidityMining.totalUsersBalance()).bignumber.equal("0"); - - await liquidityMining.updateAllPools(); - expect(await liquidityMining.totalUsersBalance()).bignumber.equal("30"); - }); - - // could still test these, but I don't see much point: - // PoolInfo[] public poolInfoList; - // mapping(address => uint256) poolIdList; - // mapping(uint256 => mapping(address => UserInfo)) public userInfoMap; - - it("getMissedBalance", async () => { - let missedBalance = await liquidityMining.getMissedBalance(); - expect(missedBalance).bignumber.equal("0"); - - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - await liquidityMining.updatePool(token1.address); - - missedBalance = await liquidityMining.getMissedBalance(); - expect(missedBalance).bignumber.equal("30"); - }); - - it("getUserAccumulatedReward", async () => { - // real tests are elsewhere in this file - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - await mineBlock(); - const reward1 = await liquidityMining.getUserAccumulatedReward(token1.address, account1); - const reward2 = await liquidityMining.getUserAccumulatedReward(token1.address, account2); - expect(reward1).bignumber.equal("30"); - expect(reward2).bignumber.equal("0"); - }); - - it("getPoolId", async () => { - const poolId = await liquidityMining.getPoolId(token1.address); - expect(poolId).bignumber.equal("0"); - await expectRevert(liquidityMining.getPoolId(token2.address), "Pool token not found"); - await liquidityMining.add(token2.address, allocationPoint, false); - const poolId2 = await liquidityMining.getPoolId(token2.address); - expect(poolId2).bignumber.equal("1"); - }); - - it("getPoolLength", async () => { - let length = await liquidityMining.getPoolLength(); - expect(length).bignumber.equal("1"); - - await liquidityMining.add(token2.address, allocationPoint, false); - length = await liquidityMining.getPoolLength(); - expect(length).bignumber.equal("2"); - }); - - it("getPoolInfoList", async () => { - const infoList = await liquidityMining.getPoolInfoList(); - expect(infoList).to.be.an("array"); - expect(infoList.length).equal(1); - const info = infoList[0]; - expect(info.poolToken).equal(token1.address); - expect(info.allocationPoint).equal(allocationPoint.toString()); - expect(info.accumulatedRewardPerShare).equal("0"); - expect(info.lastRewardBlock).equal((await web3.eth.getBlockNumber()).toString()); - }); - - it("getPoolInfo", async () => { - const info = await liquidityMining.getPoolInfo(token1.address); - expect(info.poolToken).equal(token1.address); - expect(info.allocationPoint).equal(allocationPoint.toString()); - expect(info.accumulatedRewardPerShare).equal("0"); - expect(info.lastRewardBlock).equal((await web3.eth.getBlockNumber()).toString()); - - await expectRevert(liquidityMining.getPoolInfo(token2.address), "Pool token not found"); - }); - - it("getUserBalanceList", async () => { - await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { from: account1 }); - await mineBlock(); - const balanceList = await liquidityMining.getUserBalanceList(account1); - - expect(balanceList).to.be.an("array"); - expect(balanceList.length).equal(1); - const balanceData = balanceList[0]; - expect(balanceData).to.be.an("array"); - expect(balanceData[0]).bignumber.equal(amount); - expect(balanceData[1]).bignumber.equal("30"); - }); - - it("getUserInfo", async () => { - await liquidityMining.deposit(token1.address, new BN(500), ZERO_ADDRESS, { from: account1 }); - - let userInfo = await liquidityMining.getUserInfo(token1.address, account1); - expect(userInfo.amount).bignumber.equal("500"); - expect(userInfo.accumulatedReward).bignumber.equal("0"); // XXX: not yet updated -- funny? - expect(userInfo.rewardDebt).bignumber.equal("0"); // not yet updated either - - // deposit updates it. - await liquidityMining.deposit(token1.address, new BN(1), ZERO_ADDRESS, { from: account1 }); - userInfo = await liquidityMining.getUserInfo(token1.address, account1); - expect(userInfo.amount).bignumber.equal("501"); - expect(userInfo.accumulatedReward).bignumber.equal("30"); - expect(userInfo.rewardDebt).bignumber.equal("30"); - }); - - it("getUserInfoList", async () => { - await liquidityMining.deposit(token1.address, new BN(500), ZERO_ADDRESS, { from: account1 }); - - let userInfoList = await liquidityMining.getUserInfoList(account1); - expect(userInfoList).to.be.an("array"); - expect(userInfoList.length).equal(1); - const userInfo = userInfoList[0]; - expect(userInfo.amount).bignumber.equal("500"); - expect(userInfo.accumulatedReward).bignumber.equal("0"); - expect(userInfo.rewardDebt).bignumber.equal("0"); - }); - - it("getUserAccumulatedRewardList", async () => { - await liquidityMining.deposit(token1.address, new BN(500), ZERO_ADDRESS, { from: account1 }); - - let rewardList = await liquidityMining.getUserAccumulatedRewardList(account1); - expect(rewardList).to.be.an("array"); - expect(rewardList.length).equal(1); - expect(rewardList[0]).bignumber.equal("0"); - }); - - it("getUserPoolTokenBalance", async () => { - await liquidityMining.deposit(token1.address, new BN(500), ZERO_ADDRESS, { from: account1 }); - let poolTokenBalance = await liquidityMining.getUserPoolTokenBalance(token1.address, account1); - expect(poolTokenBalance).bignumber.equal(new BN(500)); - }); - }); - - async function deployLiquidityMining() { - let liquidityMiningLogic = await LiquidityMiningLogic.new(); - let liquidityMiningProxy = await LiquidityMiningProxy.new(); - await liquidityMiningProxy.setImplementation(liquidityMiningLogic.address); - liquidityMining = await LiquidityMiningLogic.at(liquidityMiningProxy.address); - - wrapper = await Wrapper.new(liquidityMining.address); - } - - async function mineBlocks(blocks) { - for (let i = 0; i < blocks; i++) { - await mineBlock(); - } - } - - function checkPoolInfo(poolInfo, token, allocationPoint, lastRewardBlock, accumulatedRewardPerShare) { - expect(poolInfo.poolToken).equal(token); - expect(poolInfo.allocationPoint).bignumber.equal(allocationPoint); - expect(poolInfo.lastRewardBlock).bignumber.equal(lastRewardBlock); - if (accumulatedRewardPerShare.toNumber() !== -1) { - expect(poolInfo.accumulatedRewardPerShare).bignumber.equal(accumulatedRewardPerShare); - } - } - - async function checkUserPoolTokens(user, poolToken, _userAmount, _liquidityMiningBalance, _userBalance, wrapper) { - // user balance in pool - let userInfo = await liquidityMining.getUserInfo(poolToken.address, user); - expect(userInfo.amount).bignumber.equal(_userAmount); - // LM balance of pool tokens - let liquidityMiningBalance = await poolToken.balanceOf(liquidityMining.address); - expect(liquidityMiningBalance).bignumber.equal(_liquidityMiningBalance); - // user's balance of pool tokens - let userBalance = await poolToken.balanceOf(user); - if (wrapper !== undefined) { - userBalance = await poolToken.balanceOf(wrapper); - } - expect(userBalance).bignumber.equal(_userBalance); - } - - // user's balance of reward token - async function checkUserReward(user, poolToken, depositBlockNumber, latestBlockNumber) { - let passedBlocks = await liquidityMining.getPassedBlocksWithBonusMultiplier(depositBlockNumber, latestBlockNumber); - let userReward = passedBlocks.mul(rewardTokensPerBlock); - let userInfo = await liquidityMining.getUserInfo(poolToken.address, user); - expect(userInfo.accumulatedReward).bignumber.equal(new BN(0)); - return userReward; - } - - async function checkBonusPeriodHasNotEnded() { - expect(await liquidityMining.bonusEndBlock()).bignumber.gt((await web3.eth.getBlockNumber()).toString()); - } + const name = "Test SOV Token"; + const symbol = "TST"; + + const PRECISION = 1e12; + + const rewardTokensPerBlock = new BN(3); + const startDelayBlocks = new BN(1); + const numberOfBonusBlocks = new BN(50); + + // The % which determines how much will be unlocked immediately. + /// @dev 10000 is 100% + const unlockedImmediatelyPercent = new BN(1000); // 10% + + let root, account1, account2, account3, account4; + let SOVToken, token1, token2, token3, liquidityMiningConfigToken; + let liquidityMining, wrapper; + let lockedSOVAdmins, lockedSOV; + + async function deploymentAndInit() { + SOVToken = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); + token1 = await TestToken.new("Test token 1", "TST-1", 18, TOTAL_SUPPLY); + token2 = await TestToken.new("Test token 2", "TST-2", 18, TOTAL_SUPPLY); + token3 = await TestToken.new("Test token 3", "TST-3", 18, TOTAL_SUPPLY); + liquidityMiningConfigToken = await LiquidityMiningConfigToken.new(); + lockedSOVAdmins = [account1, account2]; + + lockedSOV = await TestLockedSOV.new(SOVToken.address, lockedSOVAdmins); + + await deployLiquidityMining(); + await liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ); + } + + async function deploymentAndInitFixture(_wallets, _provider) { + await deploymentAndInit(); + } + + before(async () => { + accounts = await web3.eth.getAccounts(); + [root, account1, account2, account3, account4, ...accounts] = accounts; + }); + + /// @dev Test dummy liquidityMiningConfigToken methods for coverage + describe("liquidityMiningConfigToken", () => { + it("Test liquidityMiningConfigToken methods", async () => { + await loadFixture(deploymentAndInitFixture); + let totalSupply = await liquidityMiningConfigToken.totalSupply(); + // console.log("totalSupply = ", totalSupply.toString()); + expect(totalSupply).to.be.bignumber.equal(new BN(0)); + + let transferReturn = await liquidityMiningConfigToken.transfer.call(account1, 0); + // console.log("transferReturn = ", transferReturn); + expect(transferReturn).equal(false); + + let allowance = await liquidityMiningConfigToken.allowance(account1, account2); + // console.log("allowance = ", allowance.toString()); + expect(allowance).to.be.bignumber.equal(new BN(0)); + + let approveReturn = await liquidityMiningConfigToken.approve.call(account1, 0); + // console.log("approveReturn = ", approveReturn); + expect(approveReturn).equal(false); + + let transferFromReturn = await liquidityMiningConfigToken.transferFrom.call( + account1, + account2, + 0 + ); + // console.log("transferFromReturn = ", transferFromReturn); + expect(transferFromReturn).equal(false); + }); + }); + + describe("initialize", () => { + it("sets the expected values", async () => { + await loadFixture(deploymentAndInitFixture); + await deployLiquidityMining(); + let tx = await liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ); + + let _SOV = await liquidityMining.SOV(); + let _rewardTokensPerBlock = await liquidityMining.rewardTokensPerBlock(); + let _startBlock = await liquidityMining.startBlock(); + let _bonusEndBlock = await liquidityMining.bonusEndBlock(); + let _wrapper = await liquidityMining.wrapper(); + + let blockNumber = new BN(tx.receipt.blockNumber); + + expect(_SOV).equal(SOVToken.address); + expect(_rewardTokensPerBlock).bignumber.equal(rewardTokensPerBlock); + expect(_startBlock).bignumber.equal(startDelayBlocks.add(blockNumber)); + expect(_bonusEndBlock).bignumber.equal( + startDelayBlocks.add(blockNumber).add(numberOfBonusBlocks) + ); + expect(_wrapper).equal(wrapper.address); + }); + + it("fails if not an owner or an admin", async () => { + await deployLiquidityMining(); + await expectRevert( + liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent, + { from: account1 } + ), + "unauthorized" + ); + + await liquidityMining.addAdmin(account1); + await liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent, + { from: account1 } + ); + }); + + it("fails if _startBlock = 0", async () => { + await deployLiquidityMining(); + await expectRevert( + liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + 0, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ), + "Invalid start block" + ); + }); + + it("fails if already initialized", async () => { + await deploymentAndInit(); + await expectRevert( + liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ), + "Already initialized" + ); + }); + + it("fails if the 0 address is passed as token address", async () => { + await deployLiquidityMining(); + await expectRevert( + liquidityMining.initialize( + ZERO_ADDRESS, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + unlockedImmediatelyPercent + ), + "Invalid token address" + ); + }); + + it("fails if unlockedImmediatelyPercent >= 10000", async () => { + await deployLiquidityMining(); + await expectRevert( + liquidityMining.initialize( + SOVToken.address, + rewardTokensPerBlock, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + 12345 + ), + "Unlocked immediately percent has to be less than 10000." + ); + }); + }); + + describe("addAdmin", () => { + it("adds admin", async () => { + let tx = await liquidityMining.addAdmin(account1); + + expectEvent(tx, "AdminAdded", { + admin: account1, + }); + + let isAdmin = await liquidityMining.admins(account1); + expect(isAdmin).equal(true); + }); + + it("fails sender isn't an owner", async () => { + await expectRevert( + liquidityMining.addAdmin(account1, { from: account1 }), + "unauthorized" + ); + }); + }); + + describe("removeAdmin", () => { + it("adds admin", async () => { + await liquidityMining.addAdmin(account1); + let tx = await liquidityMining.removeAdmin(account1); + + expectEvent(tx, "AdminRemoved", { + admin: account1, + }); + + let isAdmin = await liquidityMining.admins(account1); + expect(isAdmin).equal(false); + }); + + it("fails sender isn't an owner", async () => { + await expectRevert( + liquidityMining.removeAdmin(account1, { from: account1 }), + "unauthorized" + ); + }); + }); + + describe("setLockedSOV", () => { + it("sets the expected values", async () => { + let newLockedSOV = account2; + await liquidityMining.setLockedSOV(newLockedSOV); + + let _lockedSOV = await liquidityMining.lockedSOV(); + expect(_lockedSOV).equal(newLockedSOV); + }); + + it("fails if not an owner and an admin", async () => { + await expectRevert( + liquidityMining.setLockedSOV(account2, { from: account1 }), + "unauthorized" + ); + + await liquidityMining.addAdmin(account1); + await liquidityMining.setLockedSOV(account2, { from: account1 }); + }); + + it("fails if zero address passed", async () => { + await expectRevert( + liquidityMining.setLockedSOV(ZERO_ADDRESS), + "Invalid lockedSOV Address." + ); + }); + }); + + describe("setUnlockedImmediatelyPercent", () => { + it("sets the expected values", async () => { + let newUnlockedImmediatelyPercent = new BN(2000); + await liquidityMining.setUnlockedImmediatelyPercent(newUnlockedImmediatelyPercent); + + let _unlockedImmediatelyPercent = await liquidityMining.unlockedImmediatelyPercent(); + expect(_unlockedImmediatelyPercent).bignumber.equal(newUnlockedImmediatelyPercent); + }); + + it("fails if not an owner or an admin", async () => { + await deploymentAndInit(); + await expectRevert( + liquidityMining.setUnlockedImmediatelyPercent(1000, { from: account1 }), + "unauthorized" + ); + + await liquidityMining.addAdmin(account1); + await liquidityMining.setUnlockedImmediatelyPercent(1000, { from: account1 }); + }); + + it("fails if unlockedImmediatelyPercent >= 10000", async () => { + await expectRevert( + liquidityMining.setUnlockedImmediatelyPercent(100000), + "Unlocked immediately percent has to be less than 10000." + ); + }); + }); + + describe("setWrapper", () => { + it("sets the expected values", async () => { + let newWrapper = account2; + await liquidityMining.setWrapper(newWrapper); + + let _wrapper = await liquidityMining.wrapper(); + expect(_wrapper).equal(newWrapper); + }); + + it("fails if not an owner or an admin", async () => { + await deploymentAndInit(); + await expectRevert( + liquidityMining.setWrapper(account2, { from: account1 }), + "unauthorized" + ); + + await liquidityMining.addAdmin(account1); + await liquidityMining.setWrapper(account2, { from: account1 }); + }); + }); + + describe("stopMining", () => { + it("should set end block", async () => { + let tx = await liquidityMining.stopMining(); + + let blockNumber = new BN(tx.receipt.blockNumber); + let _endBlock = await liquidityMining.endBlock(); + expect(_endBlock).bignumber.equal(blockNumber); + }); + + it("fails if not an owner or an admin", async () => { + await deploymentAndInit(); + await expectRevert(liquidityMining.stopMining({ from: account1 }), "unauthorized"); + + await liquidityMining.addAdmin(account1); + await liquidityMining.stopMining({ from: account1 }); + }); + + it("fails if already stopped", async () => { + await deploymentAndInit(); + await liquidityMining.stopMining(); + await expectRevert(liquidityMining.stopMining(), "Already stopped"); + }); + }); + + describe("transferSOV", () => { + it("should be able to transfer SOV", async () => { + let amount = new BN(1000); + await SOVToken.transfer(liquidityMining.address, amount); + + let balanceBefore = await SOVToken.balanceOf(account1); + await liquidityMining.transferSOV(account1, amount); + let balanceAfter = await SOVToken.balanceOf(account1); + + expect(amount).bignumber.equal(balanceAfter.sub(balanceBefore)); + }); + + it("only owner or admin should be able to transfer", async () => { + await expectRevert( + liquidityMining.transferSOV(account1, 1000, { from: account1 }), + "unauthorized" + ); + + await liquidityMining.addAdmin(account1); + await liquidityMining.transferSOV(account1, 1000, { from: account1 }); + }); + + it("fails if the 0 address is passed as receiver address", async () => { + await expectRevert( + liquidityMining.transferSOV(ZERO_ADDRESS, 1000), + "Receiver address invalid" + ); + }); + + it("fails if the 0 is passed as an amount", async () => { + await expectRevert(liquidityMining.transferSOV(account1, 0), "Amount invalid"); + }); + }); + + describe("add", () => { + it("should be able to add pool token", async () => { + let allocationPoint = new BN(1); + let tx = await liquidityMining.add(token1.address, allocationPoint, false); + + expect(await liquidityMining.totalAllocationPoint()).bignumber.equal(allocationPoint); + + let poolInfo = await liquidityMining.poolInfoList(0); + expect(poolInfo.poolToken).equal(token1.address); + expect(poolInfo.allocationPoint).bignumber.equal(allocationPoint); + let blockNumber = new BN(tx.receipt.blockNumber); + expect(poolInfo.lastRewardBlock).bignumber.equal(blockNumber); + expect(poolInfo.accumulatedRewardPerShare).bignumber.equal(new BN(0)); + + expect(await liquidityMining.getPoolLength()).bignumber.equal(new BN(1)); + + expectEvent(tx, "PoolTokenAdded", { + user: root, + poolToken: token1.address, + allocationPoint: allocationPoint, + }); + }); + + it("should be able to add 2 pool tokens and update pools", async () => { + await deploymentAndInit(); + let allocationPoint1 = new BN(1); + let tx1 = await liquidityMining.add(token1.address, allocationPoint1, false); + + expect(await liquidityMining.totalAllocationPoint()).bignumber.equal(allocationPoint1); + + expectEvent(tx1, "PoolTokenAdded", { + user: root, + poolToken: token1.address, + allocationPoint: allocationPoint1, + }); + + let allocationPoint2 = new BN(2); + let tx2 = await liquidityMining.add(token2.address, allocationPoint2, true); + + expect(await liquidityMining.totalAllocationPoint()).bignumber.equal( + allocationPoint1.add(allocationPoint2) + ); + + expectEvent(tx2, "PoolTokenAdded", { + user: root, + poolToken: token2.address, + allocationPoint: allocationPoint2, + }); + + let poolInfo1 = await liquidityMining.getPoolInfo(token1.address); + let poolInfo2 = await liquidityMining.getPoolInfo(token2.address); + expect(poolInfo1.lastRewardBlock).bignumber.equal(poolInfo2.lastRewardBlock); + }); + + it("fails if the 0 allocation point is passed", async () => { + await expectRevert( + liquidityMining.add(token1.address, new BN(0), false), + "Invalid allocation point" + ); + }); + + it("fails if the 0 address is passed as token address", async () => { + await expectRevert( + liquidityMining.add(ZERO_ADDRESS, new BN(1), false), + "Invalid token address" + ); + }); + + it("fails if token already added", async () => { + await deploymentAndInit(); + await liquidityMining.add(token1.address, new BN(1), false); + await expectRevert( + liquidityMining.add(token1.address, new BN(1), false), + "Token already added" + ); + }); + + it("only owner or admin should be able to add pool token", async () => { + await deploymentAndInit(); + await expectRevert( + liquidityMining.add(token2.address, new BN(1), false, { from: account1 }), + "unauthorized" + ); + + await liquidityMining.addAdmin(account1); + await liquidityMining.add(token2.address, new BN(1), false, { from: account1 }); + }); + }); + + describe("update", () => { + it("should be able to update pool token", async () => { + await deploymentAndInit(); + let oldAllocationPoint = new BN(1); + await liquidityMining.add(token1.address, oldAllocationPoint, false); + + let newAllocationPoint = new BN(2); + let tx = await liquidityMining.update(token1.address, newAllocationPoint, false); + + expect(await liquidityMining.totalAllocationPoint()).bignumber.equal( + newAllocationPoint + ); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let blockNumber = new BN(tx.receipt.blockNumber); + checkPoolInfo(poolInfo, token1.address, newAllocationPoint, blockNumber, new BN(0)); + + expect(await liquidityMining.getPoolLength()).bignumber.equal(new BN(1)); + + expectEvent(tx, "PoolTokenUpdated", { + user: root, + poolToken: token1.address, + newAllocationPoint: newAllocationPoint, + oldAllocationPoint: oldAllocationPoint, + }); + }); + + it("should be able to update pool token and update pools", async () => { + await deploymentAndInit(); + let oldAllocationPoint = new BN(1); + await liquidityMining.add(token1.address, oldAllocationPoint, false); + + await liquidityMining.add(token2.address, oldAllocationPoint, false); + + let newAllocationPoint = new BN(2); + let tx = await liquidityMining.update(token1.address, newAllocationPoint, true); + + expect(await liquidityMining.totalAllocationPoint()).bignumber.equal( + oldAllocationPoint.add(newAllocationPoint) + ); + + let poolInfo = await liquidityMining.getPoolInfo(token2.address); + expect(poolInfo.lastRewardBlock).bignumber.equal(new BN(tx.receipt.blockNumber)); + }); + + it("fails if token wasn't added", async () => { + await deploymentAndInit(); + await expectRevert( + liquidityMining.update(token1.address, new BN(1), false), + "Pool token not found" + ); + }); + + it("only owner or admin should be able to update pool token", async () => { + await liquidityMining.add(token2.address, new BN(1), false); + await expectRevert( + liquidityMining.update(token2.address, new BN(1), false, { from: account1 }), + "unauthorized" + ); + + await liquidityMining.addAdmin(account1); + await liquidityMining.update(token2.address, new BN(1), false, { from: account1 }); + }); + }); + + describe("updateTokens", () => { + it("should be able to update 2 pool tokens", async () => { + await deploymentAndInit(); + let poolTokens = [token1.address, token2.address, token3.address]; + let oldAllocationPoints = [new BN(1), new BN(2), new BN(3)]; + + for (let i = 0; i < poolTokens.length; i++) { + await liquidityMining.add(poolTokens[i], oldAllocationPoints[i], false); + } + + let newAllocationPoints = [new BN(101), new BN(102), new BN(3)]; + let tx = await liquidityMining.updateTokens(poolTokens, newAllocationPoints, true); + + let totalAllocationPoint = new BN(0); + for (let i = 0; i < newAllocationPoints.length; i++) { + totalAllocationPoint = totalAllocationPoint.add(newAllocationPoints[i]); + } + expect(await liquidityMining.totalAllocationPoint()).bignumber.equal( + totalAllocationPoint + ); + + let blockNumber = new BN(tx.receipt.blockNumber); + for (let i = 0; i < poolTokens.length - 1; i++) { + let poolInfo = await liquidityMining.getPoolInfo(poolTokens[i]); + checkPoolInfo( + poolInfo, + poolTokens[i], + newAllocationPoints[i], + blockNumber, + new BN(0) + ); + + expectEvent(tx, "PoolTokenUpdated", { + user: root, + poolToken: poolTokens[i], + newAllocationPoint: newAllocationPoints[i], + oldAllocationPoint: oldAllocationPoints[i], + }); + } + + expect(await liquidityMining.getPoolLength()).bignumber.equal(new BN(3)); + + let poolInfo = await liquidityMining.getPoolInfo(poolTokens[poolTokens.length - 1]); + expect(poolInfo.lastRewardBlock).bignumber.equal(blockNumber); + }); + + it("fails if token wasn't added", async () => { + await deploymentAndInit(); + await expectRevert( + liquidityMining.updateTokens([token1.address], [new BN(1)], false), + "Pool token not found" + ); + }); + + it("fails if arrays have different length", async () => { + await liquidityMining.add(token2.address, new BN(1), false); + await expectRevert( + liquidityMining.updateTokens([token1.address, token2.address], [new BN(1)], false), + "Arrays mismatch" + ); + }); + + it("only owner or admin should be able to update pool token", async () => { + await deploymentAndInit(); + await liquidityMining.add(token2.address, new BN(1), false); + await expectRevert( + liquidityMining.updateTokens([token2.address], [new BN(1)], false, { + from: account1, + }), + "unauthorized" + ); + + await liquidityMining.addAdmin(account1); + await liquidityMining.updateTokens([token2.address], [new BN(1)], false, { + from: account1, + }); + }); + }); + + describe("deposit", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + await deploymentAndInit(); + await liquidityMining.add(token1.address, allocationPoint, false); + await mineBlocks(1); + + await token1.mint(account1, amount); + await token1.approve(liquidityMining.address, amount, { from: account1 }); + }); + + it("should be able to deposit", async () => { + let tx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let blockNumber = new BN(tx.receipt.blockNumber); + checkPoolInfo(poolInfo, token1.address, allocationPoint, blockNumber, new BN(0)); + + await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); + + expectEvent(tx, "Deposit", { + user: account1, + poolToken: token1.address, + amount: amount, + }); + }); + + it("should be able to deposit using wrapper", async () => { + let tx = await liquidityMining.deposit(token1.address, amount, account2, { + from: account1, + }); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let blockNumber = new BN(tx.receipt.blockNumber); + checkPoolInfo(poolInfo, token1.address, allocationPoint, blockNumber, new BN(0)); + + await checkUserPoolTokens(account2, token1, amount, amount, new BN(0)); + + expectEvent(tx, "Deposit", { + user: account2, + poolToken: token1.address, + amount: amount, + }); + }); + + it("fails if token pool token not found", async () => { + await expectRevert( + liquidityMining.deposit(account1, amount, ZERO_ADDRESS, { from: account1 }), + "Pool token not found" + ); + }); + }); + + describe("claimReward", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + await deploymentAndInit(); + await liquidityMining.add(token1.address, allocationPoint, false); + await mineBlocks(1); + + await token1.mint(account1, amount); + await token1.approve(liquidityMining.address, amount, { from: account1 }); + }); + + it("shouldn't be able to claim reward (will not be claimed without SOV tokens)", async () => { + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + await expectRevert( + liquidityMining.claimReward(token1.address, ZERO_ADDRESS, { from: account1 }), + "Claiming reward failed" + ); + }); + + it("should be able to claim reward (will be claimed with SOV tokens)", async () => { + let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + let depositBlockNumber = new BN(depositTx.receipt.blockNumber); + await SOVToken.transfer(liquidityMining.address, new BN(1000)); + + let tx = await liquidityMining.claimReward(token1.address, ZERO_ADDRESS, { + from: account1, + }); + + let totalUsersBalance = await liquidityMining.totalUsersBalance(); + expect(totalUsersBalance).bignumber.equal(new BN(0)); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let latestBlockNumber = new BN(tx.receipt.blockNumber); + checkPoolInfo( + poolInfo, + token1.address, + allocationPoint, + latestBlockNumber, + new BN(-1) + ); + + await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); + let userReward = await checkUserReward( + account1, + token1, + depositBlockNumber, + latestBlockNumber + ); + + // withdrawAndStakeTokensFrom was invoked + let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); + let lockedBalance = await lockedSOV.getLockedBalance(account1); + expect(unlockedBalance).bignumber.equal(new BN(0)); + expect(lockedBalance).bignumber.equal(new BN(0)); + + expectEvent(tx, "RewardClaimed", { + user: account1, + poolToken: token1.address, + amount: userReward, + }); + }); + + it("should be able to claim reward using wrapper", async () => { + let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + let depositBlockNumber = new BN(depositTx.receipt.blockNumber); + await SOVToken.transfer(liquidityMining.address, new BN(1000)); + + let tx = await wrapper.claimReward(token1.address, { from: account1 }); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let latestBlockNumber = new BN(tx.receipt.blockNumber); + checkPoolInfo( + poolInfo, + token1.address, + allocationPoint, + latestBlockNumber, + new BN(-1) + ); + + await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); + await checkUserReward(account1, token1, depositBlockNumber, latestBlockNumber); + + // withdrawAndStakeTokensFrom was invoked + let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); + let lockedBalance = await lockedSOV.getLockedBalance(account1); + expect(unlockedBalance).bignumber.equal(new BN(0)); + expect(lockedBalance).bignumber.equal(new BN(0)); + }); + + it("fails if token pool token not found", async () => { + await expectRevert( + liquidityMining.claimReward(account1, ZERO_ADDRESS, { from: account1 }), + "Pool token not found" + ); + }); + }); + + describe("claimRewardFromAllPools", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + await deploymentAndInit(); + await liquidityMining.add(token1.address, allocationPoint, false); + await liquidityMining.add(token2.address, allocationPoint, false); + await mineBlocks(1); + + await token1.mint(account1, amount); + await token1.approve(liquidityMining.address, amount, { from: account1 }); + await token2.mint(account1, amount); + await token2.approve(liquidityMining.address, amount, { from: account1 }); + }); + + it("shouldn't be able to claim reward (will not be claimed without SOV tokens)", async () => { + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + await expectRevert( + liquidityMining.claimRewardFromAllPools(ZERO_ADDRESS, { from: account1 }), + "Claiming reward failed" + ); + }); + + it("should be able to claim reward (will be claimed with SOV tokens)", async () => { + let depositTx1 = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + let depositBlockNumber1 = new BN(depositTx1.receipt.blockNumber); + let depositTx2 = await liquidityMining.deposit(token2.address, amount, ZERO_ADDRESS, { + from: account1, + }); + let depositBlockNumber2 = new BN(depositTx2.receipt.blockNumber); + await SOVToken.transfer(liquidityMining.address, amount.mul(new BN(2))); + + let tx = await liquidityMining.claimRewardFromAllPools(ZERO_ADDRESS, { + from: account1, + }); + + let totalUsersBalance = await liquidityMining.totalUsersBalance(); + expect(totalUsersBalance).bignumber.equal(new BN(0)); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let latestBlockNumber = new BN(tx.receipt.blockNumber); + checkPoolInfo( + poolInfo, + token1.address, + allocationPoint, + latestBlockNumber, + new BN(-1) + ); + + await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); + let userReward1 = await checkUserReward( + account1, + token1, + depositBlockNumber1, + latestBlockNumber + ); + // we have 2 pools with the same allocation points + userReward1 = userReward1.div(new BN(2)); + + await checkUserPoolTokens(account1, token2, amount, amount, new BN(0)); + let userReward2 = await checkUserReward( + account1, + token2, + depositBlockNumber2, + latestBlockNumber + ); + // we have 2 pools with the same allocation points + userReward2 = userReward2.div(new BN(2)); + + // withdrawAndStakeTokensFrom was invoked + let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); + let lockedBalance = await lockedSOV.getLockedBalance(account1); + expect(unlockedBalance).bignumber.equal(new BN(0)); + expect(lockedBalance).bignumber.equal(new BN(0)); + + expectEvent(tx, "RewardClaimed", { + user: account1, + poolToken: token1.address, + amount: userReward1, + }); + + expect(userReward1, tx.logs[0].args.amount); + expect(token1.address, tx.logs[0].args.poolToken); + expect(userReward2, tx.logs[1].args.amount); + expect(token2.address, tx.logs[1].args.poolToken); + }); + + it("should be able to claim reward using wrapper", async () => { + let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + let depositBlockNumber = new BN(depositTx.receipt.blockNumber); + await SOVToken.transfer(liquidityMining.address, new BN(1000)); + + let tx = await wrapper.claimRewardFromAllPools({ from: account1 }); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let latestBlockNumber = new BN(tx.receipt.blockNumber); + checkPoolInfo( + poolInfo, + token1.address, + allocationPoint, + latestBlockNumber, + new BN(-1) + ); + + await checkUserPoolTokens(account1, token1, amount, amount, new BN(0)); + await checkUserReward(account1, token1, depositBlockNumber, latestBlockNumber); + + // withdrawAndStakeTokensFrom was invoked + let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); + let lockedBalance = await lockedSOV.getLockedBalance(account1); + expect(unlockedBalance).bignumber.equal(new BN(0)); + expect(lockedBalance).bignumber.equal(new BN(0)); + }); + }); + + describe("withdraw", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + await deploymentAndInit(); + await liquidityMining.add(token1.address, allocationPoint, false); + await mineBlocks(1); + + await token1.mint(account1, amount); + await token1.approve(liquidityMining.address, amount, { from: account1 }); + }); + + it("should be able to withdraw (without claiming reward)", async () => { + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + let tx = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let blockNumber = new BN(tx.receipt.blockNumber); + checkPoolInfo(poolInfo, token1.address, allocationPoint, blockNumber, new BN(-1)); + + await checkUserPoolTokens(account1, token1, new BN(0), new BN(0), amount); + + // User's balance on lockedSOV vault + let userRewardBalance = await lockedSOV.getLockedBalance(account1); + expect(userRewardBalance).bignumber.equal(new BN(0)); + + expectEvent(tx, "Withdraw", { + user: account1, + poolToken: token1.address, + amount: amount, + }); + }); + + it("should be able to withdraw (with claiming reward)", async () => { + let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + let depositBlockNumber = new BN(depositTx.receipt.blockNumber); + await SOVToken.transfer(liquidityMining.address, new BN(1000)); + + let tx = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + let totalUsersBalance = await liquidityMining.totalUsersBalance(); + expect(totalUsersBalance).bignumber.equal(new BN(0)); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let latestBlockNumber = new BN(tx.receipt.blockNumber); + checkPoolInfo( + poolInfo, + token1.address, + allocationPoint, + latestBlockNumber, + new BN(-1) + ); + + await checkUserPoolTokens(account1, token1, new BN(0), new BN(0), amount); + let userReward = await checkUserReward( + account1, + token1, + depositBlockNumber, + latestBlockNumber + ); + + // withdrawAndStakeTokensFrom was not invoked + let expectedUnlockedBalance = userReward + .mul(unlockedImmediatelyPercent) + .div(new BN(10000)); + let expectedLockedBalance = userReward.sub(expectedUnlockedBalance); + let unlockedBalance = await lockedSOV.getUnlockedBalance(account1); + let lockedBalance = await lockedSOV.getLockedBalance(account1); + expect(unlockedBalance).bignumber.equal(expectedUnlockedBalance); + expect(lockedBalance).bignumber.equal(expectedLockedBalance); + + expectEvent(tx, "Withdraw", { + user: account1, + poolToken: token1.address, + amount: amount, + }); + + expectEvent(tx, "RewardClaimed", { + user: account1, + poolToken: token1.address, + amount: userReward, + }); + }); + + it("should be able to withdraw using wrapper", async () => { + let depositTx = await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + let depositBlockNumber = new BN(depositTx.receipt.blockNumber); + await SOVToken.transfer(liquidityMining.address, new BN(1000)); + + let tx = await wrapper.withdraw(token1.address, amount, { from: account1 }); + + let poolInfo = await liquidityMining.getPoolInfo(token1.address); + let latestBlockNumber = new BN(tx.receipt.blockNumber); + checkPoolInfo( + poolInfo, + token1.address, + allocationPoint, + latestBlockNumber, + new BN(-1) + ); + + await checkUserPoolTokens( + account1, + token1, + new BN(0), + new BN(0), + amount, + wrapper.address + ); + await checkUserReward(account1, token1, depositBlockNumber, latestBlockNumber); + }); + + it("fails if token pool token not found", async () => { + await expectRevert( + liquidityMining.withdraw(account1, amount, ZERO_ADDRESS, { from: account1 }), + "Pool token not found" + ); + }); + + it("fails if token pool token not found", async () => { + await expectRevert( + liquidityMining.withdraw(token1.address, amount.mul(new BN(2)), ZERO_ADDRESS, { + from: account1, + }), + "Not enough balance" + ); + }); + }); + + describe("emergencyWithdraw", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + await deploymentAndInit(); + await liquidityMining.add(token1.address, allocationPoint, false); + await mineBlocks(1); + + await token1.mint(account1, amount); + await token1.approve(liquidityMining.address, amount, { from: account1 }); + }); + + it("should be able to withdraw", async () => { + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + let tx = await liquidityMining.emergencyWithdraw(token1.address, { from: account1 }); + + let totalUsersBalance = await liquidityMining.totalUsersBalance(); + expect(totalUsersBalance).bignumber.equal(new BN(0)); + + await checkUserPoolTokens(account1, token1, new BN(0), new BN(0), amount); + + let userInfo = await liquidityMining.getUserInfo(token1.address, account1); + expect(userInfo.rewardDebt).bignumber.equal(new BN(0)); + expect(userInfo.accumulatedReward).bignumber.equal(new BN(0)); + + let bonusBlockMultiplier = await liquidityMining.BONUS_BLOCK_MULTIPLIER(); + let expectedAccumulatedReward = rewardTokensPerBlock.mul(bonusBlockMultiplier); + expectEvent(tx, "EmergencyWithdraw", { + user: account1, + poolToken: token1.address, + amount: amount, + accumulatedReward: expectedAccumulatedReward, + }); + }); + + it("fails if token pool token not found", async () => { + await expectRevert( + liquidityMining.emergencyWithdraw(account1, { from: account1 }), + "Pool token not found" + ); + }); + }); + + describe("getPassedBlocksWithBonusMultiplier", () => { + it("check calculation", async () => { + let bonusBlockMultiplier = await liquidityMining.BONUS_BLOCK_MULTIPLIER(); + let startBlock = await liquidityMining.startBlock(); + let bonusEndBlock = await liquidityMining.bonusEndBlock(); + let blocks; + + // [startBlock, bonusEndBlock] + blocks = await liquidityMining.getPassedBlocksWithBonusMultiplier( + startBlock, + bonusEndBlock + ); + expect(blocks).bignumber.equal(numberOfBonusBlocks.mul(bonusBlockMultiplier)); + + // [startBlock - 100, bonusEndBlock] + blocks = await liquidityMining.getPassedBlocksWithBonusMultiplier( + startBlock.sub(new BN(100)), + bonusEndBlock + ); + expect(blocks).bignumber.equal(numberOfBonusBlocks.mul(bonusBlockMultiplier)); + + // [startBlock, bonusEndBlock + 100] + let blocksAfterBonusPeriod = new BN(100); + blocks = await liquidityMining.getPassedBlocksWithBonusMultiplier( + startBlock, + bonusEndBlock.add(new BN(blocksAfterBonusPeriod)) + ); + expect(blocks).bignumber.equal( + numberOfBonusBlocks.mul(bonusBlockMultiplier).add(blocksAfterBonusPeriod) + ); + + // [startBlock, stopMining, ... bonusEndBlock] + await mineBlocks(5); + await liquidityMining.stopMining(); + let endBlock = await liquidityMining.endBlock(); + blocks = await liquidityMining.getPassedBlocksWithBonusMultiplier( + startBlock, + bonusEndBlock + ); + expect(blocks).bignumber.equal(endBlock.sub(startBlock).mul(bonusBlockMultiplier)); + }); + }); + + describe("getUserAccumulatedReward", () => { + const amount1 = new BN(1000); + const amount2 = new BN(2000); + const allocationPoint1 = new BN(1); + const allocationPoint2 = new BN(2); + const totalAllocationPoint = allocationPoint1.add(allocationPoint2); + let bonusBlockMultiplier; + let bonusEndBlock; + + beforeEach(async () => { + await deploymentAndInit(); + await liquidityMining.add(token1.address, allocationPoint1, false); + await liquidityMining.add(token2.address, allocationPoint2, false); + + await token1.mint(account1, amount1); + await token2.mint(account2, amount2); + + await token1.approve(liquidityMining.address, amount1, { from: account1 }); + await token2.approve(liquidityMining.address, amount2, { from: account2 }); + + bonusBlockMultiplier = await liquidityMining.BONUS_BLOCK_MULTIPLIER(); + bonusEndBlock = await liquidityMining.bonusEndBlock(); + }); + + it("check calculation for no deposits", async () => { + const reward1 = await liquidityMining.getUserAccumulatedReward( + token1.address, + account1 + ); + const reward2 = await liquidityMining.getUserAccumulatedReward( + token2.address, + account2 + ); + expect(reward1).bignumber.equal("0"); + expect(reward2).bignumber.equal("0"); + }); + + it("check calculation for single user, token 1, bonus period off", async () => { + await advanceBlocks(bonusEndBlock); + await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { + from: account1, + }); + await mineBlock(); + let reward = await liquidityMining.getUserAccumulatedReward(token1.address, account1); + + // 1 block has passed, bonus period is off + // users are given 3 tokens per share per block. user1 owns 100% of the shares + // token 1 counts as 1/3 of the pool + // reward = 1 * 3 * 1/3 = 1 + const expectedReward = rewardTokensPerBlock + .mul(allocationPoint1) + .div(totalAllocationPoint); + expect(expectedReward).bignumber.equal("1"); // sanity check + expect(reward).bignumber.equal(expectedReward); + + await mineBlock(); + reward = await liquidityMining.getUserAccumulatedReward(token1.address, account1); + expect(reward).bignumber.equal("2"); + }); + + it("check calculation for single user, token 2, bonus period off", async () => { + await advanceBlocks(bonusEndBlock); + await liquidityMining.deposit(token2.address, amount2, ZERO_ADDRESS, { + from: account2, + }); + await mineBlock(); + let reward = await liquidityMining.getUserAccumulatedReward(token2.address, account2); + + // 1 block has passed, bonus period is off + // users are given 3 tokens per share per block. user2 owns 100% of the shares + // token 2 counts as 2/3 of the pool + // reward = 1 * 3 * 2/3 = 2 + const expectedReward = rewardTokensPerBlock + .mul(allocationPoint2) + .div(totalAllocationPoint); + expect(expectedReward).bignumber.equal("2"); // sanity check + expect(reward).bignumber.equal(expectedReward); + + await mineBlock(); + reward = await liquidityMining.getUserAccumulatedReward(token2.address, account2); + expect(reward).bignumber.equal("4"); + }); + + it("check calculation for single user, token 1, bonus period on", async () => { + await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { + from: account1, + }); + await mineBlock(); + const reward = await liquidityMining.getUserAccumulatedReward( + token1.address, + account1 + ); + + // 1 block has passed, bonus period is on so it counts as 10 blocks, + // users are given 3 tokens per share per block. user1 owns 100% of the shares + // token 1 counts as 1/3 of the pool + // reward = 10 * 3 * 1/3 = 10 + const expectedReward = rewardTokensPerBlock + .mul(bonusBlockMultiplier) + .mul(allocationPoint1) + .div(totalAllocationPoint); + expect(expectedReward).bignumber.equal("10"); // sanity check + expect(reward).bignumber.equal(expectedReward); + }); + + it("check calculation for single user, token 1, bonus period on, smaller amount", async () => { + await liquidityMining.deposit(token1.address, new BN(1), ZERO_ADDRESS, { + from: account1, + }); + await mineBlock(); + const reward = await liquidityMining.getUserAccumulatedReward( + token1.address, + account1 + ); + + // 1 block has passed, bonus period is on so it counts as 10 blocks, + // users are given 3 tokens per share per block. user1 owns 100% of the shares + // token 1 counts as 1/3 of the pool + // reward = 10 * 3 * 1/3 = 10 + // Note that the actual amount deposited plays no role here + expect(reward).bignumber.equal("10"); + }); + + it("check calculation for single user, token 2, bonus period on", async () => { + await liquidityMining.deposit(token2.address, amount2, ZERO_ADDRESS, { + from: account2, + }); + await mineBlock(); + const reward = await liquidityMining.getUserAccumulatedReward( + token2.address, + account2 + ); + + // 1 block has passed, bonus period is on so it counts as 10 blocks, + // users are given 3 tokens per share per block. user2 owns 100% of the shares + // token 2 counts as 2/3 of the pool + // reward = 10 * 3 * 2/3 = 20 + const expectedReward = rewardTokensPerBlock + .mul(bonusBlockMultiplier) + .mul(allocationPoint2) + .div(totalAllocationPoint); + expect(expectedReward).bignumber.equal("20"); // sanity check + expect(reward).bignumber.equal(expectedReward); + }); + + it("check calculation for two users and tokens", async () => { + await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { + from: account1, + }); + // because automining is on, the following will advance a block + await liquidityMining.deposit(token2.address, amount2, ZERO_ADDRESS, { + from: account2, + }); + // sanity checks + expect( + await liquidityMining.getUserAccumulatedReward(token1.address, account1) + ).bignumber.equal("10"); + expect( + await liquidityMining.getUserAccumulatedReward(token2.address, account2) + ).bignumber.equal("0"); + await mineBlock(); + + const reward1 = await liquidityMining.getUserAccumulatedReward( + token1.address, + account1 + ); + const reward2 = await liquidityMining.getUserAccumulatedReward( + token2.address, + account2 + ); + + // for the first block, user 1 will receive the reward of 10 + // for the second block: + // - user 1 still owns 100% of the shares for token1, so same reward (total 10 + 10 = 20) + // - user 2 owns 100% of the shares for token2, so same reward as in the other cases + expect(reward1).bignumber.equal("20"); + expect(reward2).bignumber.equal("20"); + }); + + it("check calculation for two users, same token (shares taken into account)", async () => { + const token = token1; + const amount = amount1; + await token.mint(account2, amount); + await token.approve(liquidityMining.address, amount, { from: account2 }); + + await liquidityMining.deposit(token.address, amount, ZERO_ADDRESS, { from: account1 }); + // because automining is on, the following will advance a block + await liquidityMining.deposit(token.address, amount, ZERO_ADDRESS, { from: account2 }); + // sanity checks + expect( + await liquidityMining.getUserAccumulatedReward(token.address, account1) + ).bignumber.equal("10"); + expect( + await liquidityMining.getUserAccumulatedReward(token.address, account2) + ).bignumber.equal("0"); + await mineBlock(); + + const reward1 = await liquidityMining.getUserAccumulatedReward( + token.address, + account1 + ); + const reward2 = await liquidityMining.getUserAccumulatedReward( + token.address, + account2 + ); + + // for the first block, user 1 will receive the reward of 10 (reward given per block for 100% of shares) + // for the second block: + // - user 1 owns 1/2 of the shares => expected reward = 5 (total 10 + 5 = 15) + // - user 2 owns 1/2 of the shares => expected reward = 5 + expect(reward1).bignumber.equal("15"); + expect(reward2).bignumber.equal("5"); + }); + }); + + describe("getEstimatedReward", () => { + const amount1 = new BN(1000); + const amount2 = new BN(2000); + const amount3 = new BN(4000); + const allocationPoint1 = new BN(1); + const allocationPoint2 = new BN(2); + + const totalAllocationPoint = allocationPoint1.add(allocationPoint2); + let bonusBlockMultiplier; + let bonusEndBlock; + let secondsPerBlock; + + beforeEach(async () => { + await deploymentAndInit(); + await liquidityMining.add(token1.address, allocationPoint1, false); + + await token1.mint(account1, amount1); + await token1.mint(account2, amount2); + await token1.mint(account3, amount3); + + await token1.approve(liquidityMining.address, amount1, { from: account1 }); + await token1.approve(liquidityMining.address, amount2, { from: account2 }); + + bonusBlockMultiplier = await liquidityMining.BONUS_BLOCK_MULTIPLIER(); + bonusEndBlock = await liquidityMining.bonusEndBlock(); + + secondsPerBlock = await liquidityMining.SECONDS_PER_BLOCK(); + }); + + it("check calculation for 1 user, period less than 1 block", async () => { + let duration = secondsPerBlock.sub(new BN(1)); + + let estimatedReward = await liquidityMining.getEstimatedReward( + token1.address, + amount3, + duration + ); + let expectedReward = "0"; + expect(estimatedReward).bignumber.equal(expectedReward); + }); + + it("check calculation for 1 user, period is 1 block", async () => { + let duration = secondsPerBlock; + + let estimatedReward = await liquidityMining.getEstimatedReward( + token1.address, + amount3, + duration + ); + let expectedReward = rewardTokensPerBlock.mul(bonusBlockMultiplier); + expect(estimatedReward).bignumber.equal(expectedReward); + }); + + it("check calculation for 1 user, period is 40 blocks", async () => { + let blocks = new BN(40); + let duration = secondsPerBlock.mul(blocks); + + let estimatedReward = await liquidityMining.getEstimatedReward( + token1.address, + amount3, + duration + ); + let expectedReward = rewardTokensPerBlock.mul(blocks).mul(bonusBlockMultiplier); + expect(estimatedReward).bignumber.equal(expectedReward); + }); + + it("check calculation for 2 users, period is 100 blocks", async () => { + // turn off bonus period + await advanceBlocks(bonusEndBlock); + + let blocks = new BN(100); + let duration = secondsPerBlock.mul(blocks); + + await token1.approve(liquidityMining.address, amount1, { from: account1 }); + await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { + from: account1, + }); + + let estimatedReward = await liquidityMining.getEstimatedReward( + token1.address, + amount3, + duration + ); + let expectedReward = rewardTokensPerBlock.mul(blocks); + let totalAmount = amount1.add(amount3); + expectedReward = expectedReward.mul(amount3).div(totalAmount); + expect(estimatedReward).bignumber.equal(expectedReward); + }); + + it("check calculation for 3 users and 2 tokens, period is 1000 blocks", async () => { + await liquidityMining.add(token2.address, allocationPoint2, false); + // turn off bonus period + await advanceBlocks(bonusEndBlock); + + let blocks = new BN(1000); + let duration = secondsPerBlock.mul(blocks); + + await token1.approve(liquidityMining.address, amount1, { from: account1 }); + await liquidityMining.deposit(token1.address, amount1, ZERO_ADDRESS, { + from: account1, + }); + await token1.approve(liquidityMining.address, amount2, { from: account2 }); + await liquidityMining.deposit(token1.address, amount2, ZERO_ADDRESS, { + from: account2, + }); + + let estimatedReward = await liquidityMining.getEstimatedReward( + token1.address, + amount3, + duration + ); + let expectedReward = rewardTokensPerBlock.mul(blocks); + expectedReward = expectedReward.mul(allocationPoint1).div(totalAllocationPoint); + let totalAmount = amount1.add(amount2).add(amount3); + expectedReward = expectedReward.mul(amount3).div(totalAmount); + expect(estimatedReward).bignumber.equal(expectedReward); + }); + }); + + describe("deposit/withdraw", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + await deploymentAndInit(); + for (let token of [token1, token2]) { + for (let account of [account1, account2]) { + await token.mint(account, amount); + await token.approve(liquidityMining.address, amount, { from: account }); + } + } + + // make sure the pool has tokens to distribute + await SOVToken.transfer(liquidityMining.address, new BN(1000)); + }); + + it("add, add, deposit, deposit", async () => { + await liquidityMining.add(token1.address, allocationPoint, false); // weight 1/1 + await liquidityMining.add(token2.address, allocationPoint, false); // weight 1/2 + + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + await liquidityMining.deposit(token2.address, amount, ZERO_ADDRESS, { + from: account1, + }); // 1 block passed + + // await liquidityMining.update(token1.address, allocationPoint.mul(new BN(2)), true); // weight 2/3 + await liquidityMining.updateAllPools(); // 2 blocks passed from first deposit + + const currentBlockNumber = await web3.eth.getBlockNumber(); + + // 3 tokens per share per block, times bonus multiplier (10), times precision (1e12), times weight (1/2), divided by total shares + const expectedAccumulatedRewardPerBlock = rewardTokensPerBlock + .mul(new BN(10)) + .mul(new BN(1e12)) + .div(new BN(2)) + .div(amount); + + const poolInfo1 = await liquidityMining.getPoolInfo(token1.address); + expect(poolInfo1.poolToken).equal(token1.address); + expect(poolInfo1.allocationPoint).equal("1"); + expect(poolInfo1.lastRewardBlock).equal(currentBlockNumber.toString()); + // token1 deposit has been there for 2 blocks because of automining + expect(poolInfo1.accumulatedRewardPerShare).equal( + expectedAccumulatedRewardPerBlock.mul(new BN(2)).toString() + ); + + const poolInfo2 = await liquidityMining.getPoolInfo(token2.address); + expect(poolInfo2.poolToken).equal(token2.address); + expect(poolInfo2.allocationPoint).equal("1"); + expect(poolInfo1.lastRewardBlock).equal(currentBlockNumber.toString()); + // token2 deposit has been there for only 1 block + expect(poolInfo2.accumulatedRewardPerShare).equal( + expectedAccumulatedRewardPerBlock.toString() + ); + }); + + // tricky case 1 + it("add(pool1), add(pool2), deposit(user1, pool1), update(pool1), withdraw(user1, pool1)", async () => { + await liquidityMining.add(token1.address, allocationPoint, false); // weight 1/1 + await liquidityMining.add(token2.address, allocationPoint, false); // weight 1/2 + + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + await liquidityMining.update(token1.address, new BN("2"), false); // 1 block passed, new weight 2/3 + const tx = await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); // 2 blocks passed + + await checkBonusPeriodHasNotEnded(); // sanity check, it's included in calculations + + const lockedAmount = await lockedSOV.getLockedBalance(account1); + const unlockedAmount = await lockedSOV.getUnlockedBalance(account1); + const rewardAmount = lockedAmount.add(unlockedAmount); + + // reward per block 30 (because of bonus period), 1 block with weight 1/2 = 15, 1 block with weight 2/3 = 20 + const expectedRewardAmount = new BN("35"); + expect(rewardAmount).bignumber.equal(expectedRewardAmount); + + await checkUserPoolTokens( + account1, + token1, + new BN(0), // user LM balance + new BN(0), // LM contract token balance + amount // user token balance + ); + + expectEvent(tx, "Withdraw", { + user: account1, + poolToken: token1.address, + amount: amount, + }); + + expectEvent(tx, "RewardClaimed", { + user: account1, + poolToken: token1.address, + amount: rewardAmount, + }); + }); + + // tricky case 2 + it("add(pool1), deposit(user1, pool1), deposit(user2, pool1), withdraw(user1, pool1), withdraw(user2, pool1)", async () => { + await liquidityMining.add(token1.address, allocationPoint, false); // weight 1/1 + + // deposit 1: 0 blocks, deposit 2: 0 blocks + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + // deposit 1: 1 blocks (100% shares), deposit 2: 0 blocks + await mineBlock(); + + // deposit 1: 2 blocks (100% shares), deposit 2: 0 blocks + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account2, + }); + + // deposit 1: 3 blocks (50% shares), deposit 2: 1 blocks (50% shares) + const withdrawTx1 = await liquidityMining.withdraw( + token1.address, + amount, + ZERO_ADDRESS, + { from: account1 } + ); + + // deposit 1: 3 blocks (withdrawn), deposit 2: 2 blocks (100% shares) + const withdrawTx2 = await liquidityMining.withdraw( + token1.address, + amount, + ZERO_ADDRESS, + { from: account2 } + ); + + await checkBonusPeriodHasNotEnded(); // sanity check, it's included in calculations + + const lockedAmount1 = await lockedSOV.getLockedBalance(account1); + const unlockedAmount1 = await lockedSOV.getUnlockedBalance(account1); + const reward1 = lockedAmount1.add(unlockedAmount1); + + const lockedAmount2 = await lockedSOV.getLockedBalance(account2); + const unlockedAmount2 = await lockedSOV.getUnlockedBalance(account2); + const reward2 = lockedAmount2.add(unlockedAmount2); + + // reward per block 30 (because of bonus period), 2 block with 100% shares = 60, 1 block with 50% shares = 15 + const expectedReward1 = new BN("75"); + + // reward per block 30 (because of bonus period), 1 block with 50% shares = 15, 1 block with 100% shares = 30 + const expectedReward2 = new BN("45"); + + expect(reward1).bignumber.equal(expectedReward1); + expect(reward2).bignumber.equal(expectedReward2); + + await checkUserPoolTokens( + account1, + token1, + new BN(0), // user LM balance + new BN(0), // LM contract token balance + amount // user token balance + ); + await checkUserPoolTokens( + account2, + token1, + new BN(0), // user LM balance + new BN(0), // LM contract token balance + amount // user token balance + ); + + expectEvent(withdrawTx1, "Withdraw", { + user: account1, + poolToken: token1.address, + amount: amount, + }); + expectEvent(withdrawTx1, "RewardClaimed", { + user: account1, + poolToken: token1.address, + amount: reward1, + }); + expectEvent(withdrawTx2, "Withdraw", { + user: account2, + poolToken: token1.address, + amount: amount, + }); + expectEvent(withdrawTx2, "RewardClaimed", { + user: account2, + poolToken: token1.address, + amount: reward2, + }); + }); + + // tricky case 3a + it("add(pool1), deposit(user1, pool1), add(pool2, no update), withdraw(user1, pool1)", async () => { + await liquidityMining.add(token1.address, allocationPoint, false); // weight 1/1 + + // deposit: 0 blocks + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + // deposit: 1 blocks, note: pool1 is NOT updated + await liquidityMining.add(token2.address, new BN(2), false); // new weight: 1/3 + + // deposit: 2 blocks + await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + await checkBonusPeriodHasNotEnded(); // sanity check, it's included in calculations + + const lockedAmount = await lockedSOV.getLockedBalance(account1); + const unlockedAmount = await lockedSOV.getUnlockedBalance(account1); + const rewardAmount = lockedAmount.add(unlockedAmount); + + // reward per block 30 (because of bonus period), + // because add was called without updating the pool, the new weight is used for all blocks + // so 2 blocks with weight 1/3 = 20 + const expectedRewardAmount = new BN("20"); + expect(rewardAmount).bignumber.equal(expectedRewardAmount); + + await checkUserPoolTokens( + account1, + token1, + new BN(0), // user LM balance + new BN(0), // LM contract token balance + amount // user token balance + ); + }); + + // tricky case 3b + it("add(pool1), deposit(user1, pool1), add(pool2, update), withdraw(user1, pool1)", async () => { + await liquidityMining.add(token1.address, allocationPoint, false); // weight 1/1 + + // deposit: 0 blocks + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + // deposit: 1 blocks, note: pool1 IS updated + await liquidityMining.add(token2.address, new BN(2), true); // new weight: 1/3 + + // deposit: 2 blocks + await liquidityMining.withdraw(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + await checkBonusPeriodHasNotEnded(); // sanity check, it's included in calculations + + const lockedAmount = await lockedSOV.getLockedBalance(account1); + const unlockedAmount = await lockedSOV.getUnlockedBalance(account1); + const rewardAmount = lockedAmount.add(unlockedAmount); + + // reward per block 30 (because of bonus period), + // because add was called WITH updating the pools, old weight is for 1 block and new weight is for 1 block + // so 1 block with weight 1/1 = 30 and 1 block with weight 1/3 = 10 + const expectedRewardAmount = new BN("40"); + expect(rewardAmount).bignumber.equal(expectedRewardAmount); + + await checkUserPoolTokens( + account1, + token1, + new BN(0), // user LM balance + new BN(0), // LM contract token balance + amount // user token balance + ); + }); + + // tricky case 4 + it("add(pool1), deposit(user1, pool1), add(pool2), deposit(user2, pool2), withdraw(user1, pool1), withdraw(user2, pool2)", async () => { + await liquidityMining.add(token1.address, allocationPoint, false); // weight 1/1 + + // deposit 1: 0 blocks, deposit 2: 0 blocks + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + + // deposit 1: 1 blocks (weight 1/1), deposit 2: 0 blocks. pool is updated + await liquidityMining.add(token2.address, allocationPoint, true); // weight 1/2 + + // deposit 1: 2 blocks (weight 1/2), deposit 2: 0 blocks + await liquidityMining.deposit(token2.address, amount, ZERO_ADDRESS, { + from: account2, + }); + + // deposit 1: 3 blocks (weight 1/2), deposit 2: 1 blocks (weight 1/2) + const withdrawTx1 = await liquidityMining.withdraw( + token1.address, + amount, + ZERO_ADDRESS, + { from: account1 } + ); + + // deposit 1: 3 blocks (withdrawn), deposit 2: 2 blocks (weight 1/2) + const withdrawTx2 = await liquidityMining.withdraw( + token2.address, + amount, + ZERO_ADDRESS, + { from: account2 } + ); + + await checkBonusPeriodHasNotEnded(); // sanity check, it's included in calculations + + const lockedAmount1 = await lockedSOV.getLockedBalance(account1); + const unlockedAmount1 = await lockedSOV.getUnlockedBalance(account1); + const reward1 = lockedAmount1.add(unlockedAmount1); + + const lockedAmount2 = await lockedSOV.getLockedBalance(account2); + const unlockedAmount2 = await lockedSOV.getUnlockedBalance(account2); + const reward2 = lockedAmount2.add(unlockedAmount2); + + // reward per block 30 (because of bonus period) + // deposit 1 has 1 block with weight 1/1 (30) and 2 blocks with weight 1/2 (15*2 = 30) + const expectedReward1 = new BN("60"); + + // deposit 2 has 2 blocks with weight 1/2 (15 * 2 = 30) + const expectedReward2 = new BN("30"); + + expect(reward1).bignumber.equal(expectedReward1); + expect(reward2).bignumber.equal(expectedReward2); + + for (let account of [account1, account2]) { + for (let token of [token1, token2]) { + await checkUserPoolTokens( + account, + token, + new BN(0), // user LM balance + new BN(0), // LM contract token balance + amount // user token balance + ); + } + } + + expectEvent(withdrawTx1, "Withdraw", { + user: account1, + poolToken: token1.address, + amount: amount, + }); + expectEvent(withdrawTx1, "RewardClaimed", { + user: account1, + poolToken: token1.address, + amount: reward1, + }); + expectEvent(withdrawTx2, "Withdraw", { + user: account2, + poolToken: token2.address, + amount: amount, + }); + expectEvent(withdrawTx2, "RewardClaimed", { + user: account2, + poolToken: token2.address, + amount: reward2, + }); + }); + }); + + describe("LM configuration", () => { + // Maximum reward per week: 100K SOV (or 100M SOV) + // Maximum reward per block: 4.9604 SOV (4.9604 * 2880 * 7 = 100001.664) + + const REWARD_TOKENS_PER_BLOCK = new BN(49604).mul(new BN(10 ** 14)).mul(new BN(1000)); + // const REWARD_TOKENS_PER_BLOCK = new BN(49604).mul(new BN(10**14)); + + // SOV/BTC pool 40K per week + // ETH/BTC pool 37.5K per week (from second week) + // Dummy pool 100K - SOV/BTC pool (- ETH/BTC pool) + + const MAX_ALLOCATION_POINT = new BN(100000).mul(new BN(1000)); + // const MAX_ALLOCATION_POINT = new BN(100000); + const ALLOCATION_POINT_SOV_BTC = new BN(40000); + const ALLOCATION_POINT_ETH_BTC = new BN(37500); + + const ALLOCATION_POINT_SOV_BTC_2 = new BN(30000); + + const amount = new BN(1000); + + beforeEach(async () => { + await deployLiquidityMining(); + await liquidityMining.initialize( + SOVToken.address, + REWARD_TOKENS_PER_BLOCK, + startDelayBlocks, + numberOfBonusBlocks, + wrapper.address, + lockedSOV.address, + 0 + ); + + for (let token of [token1, token2]) { + for (let account of [account1, account2]) { + await token.mint(account, amount); + await token.approve(liquidityMining.address, amount, { from: account }); + } + } + + // turn off bonus period + let bonusEndBlock = await liquidityMining.bonusEndBlock(); + await advanceBlocks(bonusEndBlock); + }); + + it("dummy pool + 1 pool", async () => { + let dummyPool = liquidityMiningConfigToken.address; + + let SOVBTCpool = token1.address; + + await liquidityMining.add(SOVBTCpool, ALLOCATION_POINT_SOV_BTC, false); // weight 40000 / 100000 + await liquidityMining.add( + dummyPool, + MAX_ALLOCATION_POINT.sub(ALLOCATION_POINT_SOV_BTC), + false + ); // weight (100000 - 40000) / 100000 + + await liquidityMining.deposit(SOVBTCpool, amount, ZERO_ADDRESS, { from: account1 }); + + // reward won't be claimed because liquidityMining doesn't have enough SOV balance + // user reward will be updated + // 10 blocks passed since last deposit + await mineBlocks(9); + await liquidityMining.withdraw(SOVBTCpool, amount, ZERO_ADDRESS, { from: account1 }); + + const userInfo = await liquidityMining.getUserInfo(SOVBTCpool, account1); + // 10 blocks passed + let passedBlocks = 10; + let expectedUserReward = REWARD_TOKENS_PER_BLOCK.mul(new BN(passedBlocks)) + .mul(ALLOCATION_POINT_SOV_BTC) + .div(MAX_ALLOCATION_POINT); + expect(userInfo.accumulatedReward).bignumber.equal(expectedUserReward); + console.log(expectedUserReward.toString()); + }); + + it("dummy pool + 2 pools", async () => { + let dummyPool = liquidityMiningConfigToken.address; + + let SOVBTCpool = token1.address; + let ETHBTCpoll = token2.address; + + await liquidityMining.add(SOVBTCpool, ALLOCATION_POINT_SOV_BTC, false); // weight 40000 / 100000 + const DUMMY_ALLOCATION_POINT = MAX_ALLOCATION_POINT.sub(ALLOCATION_POINT_SOV_BTC); + await liquidityMining.add(dummyPool, DUMMY_ALLOCATION_POINT, false); // weight (100000 - 40000) / 100000 + + await liquidityMining.deposit(SOVBTCpool, amount, ZERO_ADDRESS, { from: account1 }); + + await mineBlocks(9); + await liquidityMining.updateAllPools(); // 10 blocks passed from first deposit + + // update config + // this method will also update pool reward using previous allocation point, + // so this block should be add to calculation with old values + await liquidityMining.update(SOVBTCpool, ALLOCATION_POINT_SOV_BTC_2, false); // weight 30000 / 100000 + + await liquidityMining.add(ETHBTCpoll, ALLOCATION_POINT_ETH_BTC, false); // weight 37500 / 100000 + const DUMMY_ALLOCATION_POINT_2 = MAX_ALLOCATION_POINT.sub( + ALLOCATION_POINT_SOV_BTC_2 + ).sub(ALLOCATION_POINT_ETH_BTC); + await liquidityMining.update(dummyPool, DUMMY_ALLOCATION_POINT_2, false); // weight (100000 - 30000 - 37500) / 100000 + await liquidityMining.updateAllPools(); + + // reward won't be claimed because liquidityMining doesn't have enough SOV balance + // user reward will be updated + // 10 blocks + 5 blocks passed + await liquidityMining.withdraw(SOVBTCpool, amount, ZERO_ADDRESS, { from: account1 }); + + const userInfo = await liquidityMining.getUserInfo(SOVBTCpool, account1); + // 10 blocks + 5 blocks passed + let passedBlocks = 10 + 1; // block should be add to calculation with old values + let expectedUserReward = REWARD_TOKENS_PER_BLOCK.mul(new BN(passedBlocks)) + .mul(ALLOCATION_POINT_SOV_BTC) + .div(MAX_ALLOCATION_POINT); + passedBlocks = 5 - 1; // block should be removed from calculation with new values + expectedUserReward = expectedUserReward.add( + REWARD_TOKENS_PER_BLOCK.mul(new BN(passedBlocks)) + .mul(ALLOCATION_POINT_SOV_BTC_2) + .div(MAX_ALLOCATION_POINT) + ); + expect(userInfo.accumulatedReward).bignumber.equal(expectedUserReward); + console.log(expectedUserReward.toString()); + }); + }); + + describe("onTokensDeposited", () => { + it("should revert if the sender is not a valid pool token", async () => { + await expectRevert( + liquidityMining.onTokensDeposited(ZERO_ADDRESS, new BN(1000)), + "Pool token not found" + ); + }); + }); + + describe("external getters", () => { + let allocationPoint = new BN(1); + let amount = new BN(1000); + + beforeEach(async () => { + await deploymentAndInit(); + await token1.mint(account1, amount); + await token1.approve(liquidityMining.address, amount, { from: account1 }); + await liquidityMining.add(token1.address, allocationPoint, false); + }); + + it("PRECISION", async () => { + expect(await liquidityMining.PRECISION()).bignumber.equal(new BN(1e12)); + }); + + it("BONUS_BLOCK_MULTIPLIER", async () => { + expect(await liquidityMining.BONUS_BLOCK_MULTIPLIER()).bignumber.equal("10"); + }); + + it("SVR", async () => { + expect(await liquidityMining.SOV()).equal(SOVToken.address); + }); + + it("rewardTokensPerBlock", async () => { + expect(await liquidityMining.rewardTokensPerBlock()).bignumber.equal( + rewardTokensPerBlock + ); + }); + + it("startBlock", async () => { + expect(await liquidityMining.startBlock()).bignumber.gt("0"); + }); + + it("bonusEndBlock", async () => { + const startBlock = await liquidityMining.startBlock(); + expect(await liquidityMining.bonusEndBlock()).bignumber.equal( + startBlock.add(numberOfBonusBlocks) + ); + }); + + it("endBlock", async () => { + expect(await liquidityMining.endBlock()).bignumber.equal("0"); + }); + + it("wrapper", async () => { + expect(await liquidityMining.wrapper()).equal(wrapper.address); + }); + + it("totalAllocationPoint", async () => { + expect(await liquidityMining.totalAllocationPoint()).bignumber.equal(allocationPoint); + await liquidityMining.add(token2.address, allocationPoint, false); + expect(await liquidityMining.totalAllocationPoint()).bignumber.equal( + allocationPoint.mul(new BN(2)) + ); + }); + + it("totalUsersBalance", async () => { + expect(await liquidityMining.totalUsersBalance()).bignumber.equal("0"); + + await liquidityMining.updateAllPools(); + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + expect(await liquidityMining.totalUsersBalance()).bignumber.equal("0"); + + await liquidityMining.updateAllPools(); + expect(await liquidityMining.totalUsersBalance()).bignumber.equal("30"); + }); + + // could still test these, but I don't see much point: + // PoolInfo[] public poolInfoList; + // mapping(address => uint256) poolIdList; + // mapping(uint256 => mapping(address => UserInfo)) public userInfoMap; + + it("getMissedBalance", async () => { + let missedBalance = await liquidityMining.getMissedBalance(); + expect(missedBalance).bignumber.equal("0"); + + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + await liquidityMining.updatePool(token1.address); + + missedBalance = await liquidityMining.getMissedBalance(); + expect(missedBalance).bignumber.equal("30"); + }); + + it("getUserAccumulatedReward", async () => { + // real tests are elsewhere in this file + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + await mineBlock(); + const reward1 = await liquidityMining.getUserAccumulatedReward( + token1.address, + account1 + ); + const reward2 = await liquidityMining.getUserAccumulatedReward( + token1.address, + account2 + ); + expect(reward1).bignumber.equal("30"); + expect(reward2).bignumber.equal("0"); + }); + + it("getPoolId", async () => { + const poolId = await liquidityMining.getPoolId(token1.address); + expect(poolId).bignumber.equal("0"); + await expectRevert(liquidityMining.getPoolId(token2.address), "Pool token not found"); + await liquidityMining.add(token2.address, allocationPoint, false); + const poolId2 = await liquidityMining.getPoolId(token2.address); + expect(poolId2).bignumber.equal("1"); + }); + + it("getPoolLength", async () => { + let length = await liquidityMining.getPoolLength(); + expect(length).bignumber.equal("1"); + + await liquidityMining.add(token2.address, allocationPoint, false); + length = await liquidityMining.getPoolLength(); + expect(length).bignumber.equal("2"); + }); + + it("getPoolInfoList", async () => { + const infoList = await liquidityMining.getPoolInfoList(); + expect(infoList).to.be.an("array"); + expect(infoList.length).equal(1); + const info = infoList[0]; + expect(info.poolToken).equal(token1.address); + expect(info.allocationPoint).equal(allocationPoint.toString()); + expect(info.accumulatedRewardPerShare).equal("0"); + expect(info.lastRewardBlock).equal((await web3.eth.getBlockNumber()).toString()); + }); + + it("getPoolInfo", async () => { + const info = await liquidityMining.getPoolInfo(token1.address); + expect(info.poolToken).equal(token1.address); + expect(info.allocationPoint).equal(allocationPoint.toString()); + expect(info.accumulatedRewardPerShare).equal("0"); + expect(info.lastRewardBlock).equal((await web3.eth.getBlockNumber()).toString()); + + await expectRevert( + liquidityMining.getPoolInfo(token2.address), + "Pool token not found" + ); + }); + + it("getUserBalanceList", async () => { + await liquidityMining.deposit(token1.address, amount, ZERO_ADDRESS, { + from: account1, + }); + await mineBlock(); + const balanceList = await liquidityMining.getUserBalanceList(account1); + + expect(balanceList).to.be.an("array"); + expect(balanceList.length).equal(1); + const balanceData = balanceList[0]; + expect(balanceData).to.be.an("array"); + expect(balanceData[0]).bignumber.equal(amount); + expect(balanceData[1]).bignumber.equal("30"); + }); + + it("getUserInfo", async () => { + await liquidityMining.deposit(token1.address, new BN(500), ZERO_ADDRESS, { + from: account1, + }); + + let userInfo = await liquidityMining.getUserInfo(token1.address, account1); + expect(userInfo.amount).bignumber.equal("500"); + expect(userInfo.accumulatedReward).bignumber.equal("0"); // XXX: not yet updated -- funny? + expect(userInfo.rewardDebt).bignumber.equal("0"); // not yet updated either + + // deposit updates it. + await liquidityMining.deposit(token1.address, new BN(1), ZERO_ADDRESS, { + from: account1, + }); + userInfo = await liquidityMining.getUserInfo(token1.address, account1); + expect(userInfo.amount).bignumber.equal("501"); + expect(userInfo.accumulatedReward).bignumber.equal("30"); + expect(userInfo.rewardDebt).bignumber.equal("30"); + }); + + it("getUserInfoList", async () => { + await liquidityMining.deposit(token1.address, new BN(500), ZERO_ADDRESS, { + from: account1, + }); + + let userInfoList = await liquidityMining.getUserInfoList(account1); + expect(userInfoList).to.be.an("array"); + expect(userInfoList.length).equal(1); + const userInfo = userInfoList[0]; + expect(userInfo.amount).bignumber.equal("500"); + expect(userInfo.accumulatedReward).bignumber.equal("0"); + expect(userInfo.rewardDebt).bignumber.equal("0"); + }); + + it("getUserAccumulatedRewardList", async () => { + await liquidityMining.deposit(token1.address, new BN(500), ZERO_ADDRESS, { + from: account1, + }); + + let rewardList = await liquidityMining.getUserAccumulatedRewardList(account1); + expect(rewardList).to.be.an("array"); + expect(rewardList.length).equal(1); + expect(rewardList[0]).bignumber.equal("0"); + }); + + it("getUserPoolTokenBalance", async () => { + await liquidityMining.deposit(token1.address, new BN(500), ZERO_ADDRESS, { + from: account1, + }); + let poolTokenBalance = await liquidityMining.getUserPoolTokenBalance( + token1.address, + account1 + ); + expect(poolTokenBalance).bignumber.equal(new BN(500)); + }); + }); + + async function deployLiquidityMining() { + let liquidityMiningLogic = await LiquidityMiningLogic.new(); + let liquidityMiningProxy = await LiquidityMiningProxy.new(); + await liquidityMiningProxy.setImplementation(liquidityMiningLogic.address); + liquidityMining = await LiquidityMiningLogic.at(liquidityMiningProxy.address); + + wrapper = await Wrapper.new(liquidityMining.address); + } + + async function mineBlocks(blocks) { + for (let i = 0; i < blocks; i++) { + await mineBlock(); + } + } + + function checkPoolInfo( + poolInfo, + token, + allocationPoint, + lastRewardBlock, + accumulatedRewardPerShare + ) { + expect(poolInfo.poolToken).equal(token); + expect(poolInfo.allocationPoint).bignumber.equal(allocationPoint); + expect(poolInfo.lastRewardBlock).bignumber.equal(lastRewardBlock); + if (accumulatedRewardPerShare.toNumber() !== -1) { + expect(poolInfo.accumulatedRewardPerShare).bignumber.equal(accumulatedRewardPerShare); + } + } + + async function checkUserPoolTokens( + user, + poolToken, + _userAmount, + _liquidityMiningBalance, + _userBalance, + wrapper + ) { + // user balance in pool + let userInfo = await liquidityMining.getUserInfo(poolToken.address, user); + expect(userInfo.amount).bignumber.equal(_userAmount); + // LM balance of pool tokens + let liquidityMiningBalance = await poolToken.balanceOf(liquidityMining.address); + expect(liquidityMiningBalance).bignumber.equal(_liquidityMiningBalance); + // user's balance of pool tokens + let userBalance = await poolToken.balanceOf(user); + if (wrapper !== undefined) { + userBalance = await poolToken.balanceOf(wrapper); + } + expect(userBalance).bignumber.equal(_userBalance); + } + + // user's balance of reward token + async function checkUserReward(user, poolToken, depositBlockNumber, latestBlockNumber) { + let passedBlocks = await liquidityMining.getPassedBlocksWithBonusMultiplier( + depositBlockNumber, + latestBlockNumber + ); + let userReward = passedBlocks.mul(rewardTokensPerBlock); + let userInfo = await liquidityMining.getUserInfo(poolToken.address, user); + expect(userInfo.accumulatedReward).bignumber.equal(new BN(0)); + return userReward; + } + + async function checkBonusPeriodHasNotEnded() { + expect(await liquidityMining.bonusEndBlock()).bignumber.gt( + (await web3.eth.getBlockNumber()).toString() + ); + } }); diff --git a/tests/libraries/testLibraries.test.js b/tests/libraries/testLibraries.test.js index ec7566f26..194095255 100644 --- a/tests/libraries/testLibraries.test.js +++ b/tests/libraries/testLibraries.test.js @@ -13,48 +13,66 @@ const TestLibraries = artifacts.require("TestLibraries"); const PUB_KEY_FROM_ZERO = "0xdcc703c0E500B653Ca82273B7BFAd8045D85a470"; contract("TestLibraries", (accounts) => { - let account1, account2; - let testLibraries; - - before(async () => { - [admin, account1, account2] = accounts; - testLibraries = await TestLibraries.new(); - }); - - describe("test RSKAddrValidator", async () => { - it("checkPKNotZero(PUB_KEY_FROM_ZERO_ADDRESS) == false", async () => { - // key derived from zero private address - expect(await testLibraries.RSKAddrValidator_checkPKNotZero(PUB_KEY_FROM_ZERO)).to.be.false; - }); - - it("checkPKNotZero(ZERO_ADDRESS) == true", async () => { - expect(await testLibraries.RSKAddrValidator_checkPKNotZero(constants.ZERO_ADDRESS)).to.be.false; - }); - - it("checkPKNotZero(valid_address) == true", async () => { - expect(await testLibraries.RSKAddrValidator_checkPKNotZero(account1)).to.be.true; - }); - - it("safeEquals(account1, account1) == true", async () => { - expect(await testLibraries.RSKAddrValidator_safeEquals(account1, account1)).to.be.true; - }); - - it("safeEquals(PUB_KEY_FROM_ZERO, PUB_KEY_FROM_ZERO) == false", async () => { - expect(await testLibraries.RSKAddrValidator_safeEquals(PUB_KEY_FROM_ZERO, PUB_KEY_FROM_ZERO)).to.be.false; - }); - - it("safeEquals(ZERO_ADDRESS, ZERO_ADDRESS) == false", async () => { - expect(await testLibraries.RSKAddrValidator_safeEquals(constants.ZERO_ADDRESS, constants.ZERO_ADDRESS)).to.be.false; - }); - - it("safeEquals(account1, ZERO_ADDRESS) == false", async () => { - expect(await testLibraries.RSKAddrValidator_safeEquals(constants.ZERO_ADDRESS, account1)).to.be.false; - expect(await testLibraries.RSKAddrValidator_safeEquals(account1, constants.ZERO_ADDRESS)).to.be.false; - }); - - it("safeEquals(account1, account2 || account2, account1) == false", async () => { - expect(await testLibraries.RSKAddrValidator_safeEquals(account2, account1)).to.be.false; - expect(await testLibraries.RSKAddrValidator_safeEquals(account1, account2)).to.be.false; - }); - }); + let account1, account2; + let testLibraries; + + before(async () => { + [admin, account1, account2] = accounts; + testLibraries = await TestLibraries.new(); + }); + + describe("test RSKAddrValidator", async () => { + it("checkPKNotZero(PUB_KEY_FROM_ZERO_ADDRESS) == false", async () => { + // key derived from zero private address + expect(await testLibraries.RSKAddrValidator_checkPKNotZero(PUB_KEY_FROM_ZERO)).to.be + .false; + }); + + it("checkPKNotZero(ZERO_ADDRESS) == true", async () => { + expect(await testLibraries.RSKAddrValidator_checkPKNotZero(constants.ZERO_ADDRESS)).to + .be.false; + }); + + it("checkPKNotZero(valid_address) == true", async () => { + expect(await testLibraries.RSKAddrValidator_checkPKNotZero(account1)).to.be.true; + }); + + it("safeEquals(account1, account1) == true", async () => { + expect(await testLibraries.RSKAddrValidator_safeEquals(account1, account1)).to.be.true; + }); + + it("safeEquals(PUB_KEY_FROM_ZERO, PUB_KEY_FROM_ZERO) == false", async () => { + expect( + await testLibraries.RSKAddrValidator_safeEquals( + PUB_KEY_FROM_ZERO, + PUB_KEY_FROM_ZERO + ) + ).to.be.false; + }); + + it("safeEquals(ZERO_ADDRESS, ZERO_ADDRESS) == false", async () => { + expect( + await testLibraries.RSKAddrValidator_safeEquals( + constants.ZERO_ADDRESS, + constants.ZERO_ADDRESS + ) + ).to.be.false; + }); + + it("safeEquals(account1, ZERO_ADDRESS) == false", async () => { + expect( + await testLibraries.RSKAddrValidator_safeEquals(constants.ZERO_ADDRESS, account1) + ).to.be.false; + expect( + await testLibraries.RSKAddrValidator_safeEquals(account1, constants.ZERO_ADDRESS) + ).to.be.false; + }); + + it("safeEquals(account1, account2 || account2, account1) == false", async () => { + expect(await testLibraries.RSKAddrValidator_safeEquals(account2, account1)).to.be + .false; + expect(await testLibraries.RSKAddrValidator_safeEquals(account1, account2)).to.be + .false; + }); + }); }); diff --git a/tests/loan-settings/LoanSettings.js b/tests/loan-settings/LoanSettings.js index d2cea8ce4..1c0ee7697 100644 --- a/tests/loan-settings/LoanSettings.js +++ b/tests/loan-settings/LoanSettings.js @@ -20,123 +20,127 @@ const { loadFixture } = waffle; const { expectRevert, constants, ether } = require("@openzeppelin/test-helpers"); const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getLoanTokenLogic, - getLoanToken, - getLoanTokenLogicWrbtc, - getLoanTokenWRBTC, - loan_pool_setup, - set_demand_curve, - getPriceFeeds, - getSovryn, - decodeLogs, - getSOV, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getLoanTokenLogic, + getLoanToken, + getLoanTokenLogicWrbtc, + getLoanTokenWRBTC, + loan_pool_setup, + set_demand_curve, + getPriceFeeds, + getSovryn, + decodeLogs, + getSOV, } = require("../Utils/initializer.js"); contract("LoanSettings", (accounts) => { - let lender; - let SUSD; // underlying token - let WRBTC; // collateral token - let sovryn, loanToken; - let loanParams; - - async function deploymentAndInitFixture(_wallets, _provider) { - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - - /// @dev Not included in initializer.js - await sovryn.setFeesController(lender); - - loanTokenLogicWrbtc = await getLoanTokenLogicWrbtc(); - - loanToken = await getLoanToken(lender, sovryn, WRBTC, SUSD); - - const loanTokenAddress = await loanToken.loanTokenAddress(); - if (lender == (await sovryn.owner())) await sovryn.setLoanPool([loanToken.address], [loanTokenAddress]); - - await WRBTC.mint(sovryn.address, ether("500")); - - loanParams = { - id: "0x0000000000000000000000000000000000000000000000000000000000000000", - active: false, - owner: constants.ZERO_ADDRESS, - loanToken: SUSD.address, - collateralToken: WRBTC.address, - minInitialMargin: ether("50"), - maintenanceMargin: ether("15"), - maxLoanTerm: "2419200", - }; - - let tx = await sovryn.setupLoanParams([Object.values(loanParams)]); - loanParamsId = tx.logs[1].args.id; - } - - before(async () => { - [lender, ...accounts] = accounts; - }); - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - describe("test LoanSettings", async () => { - it("test setup removeLoanParams", async () => { - let loanParamsAfter = (await sovryn.getLoanParams([loanParamsId]))[0]; - - assert(loanParamsAfter["id"] != "0x0"); - assert(loanParamsAfter["active"]); - assert(loanParamsAfter["owner"] == lender); - assert(loanParamsAfter["loanToken"] == SUSD.address); - - await expectRevert(sovryn.disableLoanParams([loanParamsId], { from: accounts[0] }), "unauthorized owner"); - - await sovryn.disableLoanParams([loanParamsId], { from: lender }); - assert((await sovryn.getLoanParams([loanParamsId]))[0]["id"] != "0x0"); - }); - - it("test disableLoanParams", async () => { - await sovryn.disableLoanParams([loanParamsId], { from: lender }); - - let loanParamsAfter = (await sovryn.getLoanParams([loanParamsId]))[0]; - - assert(loanParamsAfter["id"] != "0x0"); - assert(loanParamsAfter["active"] == false); // false because we disabled Loan Param just before - assert(loanParamsAfter["owner"] == lender); - assert(loanParamsAfter["loanToken"] == SUSD.address); - assert(loanParamsAfter["collateralToken"] == WRBTC.address); - assert(loanParamsAfter["minInitialMargin"] == ether("50")); - assert(loanParamsAfter["maintenanceMargin"] == ether("15")); - assert(loanParamsAfter["maxLoanTerm"] == "2419200"); - }); - - it("test getLoanParams", async () => { - let loanParamsAfter = (await sovryn.getLoanParams([loanParamsId]))[0]; - - assert(loanParamsAfter["id"] != "0x0"); - assert(loanParamsAfter["active"]); - assert(loanParamsAfter["owner"] == lender); - assert(loanParamsAfter["loanToken"] == SUSD.address); - assert(loanParamsAfter["collateralToken"] == WRBTC.address); - assert(loanParamsAfter["minInitialMargin"] == ether("50")); - assert(loanParamsAfter["maintenanceMargin"] == ether("15")); - assert(loanParamsAfter["maxLoanTerm"] == "2419200"); - }); - - it("test getLoanParamsList", async () => { - let loanParamsList = await sovryn.getLoanParamsList(lender, 0, 1); - assert(loanParamsList[0] == loanParamsId); - }); - - it("test getTotalPrincipal", async () => { - let totalPrincipal = await sovryn.getTotalPrincipal(lender, SUSD.address); - assert(totalPrincipal == 0); - }); - }); + let lender; + let SUSD; // underlying token + let WRBTC; // collateral token + let sovryn, loanToken; + let loanParams; + + async function deploymentAndInitFixture(_wallets, _provider) { + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + + /// @dev Not included in initializer.js + await sovryn.setFeesController(lender); + + loanTokenLogicWrbtc = await getLoanTokenLogicWrbtc(); + + loanToken = await getLoanToken(lender, sovryn, WRBTC, SUSD); + + const loanTokenAddress = await loanToken.loanTokenAddress(); + if (lender == (await sovryn.owner())) + await sovryn.setLoanPool([loanToken.address], [loanTokenAddress]); + + await WRBTC.mint(sovryn.address, ether("500")); + + loanParams = { + id: "0x0000000000000000000000000000000000000000000000000000000000000000", + active: false, + owner: constants.ZERO_ADDRESS, + loanToken: SUSD.address, + collateralToken: WRBTC.address, + minInitialMargin: ether("50"), + maintenanceMargin: ether("15"), + maxLoanTerm: "2419200", + }; + + let tx = await sovryn.setupLoanParams([Object.values(loanParams)]); + loanParamsId = tx.logs[1].args.id; + } + + before(async () => { + [lender, ...accounts] = accounts; + }); + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + describe("test LoanSettings", async () => { + it("test setup removeLoanParams", async () => { + let loanParamsAfter = (await sovryn.getLoanParams([loanParamsId]))[0]; + + assert(loanParamsAfter["id"] != "0x0"); + assert(loanParamsAfter["active"]); + assert(loanParamsAfter["owner"] == lender); + assert(loanParamsAfter["loanToken"] == SUSD.address); + + await expectRevert( + sovryn.disableLoanParams([loanParamsId], { from: accounts[0] }), + "unauthorized owner" + ); + + await sovryn.disableLoanParams([loanParamsId], { from: lender }); + assert((await sovryn.getLoanParams([loanParamsId]))[0]["id"] != "0x0"); + }); + + it("test disableLoanParams", async () => { + await sovryn.disableLoanParams([loanParamsId], { from: lender }); + + let loanParamsAfter = (await sovryn.getLoanParams([loanParamsId]))[0]; + + assert(loanParamsAfter["id"] != "0x0"); + assert(loanParamsAfter["active"] == false); // false because we disabled Loan Param just before + assert(loanParamsAfter["owner"] == lender); + assert(loanParamsAfter["loanToken"] == SUSD.address); + assert(loanParamsAfter["collateralToken"] == WRBTC.address); + assert(loanParamsAfter["minInitialMargin"] == ether("50")); + assert(loanParamsAfter["maintenanceMargin"] == ether("15")); + assert(loanParamsAfter["maxLoanTerm"] == "2419200"); + }); + + it("test getLoanParams", async () => { + let loanParamsAfter = (await sovryn.getLoanParams([loanParamsId]))[0]; + + assert(loanParamsAfter["id"] != "0x0"); + assert(loanParamsAfter["active"]); + assert(loanParamsAfter["owner"] == lender); + assert(loanParamsAfter["loanToken"] == SUSD.address); + assert(loanParamsAfter["collateralToken"] == WRBTC.address); + assert(loanParamsAfter["minInitialMargin"] == ether("50")); + assert(loanParamsAfter["maintenanceMargin"] == ether("15")); + assert(loanParamsAfter["maxLoanTerm"] == "2419200"); + }); + + it("test getLoanParamsList", async () => { + let loanParamsList = await sovryn.getLoanParamsList(lender, 0, 1); + assert(loanParamsList[0] == loanParamsId); + }); + + it("test getTotalPrincipal", async () => { + let totalPrincipal = await sovryn.getTotalPrincipal(lender, SUSD.address); + assert(totalPrincipal == 0); + }); + }); }); diff --git a/tests/loan-settings/LoanSettingsEvents.js b/tests/loan-settings/LoanSettingsEvents.js index acba541ae..75d536c1a 100644 --- a/tests/loan-settings/LoanSettingsEvents.js +++ b/tests/loan-settings/LoanSettingsEvents.js @@ -27,118 +27,124 @@ const TestSovrynSwap = artifacts.require("TestSovrynSwap"); const SwapsImplLocal = artifacts.require("SwapsImplLocal"); const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getLoanTokenLogic, - getLoanToken, - getLoanTokenLogicWrbtc, - getLoanTokenWRBTC, - loan_pool_setup, - set_demand_curve, - getPriceFeeds, - getSovryn, - decodeLogs, - getSOV, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getLoanTokenLogic, + getLoanToken, + getLoanTokenLogicWrbtc, + getLoanTokenWRBTC, + loan_pool_setup, + set_demand_curve, + getPriceFeeds, + getSovryn, + decodeLogs, + getSOV, } = require("../Utils/initializer.js"); contract("LoanSettingsEvents", (accounts) => { - let lender; - let SUSD, WRBTC; - let sovryn, loanToken; - let loanParams, loanParamsId, tx; - - async function deploymentAndInitFixture(_wallets, _provider) { - // Deploying sovrynProtocol w/ generic function from initializer.js - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - await sovryn.setSovrynProtocolAddress(sovryn.address); - - feeds = await PriceFeedsLocal.new(WRBTC.address, sovryn.address); - await feeds.setRates(SUSD.address, WRBTC.address, ether("0.01")); - const swaps = await SwapsImplLocal.new(); - const sovrynSwapSimulator = await TestSovrynSwap.new(feeds.address); - await sovryn.setSovrynSwapContractRegistryAddress(sovrynSwapSimulator.address); - await sovryn.setSupportedTokens([SUSD.address, WRBTC.address], [true, true]); - await sovryn.setPriceFeedContract( - feeds.address // priceFeeds - ); - await sovryn.setSwapsImplContract( - swaps.address // swapsImpl - ); - await sovryn.setFeesController(lender); - - loanTokenLogicWrbtc = await LoanTokenLogicWrbtc.new(); - loanToken = await LoanToken.new(lender, loanTokenLogicWrbtc.address, sovryn.address, WRBTC.address); - await loanToken.initialize(WRBTC.address, "iWRBTC", "iWRBTC"); // iToken - loanToken = await LoanTokenLogicWrbtc.at(loanToken.address); - - const loanTokenAddress = await loanToken.loanTokenAddress(); - if (lender == (await sovryn.owner())) await sovryn.setLoanPool([loanToken.address], [loanTokenAddress]); - - await WRBTC.mint(sovryn.address, ether("500")); - - loanParams = { - id: "0x0000000000000000000000000000000000000000000000000000000000000000", - active: false, - owner: constants.ZERO_ADDRESS, - loanToken: SUSD.address, - collateralToken: WRBTC.address, - minInitialMargin: ether("50"), - maintenanceMargin: ether("15"), - maxLoanTerm: "2419200", - }; - } - - before(async () => { - [lender, ...accounts] = accounts; - }); - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - describe("test LoanSettingsEvents", async () => { - it("test setupLoanParamsEvents", async () => { - tx = await sovryn.setupLoanParams([Object.values(loanParams)]); - - await expectEvent(tx, "LoanParamsIdSetup", { owner: lender }); - assert(tx.logs[1]["id"] != "0x0"); - - await expectEvent(tx, "LoanParamsSetup", { - owner: lender, - loanToken: SUSD.address, - collateralToken: WRBTC.address, - minInitialMargin: ether("50"), - maintenanceMargin: ether("15"), - maxLoanTerm: "2419200", - }); - assert(tx.logs[0]["id"] != "0x0"); - }); - - it("test disableLoanParamsEvents", async () => { - tx = await sovryn.setupLoanParams([Object.values(loanParams)]); - loanParamsId = tx.logs[1].args.id; - - tx = await sovryn.disableLoanParams([loanParamsId], { from: lender }); - - await expectEvent(tx, "LoanParamsIdDisabled", { owner: lender }); - assert(tx.logs[1]["id"] != "0x0"); - - await expectEvent(tx, "LoanParamsDisabled", { - owner: lender, - loanToken: SUSD.address, - collateralToken: WRBTC.address, - minInitialMargin: ether("50"), - maintenanceMargin: ether("15"), - maxLoanTerm: "2419200", - }); - assert(tx.logs[0]["id"] != "0x0"); - }); - }); + let lender; + let SUSD, WRBTC; + let sovryn, loanToken; + let loanParams, loanParamsId, tx; + + async function deploymentAndInitFixture(_wallets, _provider) { + // Deploying sovrynProtocol w/ generic function from initializer.js + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + await sovryn.setSovrynProtocolAddress(sovryn.address); + + feeds = await PriceFeedsLocal.new(WRBTC.address, sovryn.address); + await feeds.setRates(SUSD.address, WRBTC.address, ether("0.01")); + const swaps = await SwapsImplLocal.new(); + const sovrynSwapSimulator = await TestSovrynSwap.new(feeds.address); + await sovryn.setSovrynSwapContractRegistryAddress(sovrynSwapSimulator.address); + await sovryn.setSupportedTokens([SUSD.address, WRBTC.address], [true, true]); + await sovryn.setPriceFeedContract( + feeds.address // priceFeeds + ); + await sovryn.setSwapsImplContract( + swaps.address // swapsImpl + ); + await sovryn.setFeesController(lender); + + loanTokenLogicWrbtc = await LoanTokenLogicWrbtc.new(); + loanToken = await LoanToken.new( + lender, + loanTokenLogicWrbtc.address, + sovryn.address, + WRBTC.address + ); + await loanToken.initialize(WRBTC.address, "iWRBTC", "iWRBTC"); // iToken + loanToken = await LoanTokenLogicWrbtc.at(loanToken.address); + + const loanTokenAddress = await loanToken.loanTokenAddress(); + if (lender == (await sovryn.owner())) + await sovryn.setLoanPool([loanToken.address], [loanTokenAddress]); + + await WRBTC.mint(sovryn.address, ether("500")); + + loanParams = { + id: "0x0000000000000000000000000000000000000000000000000000000000000000", + active: false, + owner: constants.ZERO_ADDRESS, + loanToken: SUSD.address, + collateralToken: WRBTC.address, + minInitialMargin: ether("50"), + maintenanceMargin: ether("15"), + maxLoanTerm: "2419200", + }; + } + + before(async () => { + [lender, ...accounts] = accounts; + }); + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + describe("test LoanSettingsEvents", async () => { + it("test setupLoanParamsEvents", async () => { + tx = await sovryn.setupLoanParams([Object.values(loanParams)]); + + await expectEvent(tx, "LoanParamsIdSetup", { owner: lender }); + assert(tx.logs[1]["id"] != "0x0"); + + await expectEvent(tx, "LoanParamsSetup", { + owner: lender, + loanToken: SUSD.address, + collateralToken: WRBTC.address, + minInitialMargin: ether("50"), + maintenanceMargin: ether("15"), + maxLoanTerm: "2419200", + }); + assert(tx.logs[0]["id"] != "0x0"); + }); + + it("test disableLoanParamsEvents", async () => { + tx = await sovryn.setupLoanParams([Object.values(loanParams)]); + loanParamsId = tx.logs[1].args.id; + + tx = await sovryn.disableLoanParams([loanParamsId], { from: lender }); + + await expectEvent(tx, "LoanParamsIdDisabled", { owner: lender }); + assert(tx.logs[1]["id"] != "0x0"); + + await expectEvent(tx, "LoanParamsDisabled", { + owner: lender, + loanToken: SUSD.address, + collateralToken: WRBTC.address, + minInitialMargin: ether("50"), + maintenanceMargin: ether("15"), + maxLoanTerm: "2419200", + }); + assert(tx.logs[0]["id"] != "0x0"); + }); + }); }); diff --git a/tests/loan-settings/LoanSettingsNegative.js b/tests/loan-settings/LoanSettingsNegative.js index 31fa01841..8dfebb5cf 100644 --- a/tests/loan-settings/LoanSettingsNegative.js +++ b/tests/loan-settings/LoanSettingsNegative.js @@ -27,113 +27,137 @@ const TestSovrynSwap = artifacts.require("TestSovrynSwap"); const SwapsImplLocal = artifacts.require("SwapsImplLocal"); const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getLoanTokenLogic, - getLoanToken, - getLoanTokenLogicWrbtc, - getLoanTokenWRBTC, - loan_pool_setup, - set_demand_curve, - getPriceFeeds, - getSovryn, - decodeLogs, - getSOV, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getLoanTokenLogic, + getLoanToken, + getLoanTokenLogicWrbtc, + getLoanTokenWRBTC, + loan_pool_setup, + set_demand_curve, + getPriceFeeds, + getSovryn, + decodeLogs, + getSOV, } = require("../Utils/initializer.js"); contract("LoanSettingsNegative", (accounts) => { - let lender, account1; - let SUSD, WRBTC; - let sovryn, loanToken; - let loanParams, loanParamsId, tx; - - before(async () => { - [lender, account1, ...accounts] = accounts; - - // Deploying sovrynProtocol w/ generic function from initializer.js - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - await sovryn.setSovrynProtocolAddress(sovryn.address); - - feeds = await PriceFeedsLocal.new(WRBTC.address, sovryn.address); - await feeds.setRates(SUSD.address, WRBTC.address, ether("0.01")); - const swaps = await SwapsImplLocal.new(); - const sovrynSwapSimulator = await TestSovrynSwap.new(feeds.address); - await sovryn.setSovrynSwapContractRegistryAddress(sovrynSwapSimulator.address); - await sovryn.setSupportedTokens([SUSD.address, WRBTC.address], [true, true]); - await sovryn.setPriceFeedContract( - feeds.address // priceFeeds - ); - await sovryn.setSwapsImplContract( - swaps.address // swapsImpl - ); - await sovryn.setFeesController(lender); - - loanTokenLogicWrbtc = await LoanTokenLogicWrbtc.new(); - loanToken = await LoanToken.new(lender, loanTokenLogicWrbtc.address, sovryn.address, WRBTC.address); - await loanToken.initialize(WRBTC.address, "iWRBTC", "iWRBTC"); // iToken - loanToken = await LoanTokenLogicWrbtc.at(loanToken.address); - - const loanTokenAddress = await loanToken.loanTokenAddress(); - if (lender == (await sovryn.owner())) await sovryn.setLoanPool([loanToken.address], [loanTokenAddress]); - - await WRBTC.mint(sovryn.address, ether("500")); - - loanParams = { - id: "0x0000000000000000000000000000000000000000000000000000000000000000", - active: false, - owner: constants.ZERO_ADDRESS, - loanToken: SUSD.address, - collateralToken: WRBTC.address, - minInitialMargin: ether("50"), - maintenanceMargin: ether("15"), - maxLoanTerm: "2419200", - }; - - tx = await sovryn.setupLoanParams([Object.values(loanParams)]); - loanParamsId = tx.logs[1].args.id; - }); - - describe("test LoanSettingsNegative", async () => { - it("test disable unauthorized owner LoanSettings", async () => { - await expectRevert(sovryn.disableLoanParams([loanParamsId], { from: account1 }), "unauthorized owner"); - }); - - it("test LoanSettings loanParam exists", async () => { - await expectRevert(sovryn.setupLoanParams([Object.values(loanParams), Object.values(loanParams)]), "loanParams exists"); - }); - - it("test LoanSettings other requires", async () => { - let localLoanParams; - - localLoanParams = JSON.parse(JSON.stringify(loanParams)); - localLoanParams["minInitialMargin"] = ether("50"); - localLoanParams["maintenanceMargin"] = ether("15"); - localLoanParams["loanToken"] = constants.ZERO_ADDRESS; - await expectRevert(sovryn.setupLoanParams([Object.values(localLoanParams)]), "invalid params"); - - localLoanParams = JSON.parse(JSON.stringify(loanParams)); - localLoanParams["minInitialMargin"] = ether("50"); - localLoanParams["maintenanceMargin"] = ether("15"); - localLoanParams["collateralToken"] = constants.ZERO_ADDRESS; - await expectRevert(sovryn.setupLoanParams([Object.values(localLoanParams)]), "invalid params"); - - localLoanParams = JSON.parse(JSON.stringify(loanParams)); - localLoanParams["maintenanceMargin"] = ether("15"); - localLoanParams["minInitialMargin"] = ether("10"); - await expectRevert(sovryn.setupLoanParams([Object.values(localLoanParams)]), "invalid params"); - - localLoanParams = JSON.parse(JSON.stringify(loanParams)); - localLoanParams["minInitialMargin"] = ether("50"); - localLoanParams["maintenanceMargin"] = ether("15"); - localLoanParams["maxLoanTerm"] = 1; - await expectRevert(sovryn.setupLoanParams([Object.values(localLoanParams)]), "invalid params"); - }); - }); + let lender, account1; + let SUSD, WRBTC; + let sovryn, loanToken; + let loanParams, loanParamsId, tx; + + before(async () => { + [lender, account1, ...accounts] = accounts; + + // Deploying sovrynProtocol w/ generic function from initializer.js + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + await sovryn.setSovrynProtocolAddress(sovryn.address); + + feeds = await PriceFeedsLocal.new(WRBTC.address, sovryn.address); + await feeds.setRates(SUSD.address, WRBTC.address, ether("0.01")); + const swaps = await SwapsImplLocal.new(); + const sovrynSwapSimulator = await TestSovrynSwap.new(feeds.address); + await sovryn.setSovrynSwapContractRegistryAddress(sovrynSwapSimulator.address); + await sovryn.setSupportedTokens([SUSD.address, WRBTC.address], [true, true]); + await sovryn.setPriceFeedContract( + feeds.address // priceFeeds + ); + await sovryn.setSwapsImplContract( + swaps.address // swapsImpl + ); + await sovryn.setFeesController(lender); + + loanTokenLogicWrbtc = await LoanTokenLogicWrbtc.new(); + loanToken = await LoanToken.new( + lender, + loanTokenLogicWrbtc.address, + sovryn.address, + WRBTC.address + ); + await loanToken.initialize(WRBTC.address, "iWRBTC", "iWRBTC"); // iToken + loanToken = await LoanTokenLogicWrbtc.at(loanToken.address); + + const loanTokenAddress = await loanToken.loanTokenAddress(); + if (lender == (await sovryn.owner())) + await sovryn.setLoanPool([loanToken.address], [loanTokenAddress]); + + await WRBTC.mint(sovryn.address, ether("500")); + + loanParams = { + id: "0x0000000000000000000000000000000000000000000000000000000000000000", + active: false, + owner: constants.ZERO_ADDRESS, + loanToken: SUSD.address, + collateralToken: WRBTC.address, + minInitialMargin: ether("50"), + maintenanceMargin: ether("15"), + maxLoanTerm: "2419200", + }; + + tx = await sovryn.setupLoanParams([Object.values(loanParams)]); + loanParamsId = tx.logs[1].args.id; + }); + + describe("test LoanSettingsNegative", async () => { + it("test disable unauthorized owner LoanSettings", async () => { + await expectRevert( + sovryn.disableLoanParams([loanParamsId], { from: account1 }), + "unauthorized owner" + ); + }); + + it("test LoanSettings loanParam exists", async () => { + await expectRevert( + sovryn.setupLoanParams([Object.values(loanParams), Object.values(loanParams)]), + "loanParams exists" + ); + }); + + it("test LoanSettings other requires", async () => { + let localLoanParams; + + localLoanParams = JSON.parse(JSON.stringify(loanParams)); + localLoanParams["minInitialMargin"] = ether("50"); + localLoanParams["maintenanceMargin"] = ether("15"); + localLoanParams["loanToken"] = constants.ZERO_ADDRESS; + await expectRevert( + sovryn.setupLoanParams([Object.values(localLoanParams)]), + "invalid params" + ); + + localLoanParams = JSON.parse(JSON.stringify(loanParams)); + localLoanParams["minInitialMargin"] = ether("50"); + localLoanParams["maintenanceMargin"] = ether("15"); + localLoanParams["collateralToken"] = constants.ZERO_ADDRESS; + await expectRevert( + sovryn.setupLoanParams([Object.values(localLoanParams)]), + "invalid params" + ); + + localLoanParams = JSON.parse(JSON.stringify(loanParams)); + localLoanParams["maintenanceMargin"] = ether("15"); + localLoanParams["minInitialMargin"] = ether("10"); + await expectRevert( + sovryn.setupLoanParams([Object.values(localLoanParams)]), + "invalid params" + ); + + localLoanParams = JSON.parse(JSON.stringify(loanParams)); + localLoanParams["minInitialMargin"] = ether("50"); + localLoanParams["maintenanceMargin"] = ether("15"); + localLoanParams["maxLoanTerm"] = 1; + await expectRevert( + sovryn.setupLoanParams([Object.values(localLoanParams)]), + "invalid params" + ); + }); + }); }); diff --git a/tests/loan-token/Administration.test.js b/tests/loan-token/Administration.test.js index 7cfc10f9b..73d224c16 100644 --- a/tests/loan-token/Administration.test.js +++ b/tests/loan-token/Administration.test.js @@ -22,19 +22,19 @@ const ILoanTokenModules = artifacts.require("ILoanTokenModules"); const ILoanTokenLogicProxy = artifacts.require("ILoanTokenLogicProxy"); const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getLoanTokenLogic, - getLoanToken, - getLoanTokenWRBTC, - loan_pool_setup, - lend_to_pool, - getPriceFeeds, - getSovryn, - getSOV, - open_margin_trade_position, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getLoanTokenLogic, + getLoanToken, + getLoanTokenWRBTC, + loan_pool_setup, + lend_to_pool, + getPriceFeeds, + getSovryn, + getSOV, + open_margin_trade_position, } = require("../Utils/initializer.js"); const wei = web3.utils.toWei; @@ -46,233 +46,286 @@ const hunEth = new BN(wei("100", "ether")); // the same form truffle-contract uses on its receipts contract("LoanTokenAdministration", (accounts) => { - let owner; - let sovryn, SUSD, WRBTC, RBTC, BZRX, loanToken, loanTokenWRBTC; - - async function deploymentAndInitFixture(_wallets, _provider) { - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - const priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - sov = await getSOV(sovryn, priceFeeds, SUSD, accounts); - - loanToken = await getLoanToken(owner, sovryn, WRBTC, SUSD); - loanTokenWRBTC = await getLoanTokenWRBTC(owner, sovryn, WRBTC, SUSD); - await loan_pool_setup(sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC); - } - - before(async () => { - [owner] = accounts; - }); - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - describe("Test administration", () => { - it("Test demand curve setting", async () => { - const baseRate = wei("1", "ether"); - const rateMultiplier = wei("20.25", "ether"); - const targetLevel = wei("80", "ether"); - const kinkLevel = wei("90", "ether"); - const maxScaleRate = wei("100", "ether"); - - await loanToken.setDemandCurve(baseRate, rateMultiplier, baseRate, rateMultiplier, targetLevel, kinkLevel, maxScaleRate); - - /** Change to LoanToken ABI */ - loanToken = await LoanToken.at(loanToken.address); - - expect((await loanToken.baseRate()).toString() == baseRate); - expect((await loanToken.rateMultiplier()).toString() == rateMultiplier); - expect((await loanToken.lowUtilBaseRate()).toString() == baseRate); - expect((await loanToken.lowUtilRateMultiplier()).toString() == rateMultiplier); - - /** Change back to LoanTokenModules Interface */ - loanToken = await ILoanTokenModules.at(loanToken.address); - - const borrowInterestRate = await loanToken.borrowInterestRate(); - expect(borrowInterestRate.gt(oneEth)).to.be.true; - }); - - it("Test demand curve setting should fail if rateMultiplier plus baseRate is grater than 100%", async () => { - const incorrect_baseRate = wei("51", "ether"); - const incorrect_rateMultiplier = wei("50", "ether"); - const baseRate = wei("1", "ether"); - const rateMultiplier = wei("20.25", "ether"); - const targetLevel = wei("80", "ether"); - const kinkLevel = wei("90", "ether"); - const maxScaleRate = wei("100", "ether"); - - await expectRevert.unspecified( - loanToken.setDemandCurve( - incorrect_baseRate, - incorrect_rateMultiplier, - baseRate, - rateMultiplier, - targetLevel, - kinkLevel, - maxScaleRate - ) - ); - await expectRevert.unspecified( - loanToken.setDemandCurve( - baseRate, - rateMultiplier, - incorrect_baseRate, - incorrect_rateMultiplier, - targetLevel, - kinkLevel, - maxScaleRate - ) - ); - }); - - it("Test lending fee setting", async () => { - await sovryn.setLendingFeePercent(hunEth); - expect((await sovryn.lendingFeePercent()).eq(hunEth)).to.be.true; - }); - - /* + let owner; + let sovryn, SUSD, WRBTC, RBTC, BZRX, loanToken, loanTokenWRBTC; + + async function deploymentAndInitFixture(_wallets, _provider) { + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + const priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + sov = await getSOV(sovryn, priceFeeds, SUSD, accounts); + + loanToken = await getLoanToken(owner, sovryn, WRBTC, SUSD); + loanTokenWRBTC = await getLoanTokenWRBTC(owner, sovryn, WRBTC, SUSD); + await loan_pool_setup(sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC); + } + + before(async () => { + [owner] = accounts; + }); + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + describe("Test administration", () => { + it("Test demand curve setting", async () => { + const baseRate = wei("1", "ether"); + const rateMultiplier = wei("20.25", "ether"); + const targetLevel = wei("80", "ether"); + const kinkLevel = wei("90", "ether"); + const maxScaleRate = wei("100", "ether"); + + await loanToken.setDemandCurve( + baseRate, + rateMultiplier, + baseRate, + rateMultiplier, + targetLevel, + kinkLevel, + maxScaleRate + ); + + /** Change to LoanToken ABI */ + loanToken = await LoanToken.at(loanToken.address); + + expect((await loanToken.baseRate()).toString() == baseRate); + expect((await loanToken.rateMultiplier()).toString() == rateMultiplier); + expect((await loanToken.lowUtilBaseRate()).toString() == baseRate); + expect((await loanToken.lowUtilRateMultiplier()).toString() == rateMultiplier); + + /** Change back to LoanTokenModules Interface */ + loanToken = await ILoanTokenModules.at(loanToken.address); + + const borrowInterestRate = await loanToken.borrowInterestRate(); + expect(borrowInterestRate.gt(oneEth)).to.be.true; + }); + + it("Test demand curve setting should fail if rateMultiplier plus baseRate is grater than 100%", async () => { + const incorrect_baseRate = wei("51", "ether"); + const incorrect_rateMultiplier = wei("50", "ether"); + const baseRate = wei("1", "ether"); + const rateMultiplier = wei("20.25", "ether"); + const targetLevel = wei("80", "ether"); + const kinkLevel = wei("90", "ether"); + const maxScaleRate = wei("100", "ether"); + + await expectRevert.unspecified( + loanToken.setDemandCurve( + incorrect_baseRate, + incorrect_rateMultiplier, + baseRate, + rateMultiplier, + targetLevel, + kinkLevel, + maxScaleRate + ) + ); + await expectRevert.unspecified( + loanToken.setDemandCurve( + baseRate, + rateMultiplier, + incorrect_baseRate, + incorrect_rateMultiplier, + targetLevel, + kinkLevel, + maxScaleRate + ) + ); + }); + + it("Test lending fee setting", async () => { + await sovryn.setLendingFeePercent(hunEth); + expect((await sovryn.lendingFeePercent()).eq(hunEth)).to.be.true; + }); + + /* 1. pause a function 2. try to call the function - should fail 3. reactivate it 4. try to call the function - should succeed */ - it("Test toggle function pause", async () => { - await lend_to_pool(loanToken, SUSD, owner); - const functionSignature = "marginTrade(bytes32,uint256,uint256,uint256,address,address,uint256,bytes)"; - - // pause the given function and make sure the function can't be called anymore - let localLoanToken = loanToken; - let pauser = accounts[2]; - await localLoanToken.setPauser(pauser); - expect(await localLoanToken.pauser()).to.equal(pauser); - let tx = await localLoanToken.toggleFunctionPause(functionSignature, true, { from: pauser }); - expectEvent(tx, "ToggledFunctionPaused", { - functionId: functionSignature, - prevFlag: false, - newFlag: true, - }); - await expectRevert( - localLoanToken.toggleFunctionPause(functionSignature, true, { from: pauser }), - "isPaused is already set to that value" - ); - - await expectRevert(open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, accounts[1]), "unauthorized"); - - // check if checkPause returns true - assert(localLoanToken.checkPause(functionSignature)); - - pauser = accounts[3]; - await localLoanToken.setPauser(pauser); - expect(await localLoanToken.pauser()).to.equal(pauser); - tx = await localLoanToken.toggleFunctionPause(functionSignature, false, { from: pauser }); - expectEvent(tx, "ToggledFunctionPaused", { - functionId: functionSignature, - prevFlag: true, - newFlag: false, - }); - await expectRevert( - localLoanToken.toggleFunctionPause(functionSignature, false, { from: pauser }), - "isPaused is already set to that value" - ); - await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, accounts[1]); - - // check if checkPause returns false - expect(await localLoanToken.checkPause(functionSignature)).to.be.false; - }); - - // call toggleFunction with a non-admin address and make sure it fails - it("Test toggle function pause with non admin should fail", async () => { - let localLoanToken = loanToken; - await expectRevert(localLoanToken.toggleFunctionPause("mint(address,uint256)", true, { from: accounts[1] }), "onlyPauser"); - }); - - it("Should succeed with larger rate than maxSlippage in positive direction", async () => { - const priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - let rate = await priceFeeds.checkPriceDisagreement(WRBTC.address, SUSD.address, wei("1", "ether"), wei("20000", "ether"), 0); - assert(rate == wei("20000", "ether")); - }); - - it("Should fail with larger rate than maxSlippage in negative direction", async () => { - const priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - await expectRevert( - priceFeeds.checkPriceDisagreement(WRBTC.address, SUSD.address, wei("1", "ether"), wei("1", "ether"), 0), - "price disagreement" - ); - }); - - it("Initialize loan token proxy from non-admin should fail", async () => { - const initLoanTokenLogic = await getLoanTokenLogic(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] - loanTokenLogicLM = initLoanTokenLogic[0]; - loanTokenLogicBeaconLM = initLoanTokenLogic[1]; - - loanToken = await LoanToken.new(owner, loanTokenLogicLM.address, sovryn.address, WRBTC.address); - await loanToken.initialize(SUSD.address, "SUSD", "SUSD"); //iToken - - /** Initialize the loan token logic proxy */ - loanToken = await ILoanTokenLogicProxy.at(loanToken.address); - await expectRevert( - loanToken.setBeaconAddress(loanTokenLogicBeaconLM.address, { from: accounts[1] }), - "LoanTokenLogicProxy:unauthorized" - ); - }); - - it("Should revert if target not active in loan token proxy", async () => { - /** Deploy LoanTokenLogicBeacon */ - const loanTokenLogicBeaconLM = await LoanTokenLogicBeacon.new(); - - /** Deploy LoanTokenLogicProxy */ - loanTokenLogicLM = await LoanTokenLogicProxy.new(loanTokenLogicBeacon.address); - - loanToken = await LoanToken.new(owner, loanTokenLogicLM.address, sovryn.address, WRBTC.address); - await loanToken.initialize(SUSD.address, "SUSD", "SUSD"); //iToken - - /** Initialize the loan token logic proxy */ - loanToken = await ILoanTokenLogicProxy.at(loanToken.address); - await loanToken.setBeaconAddress(loanTokenLogicBeaconLM.address); - - /** Use interface of LoanTokenModules */ - loanToken = await ILoanTokenModules.at(loanToken.address); - - await expectRevert(loanToken.assetBalanceOf(owner), "LoanTokenLogicProxy:target not active"); - }); - - it("Test set beacon address in loan token logic proxy", async () => { - const initLoanTokenLogic = await getLoanTokenLogic(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] - loanTokenLogicLM = initLoanTokenLogic[0]; - loanTokenLogicBeaconLM = initLoanTokenLogic[1]; - - /** Deploy New LoanTokenLogicBeacon */ - const newLoanTokenLogicBeaconLM = await LoanTokenLogicBeacon.new(); - - await loanTokenLogicLM.setBeaconAddress(newLoanTokenLogicBeaconLM.address); - expect(await loanTokenLogicLM.beaconAddress()).to.equal(newLoanTokenLogicBeaconLM.address); - }); - - it("Set beacon address from non-owner should fail", async () => { - const initLoanTokenLogic = await getLoanTokenLogic(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] - loanTokenLogicLM = initLoanTokenLogic[0]; - loanTokenLogicBeaconLM = initLoanTokenLogic[1]; - - /** Deploy New LoanTokenLogicBeacon */ - const newLoanTokenLogicBeaconLM = await LoanTokenLogicBeacon.new(); - - await expectRevert( - loanTokenLogicLM.setBeaconAddress(newLoanTokenLogicBeaconLM.address, { from: accounts[1] }), - "LoanTokenLogicProxy:unauthorized" - ); - }); - - it("Set beacon address to non contract address should fail", async () => { - const initLoanTokenLogic = await getLoanTokenLogic(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] - loanTokenLogicLM = initLoanTokenLogic[0]; - loanTokenLogicBeaconLM = initLoanTokenLogic[1]; - - await expectRevert(loanTokenLogicLM.setBeaconAddress(accounts[1]), "Cannot set beacon address to a non-contract address"); - }); - }); + it("Test toggle function pause", async () => { + await lend_to_pool(loanToken, SUSD, owner); + const functionSignature = + "marginTrade(bytes32,uint256,uint256,uint256,address,address,uint256,bytes)"; + + // pause the given function and make sure the function can't be called anymore + let localLoanToken = loanToken; + let pauser = accounts[2]; + await localLoanToken.setPauser(pauser); + expect(await localLoanToken.pauser()).to.equal(pauser); + let tx = await localLoanToken.toggleFunctionPause(functionSignature, true, { + from: pauser, + }); + expectEvent(tx, "ToggledFunctionPaused", { + functionId: functionSignature, + prevFlag: false, + newFlag: true, + }); + await expectRevert( + localLoanToken.toggleFunctionPause(functionSignature, true, { from: pauser }), + "isPaused is already set to that value" + ); + + await expectRevert( + open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, accounts[1]), + "unauthorized" + ); + + // check if checkPause returns true + assert(localLoanToken.checkPause(functionSignature)); + + pauser = accounts[3]; + await localLoanToken.setPauser(pauser); + expect(await localLoanToken.pauser()).to.equal(pauser); + tx = await localLoanToken.toggleFunctionPause(functionSignature, false, { + from: pauser, + }); + expectEvent(tx, "ToggledFunctionPaused", { + functionId: functionSignature, + prevFlag: true, + newFlag: false, + }); + await expectRevert( + localLoanToken.toggleFunctionPause(functionSignature, false, { from: pauser }), + "isPaused is already set to that value" + ); + await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, accounts[1]); + + // check if checkPause returns false + expect(await localLoanToken.checkPause(functionSignature)).to.be.false; + }); + + // call toggleFunction with a non-admin address and make sure it fails + it("Test toggle function pause with non admin should fail", async () => { + let localLoanToken = loanToken; + await expectRevert( + localLoanToken.toggleFunctionPause("mint(address,uint256)", true, { + from: accounts[1], + }), + "onlyPauser" + ); + }); + + it("Should succeed with larger rate than maxSlippage in positive direction", async () => { + const priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + let rate = await priceFeeds.checkPriceDisagreement( + WRBTC.address, + SUSD.address, + wei("1", "ether"), + wei("20000", "ether"), + 0 + ); + assert(rate == wei("20000", "ether")); + }); + + it("Should fail with larger rate than maxSlippage in negative direction", async () => { + const priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + await expectRevert( + priceFeeds.checkPriceDisagreement( + WRBTC.address, + SUSD.address, + wei("1", "ether"), + wei("1", "ether"), + 0 + ), + "price disagreement" + ); + }); + + it("Initialize loan token proxy from non-admin should fail", async () => { + const initLoanTokenLogic = await getLoanTokenLogic(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] + loanTokenLogicLM = initLoanTokenLogic[0]; + loanTokenLogicBeaconLM = initLoanTokenLogic[1]; + + loanToken = await LoanToken.new( + owner, + loanTokenLogicLM.address, + sovryn.address, + WRBTC.address + ); + await loanToken.initialize(SUSD.address, "SUSD", "SUSD"); //iToken + + /** Initialize the loan token logic proxy */ + loanToken = await ILoanTokenLogicProxy.at(loanToken.address); + await expectRevert( + loanToken.setBeaconAddress(loanTokenLogicBeaconLM.address, { from: accounts[1] }), + "LoanTokenLogicProxy:unauthorized" + ); + }); + + it("Should revert if target not active in loan token proxy", async () => { + /** Deploy LoanTokenLogicBeacon */ + const loanTokenLogicBeaconLM = await LoanTokenLogicBeacon.new(); + + /** Deploy LoanTokenLogicProxy */ + loanTokenLogicLM = await LoanTokenLogicProxy.new(loanTokenLogicBeacon.address); + + loanToken = await LoanToken.new( + owner, + loanTokenLogicLM.address, + sovryn.address, + WRBTC.address + ); + await loanToken.initialize(SUSD.address, "SUSD", "SUSD"); //iToken + + /** Initialize the loan token logic proxy */ + loanToken = await ILoanTokenLogicProxy.at(loanToken.address); + await loanToken.setBeaconAddress(loanTokenLogicBeaconLM.address); + + /** Use interface of LoanTokenModules */ + loanToken = await ILoanTokenModules.at(loanToken.address); + + await expectRevert( + loanToken.assetBalanceOf(owner), + "LoanTokenLogicProxy:target not active" + ); + }); + + it("Test set beacon address in loan token logic proxy", async () => { + const initLoanTokenLogic = await getLoanTokenLogic(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] + loanTokenLogicLM = initLoanTokenLogic[0]; + loanTokenLogicBeaconLM = initLoanTokenLogic[1]; + + /** Deploy New LoanTokenLogicBeacon */ + const newLoanTokenLogicBeaconLM = await LoanTokenLogicBeacon.new(); + + await loanTokenLogicLM.setBeaconAddress(newLoanTokenLogicBeaconLM.address); + expect(await loanTokenLogicLM.beaconAddress()).to.equal( + newLoanTokenLogicBeaconLM.address + ); + }); + + it("Set beacon address from non-owner should fail", async () => { + const initLoanTokenLogic = await getLoanTokenLogic(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] + loanTokenLogicLM = initLoanTokenLogic[0]; + loanTokenLogicBeaconLM = initLoanTokenLogic[1]; + + /** Deploy New LoanTokenLogicBeacon */ + const newLoanTokenLogicBeaconLM = await LoanTokenLogicBeacon.new(); + + await expectRevert( + loanTokenLogicLM.setBeaconAddress(newLoanTokenLogicBeaconLM.address, { + from: accounts[1], + }), + "LoanTokenLogicProxy:unauthorized" + ); + }); + + it("Set beacon address to non contract address should fail", async () => { + const initLoanTokenLogic = await getLoanTokenLogic(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] + loanTokenLogicLM = initLoanTokenLogic[0]; + loanTokenLogicBeaconLM = initLoanTokenLogic[1]; + + await expectRevert( + loanTokenLogicLM.setBeaconAddress(accounts[1]), + "Cannot set beacon address to a non-contract address" + ); + }); + }); }); diff --git a/tests/loan-token/BorrowingTestToken.test.js b/tests/loan-token/BorrowingTestToken.test.js index 88b1f7da9..6d40db6fc 100644 --- a/tests/loan-token/BorrowingTestToken.test.js +++ b/tests/loan-token/BorrowingTestToken.test.js @@ -19,22 +19,22 @@ const FeesEvents = artifacts.require("FeesEvents"); const LoanOpenings = artifacts.require("LoanOpenings"); const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getSOV, - getLoanTokenLogic, - getLoanToken, - getLoanTokenWRBTC, - loan_pool_setup, - set_demand_curve, - lend_to_pool, - getPriceFeeds, - getSovryn, - decodeLogs, - verify_sov_reward_payment, - CONSTANTS, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getSOV, + getLoanTokenLogic, + getLoanToken, + getLoanTokenWRBTC, + loan_pool_setup, + set_demand_curve, + lend_to_pool, + getPriceFeeds, + getSovryn, + decodeLogs, + verify_sov_reward_payment, + CONSTANTS, } = require("../Utils/initializer.js"); const { ZERO_ADDRESS } = require("@openzeppelin/test-helpers/src/constants"); @@ -48,780 +48,934 @@ const hunEth = new BN(wei("100", "ether")); // the same form truffle-contract uses on its receipts contract("LoanTokenBorrowing", (accounts) => { - let owner, account1; - let sovryn, SUSD, WRBTC, RBTC, BZRX, loanToken, loanTokenWRBTC, SOV, priceFeeds; - - async function deploymentAndInitFixture(_wallets, _provider) { - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - - loanToken = await getLoanToken(owner, sovryn, WRBTC, SUSD); - loanTokenWRBTC = await getLoanTokenWRBTC(owner, sovryn, WRBTC, SUSD); - await loan_pool_setup(sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC); - - SOV = await getSOV(sovryn, priceFeeds, SUSD, accounts); - } - - before(async () => { - [owner, account1] = accounts; - }); - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - describe("Test borrow", () => { - it("Test getRequiredCollateral w/ marginAmount = 0", async () => { - // prepare the test - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, owner); - // determine borrowing parameter - const withdrawAmount = tenEth; - - const collateralTokenSent = await sovryn.getRequiredCollateral(SUSD.address, RBTC.address, withdrawAmount, new BN(0), true); - // console.log("collateralTokenSent = ", collateralTokenSent.toString()); - expect(collateralTokenSent).to.be.a.bignumber.equal(new BN(0)); - }); - - it("Test getBorrowAmount w/ marginAmount = 0", async () => { - // prepare the test - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, owner); - // determine borrowing parameter - const withdrawAmount = tenEth; - - const borrowAmount = await sovryn.getBorrowAmount(SUSD.address, RBTC.address, withdrawAmount, new BN(0), true); - // console.log("borrowAmount = ", borrowAmount.toString()); - expect(borrowAmount).to.be.a.bignumber.equal(new BN(0)); - }); - - it("Test getBorrowAmount w/ and w/o Torque Loan", async () => { - // prepare the test - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, owner); - // determine borrowing parameter - const withdrawAmount = tenEth; - let marginAmount = new BN(10).pow(new BN(20)).mul(new BN(1)); - - // Without TorqueLoan - const borrowAmountNoTorque = await sovryn.getBorrowAmount( - SUSD.address, // loanToken - RBTC.address, // collateralToken - withdrawAmount, // collateralTokenAmount - marginAmount, - false // isTorqueLoan - ); - // console.log("borrowAmountNoTorque = ", borrowAmountNoTorque.toString()); - - // Compute expected values - const { rate: trade_rate, precision } = await priceFeeds.queryRate(RBTC.address, SUSD.address); - // console.log("trade_rate = ", trade_rate.toString()); - // console.log("precision = ", precision.toString()); - - const tradingFee = (await sovryn.tradingFeePercent()).mul(withdrawAmount).div(hunEth); - let expectedBorrowAmountNoTorque = withdrawAmount.sub(tradingFee); - expectedBorrowAmountNoTorque = expectedBorrowAmountNoTorque.mul(new BN(10).pow(new BN(20))).mul(trade_rate); - expectedBorrowAmountNoTorque = expectedBorrowAmountNoTorque.div(marginAmount).div(precision); - - // Check expected = real - expect(borrowAmountNoTorque).to.be.a.bignumber.equal(expectedBorrowAmountNoTorque); - - // With TorqueLoan - const borrowAmountTorque = await sovryn.getBorrowAmount( - SUSD.address, // loanToken - RBTC.address, // collateralToken - withdrawAmount, // collateralTokenAmount - marginAmount, - true // isTorqueLoan - ); - // console.log("borrowAmountTorque = ", borrowAmountTorque.toString()); - - // Compute expected values - marginAmount = marginAmount.add(new BN(10).pow(new BN(20))); // Torque increases the margin - const borrowingFee = (await sovryn.borrowingFeePercent()).mul(withdrawAmount).div(hunEth); - let expectedBorrowAmountTorque = withdrawAmount.sub(borrowingFee); - expectedBorrowAmountTorque = expectedBorrowAmountTorque - .mul(new BN(10).pow(new BN(20))) - .mul(trade_rate) - .div(marginAmount) - .div(precision); - - // Check expected = real - expect(borrowAmountTorque).to.be.a.bignumber.equal(expectedBorrowAmountTorque); - }); - - it("Test borrow", async () => { - // prepare the test - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, owner); - // determine borrowing parameter - const withdrawAmount = tenEth; - const durationInSeconds = 60 * 60 * 24 * 10; // 10 days - // compute the required collateral. params: address loanToken, address collateralToken, uint256 newPrincipal,uint256 marginAmount, bool isTorqueLoan - // NOTE: this is not the best method for computing the required collateral for borrowing, because it is not considering the interest payment - // better use getDepositAmountForBorrow -> need to adjust the rest of the test as well, then - const collateralTokenSent = await sovryn.getRequiredCollateral( - SUSD.address, - RBTC.address, - withdrawAmount, - new BN(10).pow(new BN(18)).mul(new BN(50)), - true - ); - - // compute expected values for asserts - const interestRate = await loanToken.nextBorrowInterestRate(withdrawAmount); - // principal = withdrawAmount/(1 - interestRate/1e20 * durationInSeconds / 31536000) - const principal = withdrawAmount - .mul(oneEth) - .div(oneEth.sub(interestRate.mul(new BN(durationInSeconds)).mul(oneEth).div(new BN(31536000)).div(hunEth))); - // TODO: refactor formula to remove rounding error subn(1) - const borrowingFee = (await sovryn.borrowingFeePercent()).mul(collateralTokenSent).div(hunEth); /*.addn(1)*/ - const expectedBalance = (await SUSD.balanceOf(account1)).add(withdrawAmount); - // approve the transfer of the collateral - await RBTC.approve(loanToken.address, collateralTokenSent); - const sov_initial_balance = await SOV.balanceOf(owner); - - // borrow some funds - const { tx, receipt } = await loanToken.borrow( - "0x0", // bytes32 loanId - withdrawAmount, // uint256 withdrawAmount - durationInSeconds, // uint256 initialLoanDuration - collateralTokenSent, // uint256 collateralTokenSent - RBTC.address, // address collateralTokenAddress - owner, // address borrower - account1, // address receiver - web3.utils.fromAscii("") // bytes memory loanDataBytes - ); - // assert the trade was processed as expected - await expectEvent.inTransaction(tx, LoanOpenings, "Borrow", { - user: owner, - lender: loanToken.address, - loanToken: SUSD.address, - collateralToken: RBTC.address, - newPrincipal: principal, - newCollateral: collateralTokenSent.sub(borrowingFee), - interestRate: interestRate, - }); - const decode = decodeLogs(receipt.rawLogs, LoanOpenings, "Borrow"); - const args = decode[0].args; - - expect(args["interestDuration"] >= durationInSeconds - 1 && args["interestDuration"] <= durationInSeconds).to.be.true; - expect(new BN(args["currentMargin"])).to.be.a.bignumber.gt(new BN(49).mul(oneEth)); - - // assert the user received the borrowed amount - expect(await SUSD.balanceOf(account1)).to.be.a.bignumber.equal(expectedBalance); - await verify_sov_reward_payment( - receipt.rawLogs, - FeesEvents, - SOV, - owner, - args["loanId"], - sov_initial_balance, - 1, - RBTC.address, - SUSD.address, - sovryn - ); - }); - - it("Test borrow with special rebates percentage", async () => { - // prepare the test - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, owner); - // For borrowing, the token fee is the collateral token - await sovryn.setSpecialRebates(RBTC.address, SUSD.address, wei("300", "ether")); - - /// @dev fast checking previously added rebates - let rebates = await sovryn.getSpecialRebates.call(RBTC.address, SUSD.address); - - expect(rebates).to.be.a.bignumber.equal(wei("300", "ether")); - - // determine borrowing parameter - const withdrawAmount = tenEth; - // compute the required collateral. params: address loanToken, address collateralToken, uint256 newPrincipal,uint256 marginAmount, bool isTorqueLoan - const collateralTokenSent = await sovryn.getRequiredCollateral( - SUSD.address, - RBTC.address, - withdrawAmount, - new BN(10).pow(new BN(18)).mul(new BN(50)), - true - ); - const durationInSeconds = 60 * 60 * 24 * 10; // 10 days - // compute expected values for asserts - const interestRate = await loanToken.nextBorrowInterestRate(withdrawAmount); - // principal = withdrawAmount/(1 - interestRate/1e20 * durationInSeconds / 31536000) - const principal = withdrawAmount - .mul(oneEth) - .div(oneEth.sub(interestRate.mul(new BN(durationInSeconds)).mul(oneEth).div(new BN(31536000)).div(hunEth))); - // TODO: refactor formula to remove rounding error subn(1) - const borrowingFee = (await sovryn.borrowingFeePercent()).mul(collateralTokenSent).div(hunEth); /*.addn(1)*/ - const expectedBalance = (await SUSD.balanceOf(account1)).add(withdrawAmount); - // approve the transfer of the collateral - await RBTC.approve(loanToken.address, collateralTokenSent); - const sov_initial_balance = await SOV.balanceOf(owner); - - // borrow some funds - const { tx, receipt } = await loanToken.borrow( - "0x0", // bytes32 loanId - withdrawAmount, // uint256 withdrawAmount - durationInSeconds, // uint256 initialLoanDuration - collateralTokenSent, // uint256 collateralTokenSent - RBTC.address, // address collateralTokenAddress - owner, // address borrower - account1, // address receiver - web3.utils.fromAscii("") // bytes memory loanDataBytes - ); - // assert the trade was processed as expected - await expectEvent.inTransaction(tx, LoanOpenings, "Borrow", { - user: owner, - lender: loanToken.address, - loanToken: SUSD.address, - collateralToken: RBTC.address, - newPrincipal: principal, - newCollateral: collateralTokenSent.sub(borrowingFee), - interestRate: interestRate, - }); - const decode = decodeLogs(receipt.rawLogs, LoanOpenings, "Borrow"); - const args = decode[0].args; - - expect(args["interestDuration"] >= durationInSeconds - 1 && args["interestDuration"] <= durationInSeconds).to.be.true; - expect(new BN(args["currentMargin"])).to.be.a.bignumber.gt(new BN(49).mul(oneEth)); - - // assert the user received the borrowed amount - expect(await SUSD.balanceOf(account1)).to.be.a.bignumber.equal(expectedBalance); - await verify_sov_reward_payment( - receipt.rawLogs, - FeesEvents, - SOV, - owner, - args["loanId"], - sov_initial_balance, - 1, - RBTC.address, - SUSD.address, - sovryn - ); - }); - - it("Test borrow 0 collateral should fail", async () => { - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, owner); - await expectRevert( - loanToken.borrow( - "0x0", // bytes32 loanId - 10, // uint256 withdrawAmount - 24 * 60 * 60, // uint256 initialLoanDuration - 0, // uint256 collateralTokenSent - RBTC.address, // address collateralTokenAddress - owner, // address borrower - account1, // address receiver - "0x0" // bytes memory loanDataBytes - ), - "7" - ); - }); - - it("Test borrow 0 withdraw should fail", async () => { - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, owner); - expectRevert( - loanToken.borrow( - "0x0", // bytes32 loanId - 0, // uint256 withdrawAmount - 24 * 60 * 60, // uint256 initialLoanDuration - 10, // uint256 collateralTokenSent - RBTC.address, // address collateralTokenAddress - owner, // address borrower - account1, // address receiver - "0x0" // bytes memory loanDataBytes - ), - "6" - ); - }); - - it("Test borrow sending value with tokens should fail", async () => { - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, owner); - expectRevert( - loanToken.borrow( - "0x0", // bytes32 loanId - 10, // uint256 withdrawAmount - 24 * 60 * 60, // uint256 initialLoanDuration - 10, // uint256 collateralTokenSent - RBTC.address, // address collateralTokenAddress - owner, // address borrower - account1, // address receiver - "0x0", // bytes memory loanDataBytes - { value: 100 } - ), - "7" - ); - }); - - it("Test borrow invalid collateral should fail", async () => { - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, owner); - await expectRevert( - loanToken.borrow( - "0x0", // bytes32 loanId - 10, // uint256 withdrawAmount - 24 * 60 * 60, // uint256 initialLoanDuration - 10, // uint256 collateralTokenSent - CONSTANTS.ZERO_ADDRESS, // address collateralTokenAddress - owner, // address borrower - account1, // address receiver - "0x0" // bytes memory loanDataBytes - ), - "7" - ); - - expectRevert( - loanToken.borrow( - "0x0", // bytes32 loanId - 10, // uint256 withdrawAmount - 24 * 60 * 60, // uint256 initialLoanDuration - 10, // uint256 collateralTokenSent - SUSD.address, // address collateralTokenAddress - owner, // address borrower - account1, // address receiver - "0x0" // bytes memory loanDataBytes - ), - "10" - ); - }); - - it("Test borrow no interest should fail", async () => { - // no demand curve settings -> no interest set - // prepare the test - await lend_to_pool(loanToken, SUSD, owner); - - // determine borrowing parameter - const withdrawAmount = oneEth.mul(new BN(10)); // I want to borrow 10 USD - // compute the required collateral. params: address loanToken, address collateralToken, uint256 newPrincipal,uint256 marginAmount, bool isTorqueLoan - const collateralTokenSent = await sovryn.getRequiredCollateral( - SUSD.address, - RBTC.address, - withdrawAmount, - new BN(50).pow(new BN(18)), - true - ); - - // approve the transfer of the collateral - await RBTC.approve(loanToken.address, collateralTokenSent); - expectRevert( - loanToken.borrow( - "0x0", // bytes32 loanId - withdrawAmount, // uint256 withdrawAmount - 24 * 60 * 60, // uint256 initialLoanDuration - collateralTokenSent, // uint256 collateralTokenSent - RBTC.address, // address collateralTokenAddress - owner, // address borrower - account1, // address receiver - "0x0" // bytes memory loanDataBytes - ), - "invalid interest" - ); - }); - - it("Test borrow insufficient collateral should fail", async () => { - // prepare the test - - await lend_to_pool(loanToken, SUSD, owner); - await set_demand_curve(loanToken); - - // determine borrowing parameter - const withdrawAmount = oneEth.mul(new BN(10)); // I want to borrow 10 USD - // compute the required collateral. params: address loanToken, address collateralToken, uint256 newPrincipal,uint256 marginAmount, bool isTorqueLoan - let collateralTokenSent = await sovryn.getRequiredCollateral( - SUSD.address, - RBTC.address, - withdrawAmount, - new BN(10).pow(new BN(18)).mul(new BN(50)), - true - ); - collateralTokenSent = collateralTokenSent.div(new BN(2)); - - // approve the transfer of the collateral - await RBTC.approve(loanToken.address, collateralTokenSent); - expectRevert( - loanToken.borrow( - "0x0", // bytes32 loanId - withdrawAmount, // uint256 withdrawAmount - 24 * 60 * 60, // uint256 initialLoanDuration - collateralTokenSent, // uint256 collateralTokenSent - RBTC.address, // address collateralTokenAddress - owner, // address borrower - account1, // address receiver - "0x0" // bytes memory loanDataBytes - ), - "collateral insufficient" - ); - }); - - // borrows some funds from account 0 and then takes out some more from account 2 with 'borrow' without paying should fail. - it("Test borrow from foreign loan should fail", async () => { - // prepare the test - - await lend_to_pool(loanToken, SUSD, owner); - await set_demand_curve(loanToken); - - // determine borrowing parameter - const withdrawAmount = oneEth.mul(new BN(10)); // I want to borrow 10 USD - // compute the required collateral. params: address loanToken, address collateralToken, uint256 newPrincipal,uint256 marginAmount, bool isTorqueLoan - let collateralTokenSent = await sovryn.getRequiredCollateral( - SUSD.address, - RBTC.address, - withdrawAmount, - new BN(10).pow(new BN(18)).mul(new BN(50)), - true - ); - collateralTokenSent = collateralTokenSent.mul(new BN(2)); - const durationInSeconds = 60 * 60 * 24 * 10; - - // approve the transfer of the collateral - await RBTC.approve(loanToken.address, collateralTokenSent); - const borrower = accounts[0]; - - const { receipt } = await loanToken.borrow( - "0x0", // bytes32 loanId - withdrawAmount, // uint256 withdrawAmount - durationInSeconds, // uint256 initialLoanDuration - collateralTokenSent, // uint256 collateralTokenSent - RBTC.address, // address collateralTokenAddress - borrower, // address borrower - account1, // address receiver - "0x0" // bytes memory loanDataBytes - ); - - const decode = decodeLogs(receipt.rawLogs, LoanOpenings, "Borrow"); - const loanId = decode[0].args["loanId"]; - - await RBTC.transfer(accounts[2], collateralTokenSent); - // approve the transfer of the collateral - await RBTC.approve(loanToken.address, collateralTokenSent, { from: accounts[2] }); - - await expectRevert( - loanToken.borrow( - loanId, // bytes32 loanId - withdrawAmount.div(new BN(2)), // uint256 withdrawAmount - durationInSeconds, // uint256 initialLoanDuration - 1, // uint256 collateralTokenSent - RBTC.address, // address collateralTokenAddress - borrower, // address borrower - accounts[2], // address receiver - "0x0", // bytes memory loanDataBytes - { from: accounts[2] } - ), - "7" - ); - }); - - // borrows some funds from account 0 and then takes out some more from account 2 with a marginTrade without paying should fail. - it("Test margin trade from foreign loan should fail", async () => { - // prepare the test - - await lend_to_pool(loanToken, SUSD, owner); - await set_demand_curve(loanToken); - - // determine borrowing parameter - const withdrawAmount = oneEth.mul(new BN(10)); // I want to borrow 10 USD - // compute the required collateral. params: address loanToken, address collateralToken, uint256 newPrincipal,uint256 marginAmount, bool isTorqueLoan - let collateralTokenSent = await sovryn.getRequiredCollateral( - SUSD.address, - RBTC.address, - withdrawAmount, - new BN(10).pow(new BN(18)).mul(new BN(50)), - true - ); - collateralTokenSent = collateralTokenSent.mul(new BN(2)); - const durationInSeconds = 60 * 60 * 24 * 10; - - // approve the transfer of the collateral - await RBTC.approve(loanToken.address, collateralTokenSent); - const borrower = accounts[0]; - - const { receipt } = await loanToken.borrow( - "0x0", // bytes32 loanId - withdrawAmount, // uint256 withdrawAmount - durationInSeconds, // uint256 initialLoanDuration - collateralTokenSent, // uint256 collateralTokenSent - RBTC.address, // address collateralTokenAddress - borrower, // address borrower - account1, // address receiver - web3.utils.fromAscii("") // bytes memory loanDataBytes - ); - - const decode = decodeLogs(receipt.rawLogs, LoanOpenings, "Borrow"); - const loanId = decode[0].args["loanId"]; - - await SUSD.transfer(accounts[2], withdrawAmount); - // approve the transfer of the collateral - await SUSD.approve(loanToken.address, withdrawAmount, { from: accounts[2] }); - - await expectRevert( - loanToken.marginTrade( - loanId, // bytes32 loanId - oneEth, // uint256 withdrawAmount - withdrawAmount, // uint256 collateralTokenSent - 0, - RBTC.address, // address collateralTokenAddress - accounts[2], // address receiver - 0, - "0x0", // bytes memory loanDataBytes - { from: accounts[2] } - ), - "borrower mismatch" - ); - }); - - // margin trades from account 0 and then borrows from same loan should fail. - it("Test borrow from trade position should fail", async () => { - // prepare the test - - await lend_to_pool(loanToken, SUSD, owner); - await set_demand_curve(loanToken); - - // determine borrowing parameter - const withdrawAmount = oneEth.mul(new BN(10)); // I want to borrow 10 USD - - await SUSD.approve(loanToken.address, withdrawAmount); - - const { receipt } = await loanToken.marginTrade( - "0x0", // bytes32 loanId - oneEth, // uint256 withdrawAmount - withdrawAmount, // uint256 collateralTokenSent - 0, - RBTC.address, // address collateralTokenAddress - accounts[0], // address receiver - 0, - "0x" // bytes memory loanDataBytes - ); - - const decode = decodeLogs(receipt.rawLogs, LoanOpenings, "Trade"); - const loanId = decode[0].args["loanId"]; - - // approve the transfer of the collateral - await RBTC.approve(loanToken.address, withdrawAmount); - - await expectRevert( - loanToken.borrow( - loanId, // bytes32 loanId - withdrawAmount.div(new BN(10)), // uint256 withdrawAmount - 60 * 60 * 24 * 10, // uint256 initialLoanDuration - 1, // uint256 collateralTokenSent - RBTC.address, // address collateralTokenAddress - accounts[0], // address borrower - accounts[0], // address receiver - "0x" // bytes memory loanDataBytes - ), - "loanParams mismatch" - ); - }); - - // 50% was hardcoded on the old contracts -> would have failed, but should work now - it("Borrowing with more than 50% initial margin", async () => { - await set_demand_curve(loanToken); - await loan_pool_setup(sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC, wei("100", "ether")); - await lend_to_pool(loanToken, SUSD, owner); - // determine borrowing parameter - const withdrawAmount = tenEth; - const durationInSeconds = 60 * 60 * 24 * 10; // 10 days - // compute the required collateral - const collateralTokenSent = await loanToken.getDepositAmountForBorrow(withdrawAmount, durationInSeconds, RBTC.address); - - // TODO: refactor formula to remove rounding error subn(1) - const borrowingFee = (await sovryn.borrowingFeePercent()).mul(collateralTokenSent).div(hunEth).addn(1); - - // compute expected values for asserts - const interestRate = await loanToken.nextBorrowInterestRate(withdrawAmount); - // principal = withdrawAmount/(1 - interestRate/1e20 * durationInSeconds / 31536000) - const principal = withdrawAmount - .mul(oneEth) - .div(oneEth.sub(interestRate.mul(new BN(durationInSeconds)).mul(oneEth).div(new BN(31536000)).div(hunEth))); - - // approve the transfer of the collateral - await RBTC.approve(loanToken.address, collateralTokenSent); - - // borrow some funds - const { tx, receipt } = await loanToken.borrow( - "0x0", // bytes32 loanId - withdrawAmount, // uint256 withdrawAmount - durationInSeconds, // uint256 initialLoanDuration - collateralTokenSent, // uint256 collateralTokenSent - RBTC.address, // address collateralTokenAddress - owner, // address borrower - account1, // address receiver - web3.utils.fromAscii("") // bytes memory loanDataBytes - ); - // assert the trade was processed as expected - await expectEvent.inTransaction(tx, LoanOpenings, "Borrow", { - user: owner, - lender: loanToken.address, - loanToken: SUSD.address, - collateralToken: RBTC.address, - newPrincipal: principal, - newCollateral: collateralTokenSent.sub(borrowingFee), - interestRate: interestRate, - }); - const decode = decodeLogs(receipt.rawLogs, LoanOpenings, "Borrow"); - const args = decode[0].args; - - expect(args["interestDuration"] >= durationInSeconds - 1 && args["interestDuration"] <= durationInSeconds).to.be.true; - expect(new BN(args["currentMargin"])).to.be.a.bignumber.gt(new BN(99).mul(oneEth)); - }); - - /// @dev For test coverage - it("getDepositAmountForBorrow should return 0 when borrowAmount is 0", async () => { - await set_demand_curve(loanToken); - await loan_pool_setup(sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC, wei("100", "ether")); - await lend_to_pool(loanToken, SUSD, owner); - // determine borrowing parameter - const borrowAmount = new BN(0); - const durationInSeconds = 60 * 60 * 24 * 10; // 10 days - const collateralTokenSent = await loanToken.getDepositAmountForBorrow(borrowAmount, durationInSeconds, RBTC.address); - - expect(collateralTokenSent).to.be.bignumber.equal(new BN(0)); - }); - - it("getDepositAmountForBorrow should consider the initial margin on the loan params", async () => { - await loan_pool_setup(sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC, wei("100", "ether")); - await lend_to_pool(loanToken, SUSD, owner); - - // determine borrowing parameter - const withdrawAmount = tenEth; - const durationInSeconds = 60 * 60 * 24 * 10; // 10 days - - const requiredCollateralOnProtocol = await sovryn.getRequiredCollateral( - SUSD.address, - RBTC.address, - withdrawAmount, - new BN(10).pow(new BN(18)).mul(new BN(100)), - true - ); - - const requiredCollateralOnLoanToken = await loanToken.getDepositAmountForBorrow( - withdrawAmount, - durationInSeconds, - RBTC.address - ); - expect(requiredCollateralOnProtocol).to.be.bignumber.equal(requiredCollateralOnLoanToken.subn(10)); - }); - - it("getBorrowAmountForDeposit should consider the initial margin on the loan params", async () => { - await loan_pool_setup(sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC, wei("100", "ether")); - await lend_to_pool(loanToken, SUSD, owner); - // determine borrowing parameter - const depositAmount = tenEth; - const durationInSeconds = 60 * 60 * 24 * 10; // 10 days - const borrowAmount = await loanToken.getBorrowAmountForDeposit(depositAmount, durationInSeconds, RBTC.address); - const { rate: trade_rate, precision } = await priceFeeds.queryRate(RBTC.address, SUSD.address); - const borrowingFeePercent = await sovryn.borrowingFeePercent(); - const fee = depositAmount.divn(2).mul(borrowingFeePercent).div(hunEth); - const expectedBorrowAmount = depositAmount.divn(2).sub(fee).mul(trade_rate).div(precision); - - expect(borrowAmount).to.be.bignumber.equal(expectedBorrowAmount); - }); - - /// @dev For test coverage - it("getBorrowAmountForDeposit should return 0 when depositAmount is 0", async () => { - await loan_pool_setup(sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC, wei("100", "ether")); - await lend_to_pool(loanToken, SUSD, owner); - // determine borrowing parameter - const depositAmount = new BN(0); - const durationInSeconds = 60 * 60 * 24 * 10; // 10 days - const borrowAmount = await loanToken.getBorrowAmountForDeposit(depositAmount, durationInSeconds, RBTC.address); - - expect(borrowAmount).to.be.bignumber.equal(new BN(0)); - }); - - /// @dev For test coverage - it("getBorrowAmountForDeposit should set collateralTokenAddress = wrbtcTokenAddress when collateralTokenAddress is 0", async () => { - await loan_pool_setup(sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC, wei("100", "ether")); - await lend_to_pool(loanToken, SUSD, owner); - // determine borrowing parameter - const depositAmount = tenEth; - const durationInSeconds = 60 * 60 * 24 * 10; // 10 days - const borrowAmount1 = await loanToken.getBorrowAmountForDeposit(depositAmount, durationInSeconds, ZERO_ADDRESS); - const borrowAmount2 = await loanToken.getBorrowAmountForDeposit(depositAmount, durationInSeconds, RBTC.address); - - expect(borrowAmount1).to.be.bignumber.equal(borrowAmount2); - }); - - /// @dev Testing the interest rate calculations for the range [0,k] - it("Checking that the interest rate is calculated correctly when utilization rate is [0,k]", async () => { - // prepare the test - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, owner); - - // determine borrowing parameter - const withdrawAmount = tenEth; - - // compute expected values for asserts - const interestRate = await loanToken.nextBorrowInterestRate(withdrawAmount); - - // Replicate the values of demand curve - const baseRate = wei("1", "ether"); - const rateMultiplier = wei("20.25", "ether"); - const targetLevel = wei("80", "ether"); - - // Utilization Rate - const totalAssetBorrow = await loanToken.totalAssetBorrow(); - const totalAssetSupply = await loanToken.totalAssetSupply(); - const assetBorrow = totalAssetBorrow.add(withdrawAmount); - let utilizationRate = assetBorrow.mul(new BN(10).pow(new BN(20))).div(totalAssetSupply); - if (utilizationRate < targetLevel) utilizationRate = targetLevel; - - // Interest rate calculation - // in the interval [0,k] : f(x) = b + m*x, where b is the base rate, m is the rate multiplier and x is the utilization rate - const calculatedRate = new BN(baseRate).add( - new BN(rateMultiplier).mul(new BN(utilizationRate)).div(new BN(10).pow(new BN(20))) - ); - expect(interestRate).to.be.a.bignumber.equal(calculatedRate); - }); - - /// @dev Testing the math changes for interest rate calculations for the range (k,100] - it("Checking that the interest rate is calculated correctly when utilization rate is (k,100]", async () => { - // prepare the test - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, owner); - - // determine borrowing parameter - const withdrawAmount = new BN(wei("990000000000", "ether")); //Borrow amount above Kink Level - - // compute expected values for asserts - const interestRate = await loanToken.nextBorrowInterestRate(withdrawAmount); - - // Replicate the values of demand curve - const baseRate = wei("1", "ether"); - const rateMultiplier = wei("20.25", "ether"); - const targetLevel = wei("80", "ether"); - const kinkLevel = wei("90", "ether"); - const maxScaleRate = wei("100", "ether"); - - // Utilization Rate - const totalAssetBorrow = await loanToken.totalAssetBorrow(); - const totalAssetSupply = await loanToken.totalAssetSupply(); - const assetBorrow = totalAssetBorrow.add(withdrawAmount); - let utilizationRate = assetBorrow.mul(new BN(10).pow(new BN(20))).div(totalAssetSupply); - if (utilizationRate < targetLevel) utilizationRate = targetLevel; - - // Interest rate calculation - // In the interval (k, 100] : let z = (b + m * k) - const calculatedRate = new BN(baseRate).add(new BN(rateMultiplier).mul(new BN(kinkLevel)).div(new BN(10).pow(new BN(20)))); - - // Then f(x) = z + (s - z)*(x - k)/(100-k), where s is the maximum scale rate (could be 100% or 150%) - const interest = new BN(calculatedRate).add( - new BN(new BN(maxScaleRate)) - .sub(new BN(calculatedRate)) - .mul(new BN(utilizationRate).sub(new BN(kinkLevel))) - .div(new BN(new BN(10).pow(new BN(20))).sub(new BN(kinkLevel))) - ); - expect(interestRate).to.be.a.bignumber.equal(interest); - }); - }); + let owner, account1; + let sovryn, SUSD, WRBTC, RBTC, BZRX, loanToken, loanTokenWRBTC, SOV, priceFeeds; + + async function deploymentAndInitFixture(_wallets, _provider) { + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + + loanToken = await getLoanToken(owner, sovryn, WRBTC, SUSD); + loanTokenWRBTC = await getLoanTokenWRBTC(owner, sovryn, WRBTC, SUSD); + await loan_pool_setup(sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC); + + SOV = await getSOV(sovryn, priceFeeds, SUSD, accounts); + } + + before(async () => { + [owner, account1] = accounts; + }); + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + describe("Test borrow", () => { + it("Test getRequiredCollateral w/ marginAmount = 0", async () => { + // prepare the test + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, owner); + // determine borrowing parameter + const withdrawAmount = tenEth; + + const collateralTokenSent = await sovryn.getRequiredCollateral( + SUSD.address, + RBTC.address, + withdrawAmount, + new BN(0), + true + ); + // console.log("collateralTokenSent = ", collateralTokenSent.toString()); + expect(collateralTokenSent).to.be.a.bignumber.equal(new BN(0)); + }); + + it("Test getBorrowAmount w/ marginAmount = 0", async () => { + // prepare the test + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, owner); + // determine borrowing parameter + const withdrawAmount = tenEth; + + const borrowAmount = await sovryn.getBorrowAmount( + SUSD.address, + RBTC.address, + withdrawAmount, + new BN(0), + true + ); + // console.log("borrowAmount = ", borrowAmount.toString()); + expect(borrowAmount).to.be.a.bignumber.equal(new BN(0)); + }); + + it("Test getBorrowAmount w/ and w/o Torque Loan", async () => { + // prepare the test + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, owner); + // determine borrowing parameter + const withdrawAmount = tenEth; + let marginAmount = new BN(10).pow(new BN(20)).mul(new BN(1)); + + // Without TorqueLoan + const borrowAmountNoTorque = await sovryn.getBorrowAmount( + SUSD.address, // loanToken + RBTC.address, // collateralToken + withdrawAmount, // collateralTokenAmount + marginAmount, + false // isTorqueLoan + ); + // console.log("borrowAmountNoTorque = ", borrowAmountNoTorque.toString()); + + // Compute expected values + const { rate: trade_rate, precision } = await priceFeeds.queryRate( + RBTC.address, + SUSD.address + ); + // console.log("trade_rate = ", trade_rate.toString()); + // console.log("precision = ", precision.toString()); + + const tradingFee = (await sovryn.tradingFeePercent()).mul(withdrawAmount).div(hunEth); + let expectedBorrowAmountNoTorque = withdrawAmount.sub(tradingFee); + expectedBorrowAmountNoTorque = expectedBorrowAmountNoTorque + .mul(new BN(10).pow(new BN(20))) + .mul(trade_rate); + expectedBorrowAmountNoTorque = expectedBorrowAmountNoTorque + .div(marginAmount) + .div(precision); + + // Check expected = real + expect(borrowAmountNoTorque).to.be.a.bignumber.equal(expectedBorrowAmountNoTorque); + + // With TorqueLoan + const borrowAmountTorque = await sovryn.getBorrowAmount( + SUSD.address, // loanToken + RBTC.address, // collateralToken + withdrawAmount, // collateralTokenAmount + marginAmount, + true // isTorqueLoan + ); + // console.log("borrowAmountTorque = ", borrowAmountTorque.toString()); + + // Compute expected values + marginAmount = marginAmount.add(new BN(10).pow(new BN(20))); // Torque increases the margin + const borrowingFee = (await sovryn.borrowingFeePercent()) + .mul(withdrawAmount) + .div(hunEth); + let expectedBorrowAmountTorque = withdrawAmount.sub(borrowingFee); + expectedBorrowAmountTorque = expectedBorrowAmountTorque + .mul(new BN(10).pow(new BN(20))) + .mul(trade_rate) + .div(marginAmount) + .div(precision); + + // Check expected = real + expect(borrowAmountTorque).to.be.a.bignumber.equal(expectedBorrowAmountTorque); + }); + + it("Test borrow", async () => { + // prepare the test + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, owner); + // determine borrowing parameter + const withdrawAmount = tenEth; + const durationInSeconds = 60 * 60 * 24 * 10; // 10 days + // compute the required collateral. params: address loanToken, address collateralToken, uint256 newPrincipal,uint256 marginAmount, bool isTorqueLoan + // NOTE: this is not the best method for computing the required collateral for borrowing, because it is not considering the interest payment + // better use getDepositAmountForBorrow -> need to adjust the rest of the test as well, then + const collateralTokenSent = await sovryn.getRequiredCollateral( + SUSD.address, + RBTC.address, + withdrawAmount, + new BN(10).pow(new BN(18)).mul(new BN(50)), + true + ); + + // compute expected values for asserts + const interestRate = await loanToken.nextBorrowInterestRate(withdrawAmount); + // principal = withdrawAmount/(1 - interestRate/1e20 * durationInSeconds / 31536000) + const principal = withdrawAmount + .mul(oneEth) + .div( + oneEth.sub( + interestRate + .mul(new BN(durationInSeconds)) + .mul(oneEth) + .div(new BN(31536000)) + .div(hunEth) + ) + ); + // TODO: refactor formula to remove rounding error subn(1) + const borrowingFee = (await sovryn.borrowingFeePercent()) + .mul(collateralTokenSent) + .div(hunEth); /*.addn(1)*/ + const expectedBalance = (await SUSD.balanceOf(account1)).add(withdrawAmount); + // approve the transfer of the collateral + await RBTC.approve(loanToken.address, collateralTokenSent); + const sov_initial_balance = await SOV.balanceOf(owner); + + // borrow some funds + const { tx, receipt } = await loanToken.borrow( + "0x0", // bytes32 loanId + withdrawAmount, // uint256 withdrawAmount + durationInSeconds, // uint256 initialLoanDuration + collateralTokenSent, // uint256 collateralTokenSent + RBTC.address, // address collateralTokenAddress + owner, // address borrower + account1, // address receiver + web3.utils.fromAscii("") // bytes memory loanDataBytes + ); + // assert the trade was processed as expected + await expectEvent.inTransaction(tx, LoanOpenings, "Borrow", { + user: owner, + lender: loanToken.address, + loanToken: SUSD.address, + collateralToken: RBTC.address, + newPrincipal: principal, + newCollateral: collateralTokenSent.sub(borrowingFee), + interestRate: interestRate, + }); + const decode = decodeLogs(receipt.rawLogs, LoanOpenings, "Borrow"); + const args = decode[0].args; + + expect( + args["interestDuration"] >= durationInSeconds - 1 && + args["interestDuration"] <= durationInSeconds + ).to.be.true; + expect(new BN(args["currentMargin"])).to.be.a.bignumber.gt(new BN(49).mul(oneEth)); + + // assert the user received the borrowed amount + expect(await SUSD.balanceOf(account1)).to.be.a.bignumber.equal(expectedBalance); + await verify_sov_reward_payment( + receipt.rawLogs, + FeesEvents, + SOV, + owner, + args["loanId"], + sov_initial_balance, + 1, + RBTC.address, + SUSD.address, + sovryn + ); + }); + + it("Test borrow with special rebates percentage", async () => { + // prepare the test + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, owner); + // For borrowing, the token fee is the collateral token + await sovryn.setSpecialRebates(RBTC.address, SUSD.address, wei("300", "ether")); + + /// @dev fast checking previously added rebates + let rebates = await sovryn.getSpecialRebates.call(RBTC.address, SUSD.address); + + expect(rebates).to.be.a.bignumber.equal(wei("300", "ether")); + + // determine borrowing parameter + const withdrawAmount = tenEth; + // compute the required collateral. params: address loanToken, address collateralToken, uint256 newPrincipal,uint256 marginAmount, bool isTorqueLoan + const collateralTokenSent = await sovryn.getRequiredCollateral( + SUSD.address, + RBTC.address, + withdrawAmount, + new BN(10).pow(new BN(18)).mul(new BN(50)), + true + ); + const durationInSeconds = 60 * 60 * 24 * 10; // 10 days + // compute expected values for asserts + const interestRate = await loanToken.nextBorrowInterestRate(withdrawAmount); + // principal = withdrawAmount/(1 - interestRate/1e20 * durationInSeconds / 31536000) + const principal = withdrawAmount + .mul(oneEth) + .div( + oneEth.sub( + interestRate + .mul(new BN(durationInSeconds)) + .mul(oneEth) + .div(new BN(31536000)) + .div(hunEth) + ) + ); + // TODO: refactor formula to remove rounding error subn(1) + const borrowingFee = (await sovryn.borrowingFeePercent()) + .mul(collateralTokenSent) + .div(hunEth); /*.addn(1)*/ + const expectedBalance = (await SUSD.balanceOf(account1)).add(withdrawAmount); + // approve the transfer of the collateral + await RBTC.approve(loanToken.address, collateralTokenSent); + const sov_initial_balance = await SOV.balanceOf(owner); + + // borrow some funds + const { tx, receipt } = await loanToken.borrow( + "0x0", // bytes32 loanId + withdrawAmount, // uint256 withdrawAmount + durationInSeconds, // uint256 initialLoanDuration + collateralTokenSent, // uint256 collateralTokenSent + RBTC.address, // address collateralTokenAddress + owner, // address borrower + account1, // address receiver + web3.utils.fromAscii("") // bytes memory loanDataBytes + ); + // assert the trade was processed as expected + await expectEvent.inTransaction(tx, LoanOpenings, "Borrow", { + user: owner, + lender: loanToken.address, + loanToken: SUSD.address, + collateralToken: RBTC.address, + newPrincipal: principal, + newCollateral: collateralTokenSent.sub(borrowingFee), + interestRate: interestRate, + }); + const decode = decodeLogs(receipt.rawLogs, LoanOpenings, "Borrow"); + const args = decode[0].args; + + expect( + args["interestDuration"] >= durationInSeconds - 1 && + args["interestDuration"] <= durationInSeconds + ).to.be.true; + expect(new BN(args["currentMargin"])).to.be.a.bignumber.gt(new BN(49).mul(oneEth)); + + // assert the user received the borrowed amount + expect(await SUSD.balanceOf(account1)).to.be.a.bignumber.equal(expectedBalance); + await verify_sov_reward_payment( + receipt.rawLogs, + FeesEvents, + SOV, + owner, + args["loanId"], + sov_initial_balance, + 1, + RBTC.address, + SUSD.address, + sovryn + ); + }); + + it("Test borrow 0 collateral should fail", async () => { + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, owner); + await expectRevert( + loanToken.borrow( + "0x0", // bytes32 loanId + 10, // uint256 withdrawAmount + 24 * 60 * 60, // uint256 initialLoanDuration + 0, // uint256 collateralTokenSent + RBTC.address, // address collateralTokenAddress + owner, // address borrower + account1, // address receiver + "0x0" // bytes memory loanDataBytes + ), + "7" + ); + }); + + it("Test borrow 0 withdraw should fail", async () => { + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, owner); + expectRevert( + loanToken.borrow( + "0x0", // bytes32 loanId + 0, // uint256 withdrawAmount + 24 * 60 * 60, // uint256 initialLoanDuration + 10, // uint256 collateralTokenSent + RBTC.address, // address collateralTokenAddress + owner, // address borrower + account1, // address receiver + "0x0" // bytes memory loanDataBytes + ), + "6" + ); + }); + + it("Test borrow sending value with tokens should fail", async () => { + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, owner); + expectRevert( + loanToken.borrow( + "0x0", // bytes32 loanId + 10, // uint256 withdrawAmount + 24 * 60 * 60, // uint256 initialLoanDuration + 10, // uint256 collateralTokenSent + RBTC.address, // address collateralTokenAddress + owner, // address borrower + account1, // address receiver + "0x0", // bytes memory loanDataBytes + { value: 100 } + ), + "7" + ); + }); + + it("Test borrow invalid collateral should fail", async () => { + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, owner); + await expectRevert( + loanToken.borrow( + "0x0", // bytes32 loanId + 10, // uint256 withdrawAmount + 24 * 60 * 60, // uint256 initialLoanDuration + 10, // uint256 collateralTokenSent + CONSTANTS.ZERO_ADDRESS, // address collateralTokenAddress + owner, // address borrower + account1, // address receiver + "0x0" // bytes memory loanDataBytes + ), + "7" + ); + + expectRevert( + loanToken.borrow( + "0x0", // bytes32 loanId + 10, // uint256 withdrawAmount + 24 * 60 * 60, // uint256 initialLoanDuration + 10, // uint256 collateralTokenSent + SUSD.address, // address collateralTokenAddress + owner, // address borrower + account1, // address receiver + "0x0" // bytes memory loanDataBytes + ), + "10" + ); + }); + + it("Test borrow no interest should fail", async () => { + // no demand curve settings -> no interest set + // prepare the test + await lend_to_pool(loanToken, SUSD, owner); + + // determine borrowing parameter + const withdrawAmount = oneEth.mul(new BN(10)); // I want to borrow 10 USD + // compute the required collateral. params: address loanToken, address collateralToken, uint256 newPrincipal,uint256 marginAmount, bool isTorqueLoan + const collateralTokenSent = await sovryn.getRequiredCollateral( + SUSD.address, + RBTC.address, + withdrawAmount, + new BN(50).pow(new BN(18)), + true + ); + + // approve the transfer of the collateral + await RBTC.approve(loanToken.address, collateralTokenSent); + expectRevert( + loanToken.borrow( + "0x0", // bytes32 loanId + withdrawAmount, // uint256 withdrawAmount + 24 * 60 * 60, // uint256 initialLoanDuration + collateralTokenSent, // uint256 collateralTokenSent + RBTC.address, // address collateralTokenAddress + owner, // address borrower + account1, // address receiver + "0x0" // bytes memory loanDataBytes + ), + "invalid interest" + ); + }); + + it("Test borrow insufficient collateral should fail", async () => { + // prepare the test + + await lend_to_pool(loanToken, SUSD, owner); + await set_demand_curve(loanToken); + + // determine borrowing parameter + const withdrawAmount = oneEth.mul(new BN(10)); // I want to borrow 10 USD + // compute the required collateral. params: address loanToken, address collateralToken, uint256 newPrincipal,uint256 marginAmount, bool isTorqueLoan + let collateralTokenSent = await sovryn.getRequiredCollateral( + SUSD.address, + RBTC.address, + withdrawAmount, + new BN(10).pow(new BN(18)).mul(new BN(50)), + true + ); + collateralTokenSent = collateralTokenSent.div(new BN(2)); + + // approve the transfer of the collateral + await RBTC.approve(loanToken.address, collateralTokenSent); + expectRevert( + loanToken.borrow( + "0x0", // bytes32 loanId + withdrawAmount, // uint256 withdrawAmount + 24 * 60 * 60, // uint256 initialLoanDuration + collateralTokenSent, // uint256 collateralTokenSent + RBTC.address, // address collateralTokenAddress + owner, // address borrower + account1, // address receiver + "0x0" // bytes memory loanDataBytes + ), + "collateral insufficient" + ); + }); + + // borrows some funds from account 0 and then takes out some more from account 2 with 'borrow' without paying should fail. + it("Test borrow from foreign loan should fail", async () => { + // prepare the test + + await lend_to_pool(loanToken, SUSD, owner); + await set_demand_curve(loanToken); + + // determine borrowing parameter + const withdrawAmount = oneEth.mul(new BN(10)); // I want to borrow 10 USD + // compute the required collateral. params: address loanToken, address collateralToken, uint256 newPrincipal,uint256 marginAmount, bool isTorqueLoan + let collateralTokenSent = await sovryn.getRequiredCollateral( + SUSD.address, + RBTC.address, + withdrawAmount, + new BN(10).pow(new BN(18)).mul(new BN(50)), + true + ); + collateralTokenSent = collateralTokenSent.mul(new BN(2)); + const durationInSeconds = 60 * 60 * 24 * 10; + + // approve the transfer of the collateral + await RBTC.approve(loanToken.address, collateralTokenSent); + const borrower = accounts[0]; + + const { receipt } = await loanToken.borrow( + "0x0", // bytes32 loanId + withdrawAmount, // uint256 withdrawAmount + durationInSeconds, // uint256 initialLoanDuration + collateralTokenSent, // uint256 collateralTokenSent + RBTC.address, // address collateralTokenAddress + borrower, // address borrower + account1, // address receiver + "0x0" // bytes memory loanDataBytes + ); + + const decode = decodeLogs(receipt.rawLogs, LoanOpenings, "Borrow"); + const loanId = decode[0].args["loanId"]; + + await RBTC.transfer(accounts[2], collateralTokenSent); + // approve the transfer of the collateral + await RBTC.approve(loanToken.address, collateralTokenSent, { from: accounts[2] }); + + await expectRevert( + loanToken.borrow( + loanId, // bytes32 loanId + withdrawAmount.div(new BN(2)), // uint256 withdrawAmount + durationInSeconds, // uint256 initialLoanDuration + 1, // uint256 collateralTokenSent + RBTC.address, // address collateralTokenAddress + borrower, // address borrower + accounts[2], // address receiver + "0x0", // bytes memory loanDataBytes + { from: accounts[2] } + ), + "7" + ); + }); + + // borrows some funds from account 0 and then takes out some more from account 2 with a marginTrade without paying should fail. + it("Test margin trade from foreign loan should fail", async () => { + // prepare the test + + await lend_to_pool(loanToken, SUSD, owner); + await set_demand_curve(loanToken); + + // determine borrowing parameter + const withdrawAmount = oneEth.mul(new BN(10)); // I want to borrow 10 USD + // compute the required collateral. params: address loanToken, address collateralToken, uint256 newPrincipal,uint256 marginAmount, bool isTorqueLoan + let collateralTokenSent = await sovryn.getRequiredCollateral( + SUSD.address, + RBTC.address, + withdrawAmount, + new BN(10).pow(new BN(18)).mul(new BN(50)), + true + ); + collateralTokenSent = collateralTokenSent.mul(new BN(2)); + const durationInSeconds = 60 * 60 * 24 * 10; + + // approve the transfer of the collateral + await RBTC.approve(loanToken.address, collateralTokenSent); + const borrower = accounts[0]; + + const { receipt } = await loanToken.borrow( + "0x0", // bytes32 loanId + withdrawAmount, // uint256 withdrawAmount + durationInSeconds, // uint256 initialLoanDuration + collateralTokenSent, // uint256 collateralTokenSent + RBTC.address, // address collateralTokenAddress + borrower, // address borrower + account1, // address receiver + web3.utils.fromAscii("") // bytes memory loanDataBytes + ); + + const decode = decodeLogs(receipt.rawLogs, LoanOpenings, "Borrow"); + const loanId = decode[0].args["loanId"]; + + await SUSD.transfer(accounts[2], withdrawAmount); + // approve the transfer of the collateral + await SUSD.approve(loanToken.address, withdrawAmount, { from: accounts[2] }); + + await expectRevert( + loanToken.marginTrade( + loanId, // bytes32 loanId + oneEth, // uint256 withdrawAmount + withdrawAmount, // uint256 collateralTokenSent + 0, + RBTC.address, // address collateralTokenAddress + accounts[2], // address receiver + 0, + "0x0", // bytes memory loanDataBytes + { from: accounts[2] } + ), + "borrower mismatch" + ); + }); + + // margin trades from account 0 and then borrows from same loan should fail. + it("Test borrow from trade position should fail", async () => { + // prepare the test + + await lend_to_pool(loanToken, SUSD, owner); + await set_demand_curve(loanToken); + + // determine borrowing parameter + const withdrawAmount = oneEth.mul(new BN(10)); // I want to borrow 10 USD + + await SUSD.approve(loanToken.address, withdrawAmount); + + const { receipt } = await loanToken.marginTrade( + "0x0", // bytes32 loanId + oneEth, // uint256 withdrawAmount + withdrawAmount, // uint256 collateralTokenSent + 0, + RBTC.address, // address collateralTokenAddress + accounts[0], // address receiver + 0, + "0x" // bytes memory loanDataBytes + ); + + const decode = decodeLogs(receipt.rawLogs, LoanOpenings, "Trade"); + const loanId = decode[0].args["loanId"]; + + // approve the transfer of the collateral + await RBTC.approve(loanToken.address, withdrawAmount); + + await expectRevert( + loanToken.borrow( + loanId, // bytes32 loanId + withdrawAmount.div(new BN(10)), // uint256 withdrawAmount + 60 * 60 * 24 * 10, // uint256 initialLoanDuration + 1, // uint256 collateralTokenSent + RBTC.address, // address collateralTokenAddress + accounts[0], // address borrower + accounts[0], // address receiver + "0x" // bytes memory loanDataBytes + ), + "loanParams mismatch" + ); + }); + + // 50% was hardcoded on the old contracts -> would have failed, but should work now + it("Borrowing with more than 50% initial margin", async () => { + await set_demand_curve(loanToken); + await loan_pool_setup( + sovryn, + owner, + RBTC, + WRBTC, + SUSD, + loanToken, + loanTokenWRBTC, + wei("100", "ether") + ); + await lend_to_pool(loanToken, SUSD, owner); + // determine borrowing parameter + const withdrawAmount = tenEth; + const durationInSeconds = 60 * 60 * 24 * 10; // 10 days + // compute the required collateral + const collateralTokenSent = await loanToken.getDepositAmountForBorrow( + withdrawAmount, + durationInSeconds, + RBTC.address + ); + + // TODO: refactor formula to remove rounding error subn(1) + const borrowingFee = (await sovryn.borrowingFeePercent()) + .mul(collateralTokenSent) + .div(hunEth) + .addn(1); + + // compute expected values for asserts + const interestRate = await loanToken.nextBorrowInterestRate(withdrawAmount); + // principal = withdrawAmount/(1 - interestRate/1e20 * durationInSeconds / 31536000) + const principal = withdrawAmount + .mul(oneEth) + .div( + oneEth.sub( + interestRate + .mul(new BN(durationInSeconds)) + .mul(oneEth) + .div(new BN(31536000)) + .div(hunEth) + ) + ); + + // approve the transfer of the collateral + await RBTC.approve(loanToken.address, collateralTokenSent); + + // borrow some funds + const { tx, receipt } = await loanToken.borrow( + "0x0", // bytes32 loanId + withdrawAmount, // uint256 withdrawAmount + durationInSeconds, // uint256 initialLoanDuration + collateralTokenSent, // uint256 collateralTokenSent + RBTC.address, // address collateralTokenAddress + owner, // address borrower + account1, // address receiver + web3.utils.fromAscii("") // bytes memory loanDataBytes + ); + // assert the trade was processed as expected + await expectEvent.inTransaction(tx, LoanOpenings, "Borrow", { + user: owner, + lender: loanToken.address, + loanToken: SUSD.address, + collateralToken: RBTC.address, + newPrincipal: principal, + newCollateral: collateralTokenSent.sub(borrowingFee), + interestRate: interestRate, + }); + const decode = decodeLogs(receipt.rawLogs, LoanOpenings, "Borrow"); + const args = decode[0].args; + + expect( + args["interestDuration"] >= durationInSeconds - 1 && + args["interestDuration"] <= durationInSeconds + ).to.be.true; + expect(new BN(args["currentMargin"])).to.be.a.bignumber.gt(new BN(99).mul(oneEth)); + }); + + /// @dev For test coverage + it("getDepositAmountForBorrow should return 0 when borrowAmount is 0", async () => { + await set_demand_curve(loanToken); + await loan_pool_setup( + sovryn, + owner, + RBTC, + WRBTC, + SUSD, + loanToken, + loanTokenWRBTC, + wei("100", "ether") + ); + await lend_to_pool(loanToken, SUSD, owner); + // determine borrowing parameter + const borrowAmount = new BN(0); + const durationInSeconds = 60 * 60 * 24 * 10; // 10 days + const collateralTokenSent = await loanToken.getDepositAmountForBorrow( + borrowAmount, + durationInSeconds, + RBTC.address + ); + + expect(collateralTokenSent).to.be.bignumber.equal(new BN(0)); + }); + + it("getDepositAmountForBorrow should consider the initial margin on the loan params", async () => { + await loan_pool_setup( + sovryn, + owner, + RBTC, + WRBTC, + SUSD, + loanToken, + loanTokenWRBTC, + wei("100", "ether") + ); + await lend_to_pool(loanToken, SUSD, owner); + + // determine borrowing parameter + const withdrawAmount = tenEth; + const durationInSeconds = 60 * 60 * 24 * 10; // 10 days + + const requiredCollateralOnProtocol = await sovryn.getRequiredCollateral( + SUSD.address, + RBTC.address, + withdrawAmount, + new BN(10).pow(new BN(18)).mul(new BN(100)), + true + ); + + const requiredCollateralOnLoanToken = await loanToken.getDepositAmountForBorrow( + withdrawAmount, + durationInSeconds, + RBTC.address + ); + expect(requiredCollateralOnProtocol).to.be.bignumber.equal( + requiredCollateralOnLoanToken.subn(10) + ); + }); + + it("getBorrowAmountForDeposit should consider the initial margin on the loan params", async () => { + await loan_pool_setup( + sovryn, + owner, + RBTC, + WRBTC, + SUSD, + loanToken, + loanTokenWRBTC, + wei("100", "ether") + ); + await lend_to_pool(loanToken, SUSD, owner); + // determine borrowing parameter + const depositAmount = tenEth; + const durationInSeconds = 60 * 60 * 24 * 10; // 10 days + const borrowAmount = await loanToken.getBorrowAmountForDeposit( + depositAmount, + durationInSeconds, + RBTC.address + ); + const { rate: trade_rate, precision } = await priceFeeds.queryRate( + RBTC.address, + SUSD.address + ); + const borrowingFeePercent = await sovryn.borrowingFeePercent(); + const fee = depositAmount.divn(2).mul(borrowingFeePercent).div(hunEth); + const expectedBorrowAmount = depositAmount + .divn(2) + .sub(fee) + .mul(trade_rate) + .div(precision); + + expect(borrowAmount).to.be.bignumber.equal(expectedBorrowAmount); + }); + + /// @dev For test coverage + it("getBorrowAmountForDeposit should return 0 when depositAmount is 0", async () => { + await loan_pool_setup( + sovryn, + owner, + RBTC, + WRBTC, + SUSD, + loanToken, + loanTokenWRBTC, + wei("100", "ether") + ); + await lend_to_pool(loanToken, SUSD, owner); + // determine borrowing parameter + const depositAmount = new BN(0); + const durationInSeconds = 60 * 60 * 24 * 10; // 10 days + const borrowAmount = await loanToken.getBorrowAmountForDeposit( + depositAmount, + durationInSeconds, + RBTC.address + ); + + expect(borrowAmount).to.be.bignumber.equal(new BN(0)); + }); + + /// @dev For test coverage + it("getBorrowAmountForDeposit should set collateralTokenAddress = wrbtcTokenAddress when collateralTokenAddress is 0", async () => { + await loan_pool_setup( + sovryn, + owner, + RBTC, + WRBTC, + SUSD, + loanToken, + loanTokenWRBTC, + wei("100", "ether") + ); + await lend_to_pool(loanToken, SUSD, owner); + // determine borrowing parameter + const depositAmount = tenEth; + const durationInSeconds = 60 * 60 * 24 * 10; // 10 days + const borrowAmount1 = await loanToken.getBorrowAmountForDeposit( + depositAmount, + durationInSeconds, + ZERO_ADDRESS + ); + const borrowAmount2 = await loanToken.getBorrowAmountForDeposit( + depositAmount, + durationInSeconds, + RBTC.address + ); + + expect(borrowAmount1).to.be.bignumber.equal(borrowAmount2); + }); + + /// @dev Testing the interest rate calculations for the range [0,k] + it("Checking that the interest rate is calculated correctly when utilization rate is [0,k]", async () => { + // prepare the test + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, owner); + + // determine borrowing parameter + const withdrawAmount = tenEth; + + // compute expected values for asserts + const interestRate = await loanToken.nextBorrowInterestRate(withdrawAmount); + + // Replicate the values of demand curve + const baseRate = wei("1", "ether"); + const rateMultiplier = wei("20.25", "ether"); + const targetLevel = wei("80", "ether"); + + // Utilization Rate + const totalAssetBorrow = await loanToken.totalAssetBorrow(); + const totalAssetSupply = await loanToken.totalAssetSupply(); + const assetBorrow = totalAssetBorrow.add(withdrawAmount); + let utilizationRate = assetBorrow + .mul(new BN(10).pow(new BN(20))) + .div(totalAssetSupply); + if (utilizationRate < targetLevel) utilizationRate = targetLevel; + + // Interest rate calculation + // in the interval [0,k] : f(x) = b + m*x, where b is the base rate, m is the rate multiplier and x is the utilization rate + const calculatedRate = new BN(baseRate).add( + new BN(rateMultiplier).mul(new BN(utilizationRate)).div(new BN(10).pow(new BN(20))) + ); + expect(interestRate).to.be.a.bignumber.equal(calculatedRate); + }); + + /// @dev Testing the math changes for interest rate calculations for the range (k,100] + it("Checking that the interest rate is calculated correctly when utilization rate is (k,100]", async () => { + // prepare the test + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, owner); + + // determine borrowing parameter + const withdrawAmount = new BN(wei("990000000000", "ether")); //Borrow amount above Kink Level + + // compute expected values for asserts + const interestRate = await loanToken.nextBorrowInterestRate(withdrawAmount); + + // Replicate the values of demand curve + const baseRate = wei("1", "ether"); + const rateMultiplier = wei("20.25", "ether"); + const targetLevel = wei("80", "ether"); + const kinkLevel = wei("90", "ether"); + const maxScaleRate = wei("100", "ether"); + + // Utilization Rate + const totalAssetBorrow = await loanToken.totalAssetBorrow(); + const totalAssetSupply = await loanToken.totalAssetSupply(); + const assetBorrow = totalAssetBorrow.add(withdrawAmount); + let utilizationRate = assetBorrow + .mul(new BN(10).pow(new BN(20))) + .div(totalAssetSupply); + if (utilizationRate < targetLevel) utilizationRate = targetLevel; + + // Interest rate calculation + // In the interval (k, 100] : let z = (b + m * k) + const calculatedRate = new BN(baseRate).add( + new BN(rateMultiplier).mul(new BN(kinkLevel)).div(new BN(10).pow(new BN(20))) + ); + + // Then f(x) = z + (s - z)*(x - k)/(100-k), where s is the maximum scale rate (could be 100% or 150%) + const interest = new BN(calculatedRate).add( + new BN(new BN(maxScaleRate)) + .sub(new BN(calculatedRate)) + .mul(new BN(utilizationRate).sub(new BN(kinkLevel))) + .div(new BN(new BN(10).pow(new BN(20))).sub(new BN(kinkLevel))) + ); + expect(interestRate).to.be.a.bignumber.equal(interest); + }); + }); }); diff --git a/tests/loan-token/CallOptionalReturn.js b/tests/loan-token/CallOptionalReturn.js index e1c9b863c..1197363a6 100644 --- a/tests/loan-token/CallOptionalReturn.js +++ b/tests/loan-token/CallOptionalReturn.js @@ -30,114 +30,130 @@ const TOTAL_SUPPLY = web3.utils.toWei("1000", "ether"); const wei = web3.utils.toWei; contract("CallOptionalReturn", (accounts) => { - const name = "Test token"; - const symbol = "TST"; - - let lender, account1; - let underlyingToken, testWrbtc; - let sovryn, loanToken; - - before(async () => { - [lender, account1, ...accounts] = accounts; - }); - - beforeEach(async () => { - //Token - underlyingToken = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); - testWrbtc = await TestWrbtc.new(); - - const sovrynproxy = await sovrynProtocol.new(); - sovryn = await ISovryn.at(sovrynproxy.address); - - await sovryn.replaceContract((await LoanClosingsLiquidation.new()).address); - await sovryn.replaceContract((await LoanClosingsRollover.new()).address); - await sovryn.replaceContract((await LoanClosingsWith.new()).address); - await sovryn.replaceContract((await ProtocolSettings.new()).address); - await sovryn.replaceContract((await LoanSettings.new()).address); - await sovryn.replaceContract((await LoanMaintenance.new()).address); - await sovryn.replaceContract((await SwapsExternal.new()).address); - await sovryn.replaceContract((await LoanOpenings.new()).address); - await sovryn.replaceContract((await Affiliates.new()).address); - - await sovryn.setWrbtcToken(testWrbtc.address); - - feeds = await PriceFeedsLocal.new(testWrbtc.address, sovryn.address); - await feeds.setRates(underlyingToken.address, testWrbtc.address, wei("0.01", "ether")); - const swaps = await SwapsImplSovrynSwap.new(); - const sovrynSwapSimulator = await TestSovrynSwap.new(feeds.address); - await sovryn.setSovrynSwapContractRegistryAddress(sovrynSwapSimulator.address); - await sovryn.setSupportedTokens([underlyingToken.address, testWrbtc.address], [true, true]); - await sovryn.setPriceFeedContract( - feeds.address //priceFeeds - ); - await sovryn.setSwapsImplContract( - swaps.address // swapsImpl - ); - await sovryn.setFeesController(lender); - const sov = await TestToken.new("SOV", "SOV", 18, TOTAL_SUPPLY); - await sovryn.setProtocolTokenAddress(sov.address); - await sovryn.setSOVTokenAddress(sov.address); - await sovryn.setLockedSOVAddress((await LockedSOVMockup.new(sov.address, [accounts[0]])).address); - - /** Deploy LoanTokenLogicBeacon */ - let loanTokenLogicBeacon = await LoanTokenLogicBeacon.new(); - - /** Deploy LoanTokenSettingsLowerAdmin*/ - const loanTokenSettingsLowerAdmin = await LoanTokenSettingsLowerAdmin.new(); - - /** Register Loan Token Modules to the Beacon */ - await loanTokenLogicBeacon.registerLoanTokenModule(loanTokenSettingsLowerAdmin.address); - - let loanTokenLogicLM = await LoanTokenLogicLMMockup.new(); - - /** Register Loan Token Logic LM to the Beacon */ - await loanTokenLogicBeacon.registerLoanTokenModule(loanTokenLogicLM.address); - - /** Deploy LoanTokenLogicProxy */ - let loanTokenLogic = await LoanTokenLogicProxy.new(loanTokenLogicBeacon.address); - - loanToken = await LoanToken.new(lender, loanTokenLogic.address, sovryn.address, testWrbtc.address); - await loanToken.initialize(underlyingToken.address, name, symbol); //iToken - - const loanTokenAddress = await loanToken.loanTokenAddress(); - - params = [ - "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object - false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans - lender, // address owner; // owner of this object - underlyingToken.address, // address loanToken; // the token being loaned - testWrbtc.address, // address collateralToken; // the required collateral token - wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin - wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value - 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) - ]; - - /** Initialize the loan token logic proxy */ - loanToken = await ILoanTokenLogicProxy.at(loanToken.address); - await loanToken.setBeaconAddress(loanTokenLogicBeacon.address); - - /** Use interface of LoanTokenModules */ - loanToken = await ILoanTokenModules.at(loanToken.address); - - await loanToken.setupLoanParams([params], false); - - if (lender == (await sovryn.owner())) await sovryn.setLoanPool([loanToken.address], [loanTokenAddress]); - await testWrbtc.mint(sovryn.address, wei("500", "ether")); - }); - - describe("Token should be a contract address", () => { - it("Check that it reverts when internal function _callOptionalReturn is called", async () => { - await lend_to_the_pool(loanToken, lender, underlyingToken, testWrbtc, sovryn); - - // above functionn also opens a trading position, so I need to add some more funds to be able to withdraw everything - const balanceOf0 = await loanToken.assetBalanceOf(lender); - await underlyingToken.approve(loanToken.address, balanceOf0.toString()); - await loanToken.mint(account1, balanceOf0.toString()); - const profitBefore = await loanToken.profitOf(lender); - const iTokenBalance = await loanToken.balanceOf(lender); - - // burn everything -> profit should be 0 - await expectRevert(loanToken.burn(lender, iTokenBalance.toString()), "call to a non-contract address"); - }); - }); + const name = "Test token"; + const symbol = "TST"; + + let lender, account1; + let underlyingToken, testWrbtc; + let sovryn, loanToken; + + before(async () => { + [lender, account1, ...accounts] = accounts; + }); + + beforeEach(async () => { + //Token + underlyingToken = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); + testWrbtc = await TestWrbtc.new(); + + const sovrynproxy = await sovrynProtocol.new(); + sovryn = await ISovryn.at(sovrynproxy.address); + + await sovryn.replaceContract((await LoanClosingsLiquidation.new()).address); + await sovryn.replaceContract((await LoanClosingsRollover.new()).address); + await sovryn.replaceContract((await LoanClosingsWith.new()).address); + await sovryn.replaceContract((await ProtocolSettings.new()).address); + await sovryn.replaceContract((await LoanSettings.new()).address); + await sovryn.replaceContract((await LoanMaintenance.new()).address); + await sovryn.replaceContract((await SwapsExternal.new()).address); + await sovryn.replaceContract((await LoanOpenings.new()).address); + await sovryn.replaceContract((await Affiliates.new()).address); + + await sovryn.setWrbtcToken(testWrbtc.address); + + feeds = await PriceFeedsLocal.new(testWrbtc.address, sovryn.address); + await feeds.setRates(underlyingToken.address, testWrbtc.address, wei("0.01", "ether")); + const swaps = await SwapsImplSovrynSwap.new(); + const sovrynSwapSimulator = await TestSovrynSwap.new(feeds.address); + await sovryn.setSovrynSwapContractRegistryAddress(sovrynSwapSimulator.address); + await sovryn.setSupportedTokens( + [underlyingToken.address, testWrbtc.address], + [true, true] + ); + await sovryn.setPriceFeedContract( + feeds.address //priceFeeds + ); + await sovryn.setSwapsImplContract( + swaps.address // swapsImpl + ); + await sovryn.setFeesController(lender); + const sov = await TestToken.new("SOV", "SOV", 18, TOTAL_SUPPLY); + await sovryn.setProtocolTokenAddress(sov.address); + await sovryn.setSOVTokenAddress(sov.address); + await sovryn.setLockedSOVAddress( + ( + await LockedSOVMockup.new(sov.address, [accounts[0]]) + ).address + ); + + /** Deploy LoanTokenLogicBeacon */ + let loanTokenLogicBeacon = await LoanTokenLogicBeacon.new(); + + /** Deploy LoanTokenSettingsLowerAdmin*/ + const loanTokenSettingsLowerAdmin = await LoanTokenSettingsLowerAdmin.new(); + + /** Register Loan Token Modules to the Beacon */ + await loanTokenLogicBeacon.registerLoanTokenModule(loanTokenSettingsLowerAdmin.address); + + let loanTokenLogicLM = await LoanTokenLogicLMMockup.new(); + + /** Register Loan Token Logic LM to the Beacon */ + await loanTokenLogicBeacon.registerLoanTokenModule(loanTokenLogicLM.address); + + /** Deploy LoanTokenLogicProxy */ + let loanTokenLogic = await LoanTokenLogicProxy.new(loanTokenLogicBeacon.address); + + loanToken = await LoanToken.new( + lender, + loanTokenLogic.address, + sovryn.address, + testWrbtc.address + ); + await loanToken.initialize(underlyingToken.address, name, symbol); //iToken + + const loanTokenAddress = await loanToken.loanTokenAddress(); + + params = [ + "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object + false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans + lender, // address owner; // owner of this object + underlyingToken.address, // address loanToken; // the token being loaned + testWrbtc.address, // address collateralToken; // the required collateral token + wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin + wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value + 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) + ]; + + /** Initialize the loan token logic proxy */ + loanToken = await ILoanTokenLogicProxy.at(loanToken.address); + await loanToken.setBeaconAddress(loanTokenLogicBeacon.address); + + /** Use interface of LoanTokenModules */ + loanToken = await ILoanTokenModules.at(loanToken.address); + + await loanToken.setupLoanParams([params], false); + + if (lender == (await sovryn.owner())) + await sovryn.setLoanPool([loanToken.address], [loanTokenAddress]); + await testWrbtc.mint(sovryn.address, wei("500", "ether")); + }); + + describe("Token should be a contract address", () => { + it("Check that it reverts when internal function _callOptionalReturn is called", async () => { + await lend_to_the_pool(loanToken, lender, underlyingToken, testWrbtc, sovryn); + + // above functionn also opens a trading position, so I need to add some more funds to be able to withdraw everything + const balanceOf0 = await loanToken.assetBalanceOf(lender); + await underlyingToken.approve(loanToken.address, balanceOf0.toString()); + await loanToken.mint(account1, balanceOf0.toString()); + const profitBefore = await loanToken.profitOf(lender); + const iTokenBalance = await loanToken.balanceOf(lender); + + // burn everything -> profit should be 0 + await expectRevert( + loanToken.burn(lender, iTokenBalance.toString()), + "call to a non-contract address" + ); + }); + }); }); diff --git a/tests/loan-token/LendingTestToken.test.js b/tests/loan-token/LendingTestToken.test.js index 4c30d92e3..fb5e4cc22 100644 --- a/tests/loan-token/LendingTestToken.test.js +++ b/tests/loan-token/LendingTestToken.test.js @@ -28,22 +28,26 @@ const PriceFeedsLocal = artifacts.require("PriceFeedsLocal"); const TestSovrynSwap = artifacts.require("TestSovrynSwap"); const SwapsImplSovrynSwap = artifacts.require("SwapsImplSovrynSwap"); -const { lend_to_the_pool, cash_out_from_the_pool, cash_out_from_the_pool_uint256_max_should_withdraw_total_balance } = require("./helpers"); const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getLoanTokenLogic, - getLoanToken, - getLoanTokenLogicWrbtc, - getLoanTokenWRBTC, - loan_pool_setup, - set_demand_curve, - getPriceFeeds, - getSovryn, - decodeLogs, - getSOV, + lend_to_the_pool, + cash_out_from_the_pool, + cash_out_from_the_pool_uint256_max_should_withdraw_total_balance, +} = require("./helpers"); +const { + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getLoanTokenLogic, + getLoanToken, + getLoanTokenLogicWrbtc, + getLoanTokenWRBTC, + loan_pool_setup, + set_demand_curve, + getPriceFeeds, + getSovryn, + decodeLogs, + getSOV, } = require("../Utils/initializer.js"); const { ZERO_ADDRESS } = require("@openzeppelin/test-helpers/src/constants"); @@ -51,244 +55,313 @@ const { ZERO_ADDRESS } = require("@openzeppelin/test-helpers/src/constants"); const wei = web3.utils.toWei; contract("LoanTokenLending", (accounts) => { - const name = "Test token"; - const symbol = "TST"; - - let lender, account1, account2, account3, account4; - let SUSD, WRBTC; - let sovryn, loanToken; - - async function deploymentAndInitFixture(_wallets, _provider) { - // Deploying sovrynProtocol w/ generic function from initializer.js - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - await sovryn.setSovrynProtocolAddress(sovryn.address); - - await sovryn.setWrbtcToken(WRBTC.address); - - feeds = await PriceFeedsLocal.new(WRBTC.address, sovryn.address); - await feeds.setRates(SUSD.address, WRBTC.address, wei("0.01", "ether")); - const swaps = await SwapsImplSovrynSwap.new(); - const sovrynSwapSimulator = await TestSovrynSwap.new(feeds.address); - await sovryn.setSovrynSwapContractRegistryAddress(sovrynSwapSimulator.address); - await sovryn.setSupportedTokens([SUSD.address, WRBTC.address], [true, true]); - await sovryn.setPriceFeedContract( - feeds.address // priceFeeds - ); - await sovryn.setSwapsImplContract( - swaps.address // swapsImpl - ); - await sovryn.setFeesController(lender); - - sov = await getSOV(sovryn, priceFeeds, SUSD, accounts); - - const initLoanTokenLogic = await getLoanTokenLogic(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] - loanTokenLogic = initLoanTokenLogic[0]; - loanTokenLogicBeacon = initLoanTokenLogic[1]; - - loanToken = await LoanToken.new(lender, loanTokenLogic.address, sovryn.address, WRBTC.address); - await loanToken.initialize(SUSD.address, name, symbol); // iToken - - const loanTokenAddress = await loanToken.loanTokenAddress(); - - params = [ - "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object - false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans - lender, // address owner; // owner of this object - SUSD.address, // address loanToken; // the token being loaned - WRBTC.address, // address collateralToken; // the required collateral token - wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin - wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value - 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) - ]; - - /** Initialize the loan token logic proxy */ - loanToken = await ILoanTokenLogicProxy.at(loanToken.address); - await loanToken.setBeaconAddress(loanTokenLogicBeacon.address); - - /** Use interface of LoanTokenModules */ - loanToken = await ILoanTokenModules.at(loanToken.address); - - await loanToken.setupLoanParams([params], false); - - if (lender == (await sovryn.owner())) await sovryn.setLoanPool([loanToken.address], [loanTokenAddress]); - // const baseRate = wei("1", "ether"); - // const rateMultiplier = wei("20.25", "ether"); - // const targetLevel = wei("80", "ether"); - // const kinkLevel = wei("90", "ether"); - // const maxScaleRate = wei("100", "ether"); - // await loanToken.setDemandCurve(baseRate, rateMultiplier, baseRate, rateMultiplier, targetLevel, kinkLevel, maxScaleRate); - - await WRBTC.mint(sovryn.address, wei("500", "ether")); - } - - before(async () => { - [lender, account1, account2, account3, account4, ...accounts] = accounts; - }); - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - describe("Test lending using TestToken", () => { - it("Test disableLoanParams", async () => { - await loanToken.disableLoanParams([WRBTC.address], [false]); - }); - - it("Should revert when calling disableLoanParams by other than Admin", async () => { - await expectRevert(loanToken.disableLoanParams([WRBTC.address], [false], { from: account2 }), "unauthorized"); - }); - - it("Should revert when calling disableLoanParams w/ mismatching parameters", async () => { - await expectRevert(loanToken.disableLoanParams([WRBTC.address, ZERO_ADDRESS], [false]), "count mismatch"); - }); - - it("test lend to the pool", async () => { - await lend_to_the_pool(loanToken, lender, SUSD, WRBTC, sovryn); - }); - - it("test cash out from the pool", async () => { - await cash_out_from_the_pool(loanToken, lender, SUSD, false); - }); - - it("test cash out from the pool more of lender balance should not fail", async () => { - // await cash_out_from_the_pool_more_of_lender_balance_should_not_fail(loanToken, lender, SUSD); - await cash_out_from_the_pool_uint256_max_should_withdraw_total_balance(loanToken, lender, SUSD); - }); - - it("should fail when minting on address(0) behalf", async () => { - let amount = new BN(1); - await SUSD.approve(loanToken.address, amount); - await expectRevert(loanToken.mint(constants.ZERO_ADDRESS, amount), "15"); - }); - - /// @dev For test coverage - it("should revert _prepareMinting when depositAmount is 0", async () => { - await lend_to_the_pool(loanToken, lender, SUSD, WRBTC, sovryn); - - await expectRevert(loanToken.mint(account2, 0), "17"); - }); - - it("test profit", async () => { - await lend_to_the_pool(loanToken, lender, SUSD, WRBTC, sovryn); - - // above function also opens a trading position, so I need to add some more funds to be able to withdraw everything - const balanceOf0 = await loanToken.assetBalanceOf(lender); - await SUSD.approve(loanToken.address, balanceOf0.toString()); - await loanToken.mint(account2, balanceOf0.toString()); - const profitBefore = await loanToken.profitOf(lender); - const iTokenBalance = await loanToken.balanceOf(lender); - - // burn everything -> profit should be 0 - await loanToken.burn(lender, iTokenBalance.toString()); - const profitInt = await loanToken.profitOf(lender); - - // lend again and wait some time -> profit should rise again, but less than before, because there are more funds in the pool. - await SUSD.approve(loanToken.address, balanceOf0.add(new BN(wei("100", "ether"))).toString()); - await loanToken.mint(lender, balanceOf0.toString()); - await SUSD.approve(loanToken.address, balanceOf0.add(new BN(wei("100", "ether"))).toString()); - - await increaseTime(10000); - - const profitAfter = await loanToken.profitOf(lender); - - expect(profitInt).to.be.a.bignumber.equal(new BN(0)); - expect(profitAfter.gt(new BN(0))).to.be.true; - expect(profitAfter.lt(profitBefore)).to.be.true; - }); - - /// @dev For test coverage - it("should revert _burnToken when depositAmount is 0", async () => { - await lend_to_the_pool(loanToken, lender, SUSD, WRBTC, sovryn); - - // above function also opens a trading position, so I need to add some more funds to be able to withdraw everything - const balanceOf0 = await loanToken.assetBalanceOf(lender); - await SUSD.approve(loanToken.address, balanceOf0.toString()); - await loanToken.mint(account2, balanceOf0.toString()); - - // Try to burn 0 - await expectRevert(loanToken.burn(lender, 0), "19"); - }); - - it("Check swapExternal with minReturn > 0 should revert if minReturn is not valid (higher)", async () => { - const balanceOf0 = await loanToken.assetBalanceOf(lender); - await SUSD.approve(sovryn.address, balanceOf0.add(new BN(wei("10", "ether"))).toString()); - await expectRevert( - sovryn.swapExternal(SUSD.address, WRBTC.address, accounts[0], accounts[0], wei("1", "ether"), 0, wei("10", "ether"), "0x"), - "destTokenAmountReceived too low" - ); - }); - - it("Check swapExternal with minReturn > 0 should revert if minReturn is valid", async () => { - const balanceOf0 = await loanToken.assetBalanceOf(lender); - await SUSD.approve(sovryn.address, balanceOf0.add(new BN(wei("10", "ether"))).toString()); - // feeds price is set 0.01, so test minReturn with 0.01 as well for the 1 ether swap - await sovryn.swapExternal( - SUSD.address, - WRBTC.address, - accounts[0], - accounts[0], - wei("1", "ether"), - 0, - wei("0.01", "ether"), - "0x" - ); - }); - - it("Should revert _swapsCall through swapExternal w/ non-empty loanDataBytes", async () => { - const balanceOf0 = await loanToken.assetBalanceOf(lender); - await SUSD.approve(sovryn.address, balanceOf0.add(new BN(wei("10", "ether"))).toString()); - - await expectRevert( - sovryn.swapExternal( - SUSD.address, - WRBTC.address, - accounts[0], - accounts[0], - wei("1", "ether"), - 0, - wei("0.01", "ether"), - "0x1" - ), - "invalid state" - ); - }); - }); - - describe("Test iRBTC withdrawal from LoanToken Contracts", () => { - it("test withdrawal from LoanToken contract", async () => { - await web3.eth.sendTransaction({ from: accounts[0].toString(), to: loanToken.address, value: 10000, gas: 22000 }); - const contractBalance = await web3.eth.getBalance(loanToken.address); - const balanceBefore = await web3.eth.getBalance(account4); - let tx = await loanToken.withdrawRBTCTo(account4, contractBalance); - expectEvent(tx, "WithdrawRBTCTo", { - to: account4, - amount: contractBalance, - }); - const balanceAfter = await web3.eth.getBalance(account4); - expect(new BN(balanceAfter).sub(new BN(balanceBefore))).to.be.a.bignumber.equal(new BN(contractBalance)); - }); - - it("shouldn't withdraw when zero address is passed", async () => { - await expectRevert(loanToken.withdrawRBTCTo(constants.ZERO_ADDRESS, 100), "receiver address invalid"); - }); - - it("shouldn't withdraw when triggered by anyone other than owner", async () => { - await expectRevert(loanToken.withdrawRBTCTo(account4, 100, { from: account4 }), "unauthorized"); - }); - - it("shouldn't withdraw if amount is 0", async () => { - await web3.eth.sendTransaction({ from: accounts[0].toString(), to: loanToken.address, value: 10000, gas: 22000 }); - await expectRevert(loanToken.withdrawRBTCTo(account4, 0), "non-zero withdraw amount expected"); - }); - - it("shouldn't withdraw if amount is invalid", async () => { - await web3.eth.sendTransaction({ from: accounts[0].toString(), to: loanToken.address, value: 10000, gas: 22000 }); - await expectRevert(loanToken.withdrawRBTCTo(account4, 20000), "withdraw amount cannot exceed balance"); - }); - }); + const name = "Test token"; + const symbol = "TST"; + + let lender, account1, account2, account3, account4; + let SUSD, WRBTC; + let sovryn, loanToken; + + async function deploymentAndInitFixture(_wallets, _provider) { + // Deploying sovrynProtocol w/ generic function from initializer.js + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + await sovryn.setSovrynProtocolAddress(sovryn.address); + + await sovryn.setWrbtcToken(WRBTC.address); + + feeds = await PriceFeedsLocal.new(WRBTC.address, sovryn.address); + await feeds.setRates(SUSD.address, WRBTC.address, wei("0.01", "ether")); + const swaps = await SwapsImplSovrynSwap.new(); + const sovrynSwapSimulator = await TestSovrynSwap.new(feeds.address); + await sovryn.setSovrynSwapContractRegistryAddress(sovrynSwapSimulator.address); + await sovryn.setSupportedTokens([SUSD.address, WRBTC.address], [true, true]); + await sovryn.setPriceFeedContract( + feeds.address // priceFeeds + ); + await sovryn.setSwapsImplContract( + swaps.address // swapsImpl + ); + await sovryn.setFeesController(lender); + + sov = await getSOV(sovryn, priceFeeds, SUSD, accounts); + + const initLoanTokenLogic = await getLoanTokenLogic(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] + loanTokenLogic = initLoanTokenLogic[0]; + loanTokenLogicBeacon = initLoanTokenLogic[1]; + + loanToken = await LoanToken.new( + lender, + loanTokenLogic.address, + sovryn.address, + WRBTC.address + ); + await loanToken.initialize(SUSD.address, name, symbol); // iToken + + const loanTokenAddress = await loanToken.loanTokenAddress(); + + params = [ + "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object + false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans + lender, // address owner; // owner of this object + SUSD.address, // address loanToken; // the token being loaned + WRBTC.address, // address collateralToken; // the required collateral token + wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin + wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value + 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) + ]; + + /** Initialize the loan token logic proxy */ + loanToken = await ILoanTokenLogicProxy.at(loanToken.address); + await loanToken.setBeaconAddress(loanTokenLogicBeacon.address); + + /** Use interface of LoanTokenModules */ + loanToken = await ILoanTokenModules.at(loanToken.address); + + await loanToken.setupLoanParams([params], false); + + if (lender == (await sovryn.owner())) + await sovryn.setLoanPool([loanToken.address], [loanTokenAddress]); + // const baseRate = wei("1", "ether"); + // const rateMultiplier = wei("20.25", "ether"); + // const targetLevel = wei("80", "ether"); + // const kinkLevel = wei("90", "ether"); + // const maxScaleRate = wei("100", "ether"); + // await loanToken.setDemandCurve(baseRate, rateMultiplier, baseRate, rateMultiplier, targetLevel, kinkLevel, maxScaleRate); + + await WRBTC.mint(sovryn.address, wei("500", "ether")); + } + + before(async () => { + [lender, account1, account2, account3, account4, ...accounts] = accounts; + }); + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + describe("Test lending using TestToken", () => { + it("Test disableLoanParams", async () => { + await loanToken.disableLoanParams([WRBTC.address], [false]); + }); + + it("Should revert when calling disableLoanParams by other than Admin", async () => { + await expectRevert( + loanToken.disableLoanParams([WRBTC.address], [false], { from: account2 }), + "unauthorized" + ); + }); + + it("Should revert when calling disableLoanParams w/ mismatching parameters", async () => { + await expectRevert( + loanToken.disableLoanParams([WRBTC.address, ZERO_ADDRESS], [false]), + "count mismatch" + ); + }); + + it("test lend to the pool", async () => { + await lend_to_the_pool(loanToken, lender, SUSD, WRBTC, sovryn); + }); + + it("test cash out from the pool", async () => { + await cash_out_from_the_pool(loanToken, lender, SUSD, false); + }); + + it("test cash out from the pool more of lender balance should not fail", async () => { + // await cash_out_from_the_pool_more_of_lender_balance_should_not_fail(loanToken, lender, SUSD); + await cash_out_from_the_pool_uint256_max_should_withdraw_total_balance( + loanToken, + lender, + SUSD + ); + }); + + it("should fail when minting on address(0) behalf", async () => { + let amount = new BN(1); + await SUSD.approve(loanToken.address, amount); + await expectRevert(loanToken.mint(constants.ZERO_ADDRESS, amount), "15"); + }); + + /// @dev For test coverage + it("should revert _prepareMinting when depositAmount is 0", async () => { + await lend_to_the_pool(loanToken, lender, SUSD, WRBTC, sovryn); + + await expectRevert(loanToken.mint(account2, 0), "17"); + }); + + it("test profit", async () => { + await lend_to_the_pool(loanToken, lender, SUSD, WRBTC, sovryn); + + // above function also opens a trading position, so I need to add some more funds to be able to withdraw everything + const balanceOf0 = await loanToken.assetBalanceOf(lender); + await SUSD.approve(loanToken.address, balanceOf0.toString()); + await loanToken.mint(account2, balanceOf0.toString()); + const profitBefore = await loanToken.profitOf(lender); + const iTokenBalance = await loanToken.balanceOf(lender); + + // burn everything -> profit should be 0 + await loanToken.burn(lender, iTokenBalance.toString()); + const profitInt = await loanToken.profitOf(lender); + + // lend again and wait some time -> profit should rise again, but less than before, because there are more funds in the pool. + await SUSD.approve( + loanToken.address, + balanceOf0.add(new BN(wei("100", "ether"))).toString() + ); + await loanToken.mint(lender, balanceOf0.toString()); + await SUSD.approve( + loanToken.address, + balanceOf0.add(new BN(wei("100", "ether"))).toString() + ); + + await increaseTime(10000); + + const profitAfter = await loanToken.profitOf(lender); + + expect(profitInt).to.be.a.bignumber.equal(new BN(0)); + expect(profitAfter.gt(new BN(0))).to.be.true; + expect(profitAfter.lt(profitBefore)).to.be.true; + }); + + /// @dev For test coverage + it("should revert _burnToken when depositAmount is 0", async () => { + await lend_to_the_pool(loanToken, lender, SUSD, WRBTC, sovryn); + + // above function also opens a trading position, so I need to add some more funds to be able to withdraw everything + const balanceOf0 = await loanToken.assetBalanceOf(lender); + await SUSD.approve(loanToken.address, balanceOf0.toString()); + await loanToken.mint(account2, balanceOf0.toString()); + + // Try to burn 0 + await expectRevert(loanToken.burn(lender, 0), "19"); + }); + + it("Check swapExternal with minReturn > 0 should revert if minReturn is not valid (higher)", async () => { + const balanceOf0 = await loanToken.assetBalanceOf(lender); + await SUSD.approve( + sovryn.address, + balanceOf0.add(new BN(wei("10", "ether"))).toString() + ); + await expectRevert( + sovryn.swapExternal( + SUSD.address, + WRBTC.address, + accounts[0], + accounts[0], + wei("1", "ether"), + 0, + wei("10", "ether"), + "0x" + ), + "destTokenAmountReceived too low" + ); + }); + + it("Check swapExternal with minReturn > 0 should revert if minReturn is valid", async () => { + const balanceOf0 = await loanToken.assetBalanceOf(lender); + await SUSD.approve( + sovryn.address, + balanceOf0.add(new BN(wei("10", "ether"))).toString() + ); + // feeds price is set 0.01, so test minReturn with 0.01 as well for the 1 ether swap + await sovryn.swapExternal( + SUSD.address, + WRBTC.address, + accounts[0], + accounts[0], + wei("1", "ether"), + 0, + wei("0.01", "ether"), + "0x" + ); + }); + + it("Should revert _swapsCall through swapExternal w/ non-empty loanDataBytes", async () => { + const balanceOf0 = await loanToken.assetBalanceOf(lender); + await SUSD.approve( + sovryn.address, + balanceOf0.add(new BN(wei("10", "ether"))).toString() + ); + + await expectRevert( + sovryn.swapExternal( + SUSD.address, + WRBTC.address, + accounts[0], + accounts[0], + wei("1", "ether"), + 0, + wei("0.01", "ether"), + "0x1" + ), + "invalid state" + ); + }); + }); + + describe("Test iRBTC withdrawal from LoanToken Contracts", () => { + it("test withdrawal from LoanToken contract", async () => { + await web3.eth.sendTransaction({ + from: accounts[0].toString(), + to: loanToken.address, + value: 10000, + gas: 22000, + }); + const contractBalance = await web3.eth.getBalance(loanToken.address); + const balanceBefore = await web3.eth.getBalance(account4); + let tx = await loanToken.withdrawRBTCTo(account4, contractBalance); + expectEvent(tx, "WithdrawRBTCTo", { + to: account4, + amount: contractBalance, + }); + const balanceAfter = await web3.eth.getBalance(account4); + expect(new BN(balanceAfter).sub(new BN(balanceBefore))).to.be.a.bignumber.equal( + new BN(contractBalance) + ); + }); + + it("shouldn't withdraw when zero address is passed", async () => { + await expectRevert( + loanToken.withdrawRBTCTo(constants.ZERO_ADDRESS, 100), + "receiver address invalid" + ); + }); + + it("shouldn't withdraw when triggered by anyone other than owner", async () => { + await expectRevert( + loanToken.withdrawRBTCTo(account4, 100, { from: account4 }), + "unauthorized" + ); + }); + + it("shouldn't withdraw if amount is 0", async () => { + await web3.eth.sendTransaction({ + from: accounts[0].toString(), + to: loanToken.address, + value: 10000, + gas: 22000, + }); + await expectRevert( + loanToken.withdrawRBTCTo(account4, 0), + "non-zero withdraw amount expected" + ); + }); + + it("shouldn't withdraw if amount is invalid", async () => { + await web3.eth.sendTransaction({ + from: accounts[0].toString(), + to: loanToken.address, + value: 10000, + gas: 22000, + }); + await expectRevert( + loanToken.withdrawRBTCTo(account4, 20000), + "withdraw amount cannot exceed balance" + ); + }); + }); }); diff --git a/tests/loan-token/LendingWithLM.test.js b/tests/loan-token/LendingWithLM.test.js index 55a8bfc99..895ebd9a8 100644 --- a/tests/loan-token/LendingWithLM.test.js +++ b/tests/loan-token/LendingWithLM.test.js @@ -27,262 +27,287 @@ const LiquidityMiningProxy = artifacts.require("LiquidityMiningProxy"); const LockedSOV = artifacts.require("LockedSOV"); // const { lend_to_the_pool, cash_out_from_the_pool, cash_out_from_the_pool_more_of_lender_balance_should_not_fail } = require("./helpers"); -const { lend_to_the_pool, cash_out_from_the_pool, cash_out_from_the_pool_uint256_max_should_withdraw_total_balance } = require("./helpers"); +const { + lend_to_the_pool, + cash_out_from_the_pool, + cash_out_from_the_pool_uint256_max_should_withdraw_total_balance, +} = require("./helpers"); const wei = web3.utils.toWei; const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getLoanTokenLogic, - getLoanToken, - getLoanTokenLogicWrbtc, - getLoanTokenWRBTC, - loan_pool_setup, - set_demand_curve, - getPriceFeeds, - getSovryn, - decodeLogs, - getSOV, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getLoanTokenLogic, + getLoanToken, + getLoanTokenLogicWrbtc, + getLoanTokenWRBTC, + loan_pool_setup, + set_demand_curve, + getPriceFeeds, + getSovryn, + decodeLogs, + getSOV, } = require("../Utils/initializer.js"); contract("LoanTokenLogicLM", (accounts) => { - const name = "Test token"; - const symbol = "TST"; - const depositAmount = new BN(wei("400", "ether")); - - let lender, account1, account2; - let SUSD, WRBTC, SOV; - let sovryn, loanToken, loanTokenWRBTC; - let liquidityMining; - let lockedSOVAdmins, lockedSOV; - - before(async () => { - [lender, account1, account2, ...accounts] = accounts; - await deployProtocol(); - await deployLoanTokens(); - await deployLiquidityMining(); - - await loanToken.setLiquidityMiningAddress(liquidityMining.address); - await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMining.address); - await liquidityMining.add(loanToken.address, 10, false); - await liquidityMining.add(loanTokenWRBTC.address, 10, true); - }); - - describe("Test lending with liquidity mining", () => { - it("Should lend to the pool and deposit the pool tokens at the liquidity mining contract", async () => { - // await lend_to_the_pool(loanToken, lender, SUSD, WRBTC, sovryn); - await SUSD.approve(loanToken.address, depositAmount); - const tx = await loanToken.mint(lender, depositAmount, true); - const userInfo = await liquidityMining.getUserInfo(loanToken.address, lender); - - // Expected: user pool token balance is 0, but balance of LM contract increased - expect(await loanToken.balanceOf(lender)).bignumber.equal("0"); - expect(userInfo.amount).bignumber.equal(depositAmount); - expect(await loanToken.totalSupply()).bignumber.equal(depositAmount); - - // Expect the Mint event to mention the lender - expectEvent(tx, "Mint", { - minter: lender, - tokenAmount: depositAmount, - assetAmount: depositAmount, - }); - - // Expect the AllowanceUpdate event triggered at LoanTokenLogicStandard::_internalTransferFrom - expectEvent(tx, "AllowanceUpdate", { owner: lender, spender: lender, valueBefore: depositAmount, valueAfter: new BN(0) }); - }); - - it("Should lend to the pool without depositing the pool tokens at the liquidity mining contract", async () => { - await SUSD.approve(loanToken.address, depositAmount); - const tx = await loanToken.mint(lender, depositAmount, false); - const userInfo = await liquidityMining.getUserInfo(loanToken.address, lender); - // expected: user pool token balance increased by the deposited amount, LM balance stays unchanged - expect(await loanToken.balanceOf(lender)).bignumber.equal(depositAmount); - expect(userInfo.amount).bignumber.equal(depositAmount); - expect(await loanToken.totalSupply()).bignumber.equal(depositAmount.mul(new BN("2"))); - }); - - it("Should remove the pool tokens from the liquidity mining pool and burn them", async () => { - let userInfo = await liquidityMining.getUserInfo(loanToken.address, lender); - const tx = await loanToken.burn(lender, userInfo.amount, true); - userInfo = await liquidityMining.getUserInfo(loanToken.address, lender); - // expected: user pool token balance stayed the same but LM balance is 0 - expect(await loanToken.balanceOf(lender)).bignumber.equal(depositAmount); - expect(userInfo.amount).bignumber.equal("0"); - expect(await loanToken.totalSupply()).bignumber.equal(depositAmount); - // expect the Burn event to mention the lender - expectEvent(tx, "Burn", { - burner: lender, - tokenAmount: depositAmount, - assetAmount: depositAmount, - }); - }); - - it("Should burn pool tokens without removing them from the LM pool", async () => { - await loanToken.burn(lender, depositAmount, false); - expect(await loanToken.balanceOf(lender)).bignumber.equal("0"); - expect(await loanToken.totalSupply()).bignumber.equal("0"); - }); - }); - - describe("Test WRBTC lending with liquidity mining", () => { - it("Should lend to the pool and deposit the pool tokens at the liquidity mining contract", async () => { - // await lend_to_the_pool(loanToken, lender, SUSD, WRBTC, sovryn); - const tx = await loanTokenWRBTC.mintWithBTC(lender, true, { value: depositAmount }); - const userInfo = await liquidityMining.getUserInfo(loanTokenWRBTC.address, lender); - // expected: user pool token balance is 0, but balance of LM contract increased - expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal("0"); - expect(userInfo.amount).bignumber.equal(depositAmount); - expect(await loanTokenWRBTC.totalSupply()).bignumber.equal(depositAmount); - // expect the Mint event to mention the lender - expectEvent(tx, "Mint", { - minter: lender, - tokenAmount: depositAmount, - assetAmount: depositAmount, - }); - }); - - it("Should lend to the pool without depositing the pool tokens at the liquidity mining contract", async () => { - await loanTokenWRBTC.mintWithBTC(lender, false, { value: depositAmount }); - const userInfo = await liquidityMining.getUserInfo(loanTokenWRBTC.address, lender); - // expected: user pool token balance increased by the deposited amount, LM balance stays unchanged - expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal(depositAmount); - expect(userInfo.amount).bignumber.equal(depositAmount); - expect(await loanTokenWRBTC.totalSupply()).bignumber.equal(depositAmount.mul(new BN("2"))); - }); - - it("Should remove the pool tokens from the liquidity mining pool and burn them", async () => { - let userInfo = await liquidityMining.getUserInfo(loanTokenWRBTC.address, lender); - const tx = await loanTokenWRBTC.burnToBTC(lender, userInfo.amount, true); - userInfo = await liquidityMining.getUserInfo(loanTokenWRBTC.address, lender); - // expected: user pool token balance stayed the same but LM balance is 0 - expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal(depositAmount); - expect(userInfo.amount).bignumber.equal("0"); - expect(await loanTokenWRBTC.totalSupply()).bignumber.equal(depositAmount); - // expect the Burn event to mention the lender - expectEvent(tx, "Burn", { - burner: lender, - tokenAmount: depositAmount, - assetAmount: depositAmount, - }); - }); - - it("Should burn pool tokens without removing them from the LM pool", async () => { - await loanTokenWRBTC.burnToBTC(lender, depositAmount, false); - expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal("0"); - expect(await loanTokenWRBTC.totalSupply()).bignumber.equal("0"); - }); - }); - - describe("Test setting the liquidity mining address", () => { - it("Should be able to set the liquidity mining address", async () => { - await loanToken.setLiquidityMiningAddress(account2); - expect(await loanToken.getLiquidityMiningAddress()).to.be.equal(account2); - }); - - it("Should fail to set the liquidity mining address with an unauthorized wallet", async () => { - await expectRevert(loanToken.setLiquidityMiningAddress(account2, { from: account1 }), "unauthorized"); - }); - }); - - async function deployLiquidityMining() { - lockedSOVAdmins = [lender, account1, account2]; - // account 1 is a dummy value for the vesting registry - lockedSOV = await LockedSOV.new(SOV.address, account1, 1, 10, lockedSOVAdmins); - - let liquidityMiningLogic = await LiquidityMiningLogic.new(); - let liquidityMiningProxy = await LiquidityMiningProxy.new(); - await liquidityMiningProxy.setImplementation(liquidityMiningLogic.address); - liquidityMining = await LiquidityMiningLogic.at(liquidityMiningProxy.address); - - // dummy settings - await liquidityMining.initialize(SOV.address, 10, 1, 1, account1, lockedSOV.address, 0); - } - - async function deployProtocol() { - // Deploying sovrynProtocol w/ generic function from initializer.js - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - await sovryn.setSovrynProtocolAddress(sovryn.address); - - // Custom tokens - SOV = await getSOV(sovryn, priceFeeds, SUSD, accounts); - - feeds = await PriceFeedsLocal.new(WRBTC.address, sovryn.address); - await feeds.setRates(SUSD.address, WRBTC.address, wei("0.01", "ether")); - await sovryn.setSupportedTokens([SUSD.address, WRBTC.address], [true, true]); - await sovryn.setFeesController(lender); - } - - async function deployLoanTokens() { - const initLoanTokenLogic = await getLoanTokenLogic(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] - loanTokenLogicLM = initLoanTokenLogic[0]; - loanTokenLogicBeaconLM = initLoanTokenLogic[1]; - - loanToken = await LoanToken.new(lender, loanTokenLogicLM.address, sovryn.address, WRBTC.address); - await loanToken.initialize(SUSD.address, name, symbol); //iToken - - /** Initialize the loan token logic proxy */ - loanToken = await ILoanTokenLogicProxy.at(loanToken.address); - await loanToken.setBeaconAddress(loanTokenLogicBeaconLM.address); - - /** Use interface of LoanTokenModules */ - loanToken = await ILoanTokenModules.at(loanToken.address); - - params = [ - "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object - false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans - lender, // address owner; // owner of this object - SUSD.address, // address loanToken; // the token being loaned - WRBTC.address, // address collateralToken; // the required collateral token - wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin - wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value - 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) - ]; - - await loanToken.setupLoanParams([params], false); - - const loanTokenAddress = await loanToken.loanTokenAddress(); - if (lender == (await sovryn.owner())) await sovryn.setLoanPool([loanToken.address], [loanTokenAddress]); - - // --------------- WRBTC -----------------------// - - const initLoanTokenLogicWrbtc = await getLoanTokenLogicWrbtc(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] - loanTokenLogicWrbtc = initLoanTokenLogicWrbtc[0]; - loanTokenLogicBeaconWrbtc = initLoanTokenLogicWrbtc[1]; - - loanTokenWRBTC = await LoanToken.new(lender, loanTokenLogicWrbtc.address, sovryn.address, WRBTC.address); - await loanTokenWRBTC.initialize(WRBTC.address, "iRBTC", "iRBTC"); - - /** Initialize the loan token logic proxy */ - loanTokenWRBTC = await ILoanTokenLogicProxy.at(loanTokenWRBTC.address); - await loanTokenWRBTC.setBeaconAddress(loanTokenLogicBeaconWrbtc.address); - - /** Use interface of LoanTokenModules */ - loanTokenWRBTC = await ILoanTokenModules.at(loanTokenWRBTC.address); - - params = [ - "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object - false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans - lender, // address owner; // owner of this object - WRBTC.address, // address loanToken; // the token being loaned - SUSD.address, // address collateralToken; // the required collateral token - wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin - wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value - 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) - ]; - - await loanTokenWRBTC.setupLoanParams([params], false); - await sovryn.setLoanPool([loanTokenWRBTC.address], [WRBTC.address]); - - // ---------------- SUPPLY FUNDS TO PROTOCOL ---------------------// - await WRBTC.mint(sovryn.address, wei("500", "ether")); - await SUSD.mint(sovryn.address, wei("50000", "ether")); - } + const name = "Test token"; + const symbol = "TST"; + const depositAmount = new BN(wei("400", "ether")); + + let lender, account1, account2; + let SUSD, WRBTC, SOV; + let sovryn, loanToken, loanTokenWRBTC; + let liquidityMining; + let lockedSOVAdmins, lockedSOV; + + before(async () => { + [lender, account1, account2, ...accounts] = accounts; + await deployProtocol(); + await deployLoanTokens(); + await deployLiquidityMining(); + + await loanToken.setLiquidityMiningAddress(liquidityMining.address); + await loanTokenWRBTC.setLiquidityMiningAddress(liquidityMining.address); + await liquidityMining.add(loanToken.address, 10, false); + await liquidityMining.add(loanTokenWRBTC.address, 10, true); + }); + + describe("Test lending with liquidity mining", () => { + it("Should lend to the pool and deposit the pool tokens at the liquidity mining contract", async () => { + // await lend_to_the_pool(loanToken, lender, SUSD, WRBTC, sovryn); + await SUSD.approve(loanToken.address, depositAmount); + const tx = await loanToken.mint(lender, depositAmount, true); + const userInfo = await liquidityMining.getUserInfo(loanToken.address, lender); + + // Expected: user pool token balance is 0, but balance of LM contract increased + expect(await loanToken.balanceOf(lender)).bignumber.equal("0"); + expect(userInfo.amount).bignumber.equal(depositAmount); + expect(await loanToken.totalSupply()).bignumber.equal(depositAmount); + + // Expect the Mint event to mention the lender + expectEvent(tx, "Mint", { + minter: lender, + tokenAmount: depositAmount, + assetAmount: depositAmount, + }); + + // Expect the AllowanceUpdate event triggered at LoanTokenLogicStandard::_internalTransferFrom + expectEvent(tx, "AllowanceUpdate", { + owner: lender, + spender: lender, + valueBefore: depositAmount, + valueAfter: new BN(0), + }); + }); + + it("Should lend to the pool without depositing the pool tokens at the liquidity mining contract", async () => { + await SUSD.approve(loanToken.address, depositAmount); + const tx = await loanToken.mint(lender, depositAmount, false); + const userInfo = await liquidityMining.getUserInfo(loanToken.address, lender); + // expected: user pool token balance increased by the deposited amount, LM balance stays unchanged + expect(await loanToken.balanceOf(lender)).bignumber.equal(depositAmount); + expect(userInfo.amount).bignumber.equal(depositAmount); + expect(await loanToken.totalSupply()).bignumber.equal(depositAmount.mul(new BN("2"))); + }); + + it("Should remove the pool tokens from the liquidity mining pool and burn them", async () => { + let userInfo = await liquidityMining.getUserInfo(loanToken.address, lender); + const tx = await loanToken.burn(lender, userInfo.amount, true); + userInfo = await liquidityMining.getUserInfo(loanToken.address, lender); + // expected: user pool token balance stayed the same but LM balance is 0 + expect(await loanToken.balanceOf(lender)).bignumber.equal(depositAmount); + expect(userInfo.amount).bignumber.equal("0"); + expect(await loanToken.totalSupply()).bignumber.equal(depositAmount); + // expect the Burn event to mention the lender + expectEvent(tx, "Burn", { + burner: lender, + tokenAmount: depositAmount, + assetAmount: depositAmount, + }); + }); + + it("Should burn pool tokens without removing them from the LM pool", async () => { + await loanToken.burn(lender, depositAmount, false); + expect(await loanToken.balanceOf(lender)).bignumber.equal("0"); + expect(await loanToken.totalSupply()).bignumber.equal("0"); + }); + }); + + describe("Test WRBTC lending with liquidity mining", () => { + it("Should lend to the pool and deposit the pool tokens at the liquidity mining contract", async () => { + // await lend_to_the_pool(loanToken, lender, SUSD, WRBTC, sovryn); + const tx = await loanTokenWRBTC.mintWithBTC(lender, true, { value: depositAmount }); + const userInfo = await liquidityMining.getUserInfo(loanTokenWRBTC.address, lender); + // expected: user pool token balance is 0, but balance of LM contract increased + expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal("0"); + expect(userInfo.amount).bignumber.equal(depositAmount); + expect(await loanTokenWRBTC.totalSupply()).bignumber.equal(depositAmount); + // expect the Mint event to mention the lender + expectEvent(tx, "Mint", { + minter: lender, + tokenAmount: depositAmount, + assetAmount: depositAmount, + }); + }); + + it("Should lend to the pool without depositing the pool tokens at the liquidity mining contract", async () => { + await loanTokenWRBTC.mintWithBTC(lender, false, { value: depositAmount }); + const userInfo = await liquidityMining.getUserInfo(loanTokenWRBTC.address, lender); + // expected: user pool token balance increased by the deposited amount, LM balance stays unchanged + expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal(depositAmount); + expect(userInfo.amount).bignumber.equal(depositAmount); + expect(await loanTokenWRBTC.totalSupply()).bignumber.equal( + depositAmount.mul(new BN("2")) + ); + }); + + it("Should remove the pool tokens from the liquidity mining pool and burn them", async () => { + let userInfo = await liquidityMining.getUserInfo(loanTokenWRBTC.address, lender); + const tx = await loanTokenWRBTC.burnToBTC(lender, userInfo.amount, true); + userInfo = await liquidityMining.getUserInfo(loanTokenWRBTC.address, lender); + // expected: user pool token balance stayed the same but LM balance is 0 + expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal(depositAmount); + expect(userInfo.amount).bignumber.equal("0"); + expect(await loanTokenWRBTC.totalSupply()).bignumber.equal(depositAmount); + // expect the Burn event to mention the lender + expectEvent(tx, "Burn", { + burner: lender, + tokenAmount: depositAmount, + assetAmount: depositAmount, + }); + }); + + it("Should burn pool tokens without removing them from the LM pool", async () => { + await loanTokenWRBTC.burnToBTC(lender, depositAmount, false); + expect(await loanTokenWRBTC.balanceOf(lender)).bignumber.equal("0"); + expect(await loanTokenWRBTC.totalSupply()).bignumber.equal("0"); + }); + }); + + describe("Test setting the liquidity mining address", () => { + it("Should be able to set the liquidity mining address", async () => { + await loanToken.setLiquidityMiningAddress(account2); + expect(await loanToken.getLiquidityMiningAddress()).to.be.equal(account2); + }); + + it("Should fail to set the liquidity mining address with an unauthorized wallet", async () => { + await expectRevert( + loanToken.setLiquidityMiningAddress(account2, { from: account1 }), + "unauthorized" + ); + }); + }); + + async function deployLiquidityMining() { + lockedSOVAdmins = [lender, account1, account2]; + // account 1 is a dummy value for the vesting registry + lockedSOV = await LockedSOV.new(SOV.address, account1, 1, 10, lockedSOVAdmins); + + let liquidityMiningLogic = await LiquidityMiningLogic.new(); + let liquidityMiningProxy = await LiquidityMiningProxy.new(); + await liquidityMiningProxy.setImplementation(liquidityMiningLogic.address); + liquidityMining = await LiquidityMiningLogic.at(liquidityMiningProxy.address); + + // dummy settings + await liquidityMining.initialize(SOV.address, 10, 1, 1, account1, lockedSOV.address, 0); + } + + async function deployProtocol() { + // Deploying sovrynProtocol w/ generic function from initializer.js + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + await sovryn.setSovrynProtocolAddress(sovryn.address); + + // Custom tokens + SOV = await getSOV(sovryn, priceFeeds, SUSD, accounts); + + feeds = await PriceFeedsLocal.new(WRBTC.address, sovryn.address); + await feeds.setRates(SUSD.address, WRBTC.address, wei("0.01", "ether")); + await sovryn.setSupportedTokens([SUSD.address, WRBTC.address], [true, true]); + await sovryn.setFeesController(lender); + } + + async function deployLoanTokens() { + const initLoanTokenLogic = await getLoanTokenLogic(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] + loanTokenLogicLM = initLoanTokenLogic[0]; + loanTokenLogicBeaconLM = initLoanTokenLogic[1]; + + loanToken = await LoanToken.new( + lender, + loanTokenLogicLM.address, + sovryn.address, + WRBTC.address + ); + await loanToken.initialize(SUSD.address, name, symbol); //iToken + + /** Initialize the loan token logic proxy */ + loanToken = await ILoanTokenLogicProxy.at(loanToken.address); + await loanToken.setBeaconAddress(loanTokenLogicBeaconLM.address); + + /** Use interface of LoanTokenModules */ + loanToken = await ILoanTokenModules.at(loanToken.address); + + params = [ + "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object + false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans + lender, // address owner; // owner of this object + SUSD.address, // address loanToken; // the token being loaned + WRBTC.address, // address collateralToken; // the required collateral token + wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin + wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value + 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) + ]; + + await loanToken.setupLoanParams([params], false); + + const loanTokenAddress = await loanToken.loanTokenAddress(); + if (lender == (await sovryn.owner())) + await sovryn.setLoanPool([loanToken.address], [loanTokenAddress]); + + // --------------- WRBTC -----------------------// + + const initLoanTokenLogicWrbtc = await getLoanTokenLogicWrbtc(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] + loanTokenLogicWrbtc = initLoanTokenLogicWrbtc[0]; + loanTokenLogicBeaconWrbtc = initLoanTokenLogicWrbtc[1]; + + loanTokenWRBTC = await LoanToken.new( + lender, + loanTokenLogicWrbtc.address, + sovryn.address, + WRBTC.address + ); + await loanTokenWRBTC.initialize(WRBTC.address, "iRBTC", "iRBTC"); + + /** Initialize the loan token logic proxy */ + loanTokenWRBTC = await ILoanTokenLogicProxy.at(loanTokenWRBTC.address); + await loanTokenWRBTC.setBeaconAddress(loanTokenLogicBeaconWrbtc.address); + + /** Use interface of LoanTokenModules */ + loanTokenWRBTC = await ILoanTokenModules.at(loanTokenWRBTC.address); + + params = [ + "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object + false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans + lender, // address owner; // owner of this object + WRBTC.address, // address loanToken; // the token being loaned + SUSD.address, // address collateralToken; // the required collateral token + wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin + wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value + 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) + ]; + + await loanTokenWRBTC.setupLoanParams([params], false); + await sovryn.setLoanPool([loanTokenWRBTC.address], [WRBTC.address]); + + // ---------------- SUPPLY FUNDS TO PROTOCOL ---------------------// + await WRBTC.mint(sovryn.address, wei("500", "ether")); + await SUSD.mint(sovryn.address, wei("50000", "ether")); + } }); diff --git a/tests/loan-token/LendingwRBTCCollateral.test.js b/tests/loan-token/LendingwRBTCCollateral.test.js index da2d802ba..66b16ba33 100644 --- a/tests/loan-token/LendingwRBTCCollateral.test.js +++ b/tests/loan-token/LendingwRBTCCollateral.test.js @@ -23,113 +23,127 @@ const ILoanTokenModules = artifacts.require("ILoanTokenModules"); const PriceFeedsLocal = artifacts.require("PriceFeedsLocal"); -const { lend_to_the_pool, cash_out_from_the_pool, cash_out_from_the_pool_uint256_max_should_withdraw_total_balance } = require("./helpers"); +const { + lend_to_the_pool, + cash_out_from_the_pool, + cash_out_from_the_pool_uint256_max_should_withdraw_total_balance, +} = require("./helpers"); const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getLoanTokenLogic, - getLoanToken, - getLoanTokenLogicWrbtc, - getLoanTokenWRBTC, - loan_pool_setup, - set_demand_curve, - getPriceFeeds, - getSovryn, - decodeLogs, - getSOV, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getLoanTokenLogic, + getLoanToken, + getLoanTokenLogicWrbtc, + getLoanTokenWRBTC, + loan_pool_setup, + set_demand_curve, + getPriceFeeds, + getSovryn, + decodeLogs, + getSOV, } = require("../Utils/initializer.js"); const wei = web3.utils.toWei; contract("LoanTokenLending", (accounts) => { - const name = "Test token"; - const symbol = "TST"; - - let lender; - let SUSD, RBTC, SOV; - let sovryn, loanToken; - - async function deploymentAndInitFixture(_wallets, _provider) { - // Deploying sovrynProtocol w/ generic function from initializer.js - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - await sovryn.setSovrynProtocolAddress(sovryn.address); - - feeds = await PriceFeedsLocal.new(RBTC.address, sovryn.address); - await feeds.setRates(SUSD.address, RBTC.address, wei("0.01", "ether")); - await sovryn.setSupportedTokens([SUSD.address, RBTC.address], [true, true]); - await sovryn.setFeesController(lender); - - // Custom tokens - SOV = await getSOV(sovryn, priceFeeds, SUSD, accounts); - - const initLoanTokenLogic = await getLoanTokenLogic(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] - loanTokenLogic = initLoanTokenLogic[0]; - loanTokenLogicBeacon = initLoanTokenLogic[1]; - - loanToken = await LoanToken.new(lender, loanTokenLogic.address, sovryn.address, RBTC.address); - await loanToken.initialize(SUSD.address, name, symbol); //iToken - - /** Initialize the loan token logic proxy */ - loanToken = await ILoanTokenLogicProxy.at(loanToken.address); - await loanToken.setBeaconAddress(loanTokenLogicBeacon.address); - - /** Use interface of LoanTokenModules */ - loanToken = await ILoanTokenModules.at(loanToken.address); - - params = [ - "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object - false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans - lender, // address owner; // owner of this object - SUSD.address, // address loanToken; // the token being loaned - RBTC.address, // address collateralToken; // the required collateral token - wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin - wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value - 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) - ]; - - await loanToken.setupLoanParams([params], false); - - const loanTokenAddress = await loanToken.loanTokenAddress(); - if (lender == (await sovryn.owner())) await sovryn.setLoanPool([loanToken.address], [loanTokenAddress]); - - // const baseRate = wei("1", "ether"); - // const rateMultiplier = wei("20.25", "ether"); - // const targetLevel = wei("80", "ether"); - // const kinkLevel = wei("90", "ether"); - // const maxScaleRate = wei("100", "ether"); - // await loanToken.setDemandCurve(baseRate, rateMultiplier, baseRate, rateMultiplier, targetLevel, kinkLevel, maxScaleRate); - - await RBTC.mint(sovryn.address, wei("500", "ether")); - } - - before(async () => { - [lender, ...accounts] = accounts; - }); - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - describe("test lending using wRBTC as collateral", () => { - it("test lend to the pool", async () => { - await lend_to_the_pool(loanToken, lender, SUSD, RBTC, sovryn); - }); - - it("test cash out from the pool", async () => { - await cash_out_from_the_pool(loanToken, lender, SUSD, false); - }); - - it("test cash out from the pool more of lender balance should not fail", async () => { - // await cash_out_from_the_pool_more_of_lender_balance_should_not_fail(loanToken, lender, SUSD); - await cash_out_from_the_pool_uint256_max_should_withdraw_total_balance(loanToken, lender, SUSD); - }); - }); + const name = "Test token"; + const symbol = "TST"; + + let lender; + let SUSD, RBTC, SOV; + let sovryn, loanToken; + + async function deploymentAndInitFixture(_wallets, _provider) { + // Deploying sovrynProtocol w/ generic function from initializer.js + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + await sovryn.setSovrynProtocolAddress(sovryn.address); + + feeds = await PriceFeedsLocal.new(RBTC.address, sovryn.address); + await feeds.setRates(SUSD.address, RBTC.address, wei("0.01", "ether")); + await sovryn.setSupportedTokens([SUSD.address, RBTC.address], [true, true]); + await sovryn.setFeesController(lender); + + // Custom tokens + SOV = await getSOV(sovryn, priceFeeds, SUSD, accounts); + + const initLoanTokenLogic = await getLoanTokenLogic(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] + loanTokenLogic = initLoanTokenLogic[0]; + loanTokenLogicBeacon = initLoanTokenLogic[1]; + + loanToken = await LoanToken.new( + lender, + loanTokenLogic.address, + sovryn.address, + RBTC.address + ); + await loanToken.initialize(SUSD.address, name, symbol); //iToken + + /** Initialize the loan token logic proxy */ + loanToken = await ILoanTokenLogicProxy.at(loanToken.address); + await loanToken.setBeaconAddress(loanTokenLogicBeacon.address); + + /** Use interface of LoanTokenModules */ + loanToken = await ILoanTokenModules.at(loanToken.address); + + params = [ + "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object + false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans + lender, // address owner; // owner of this object + SUSD.address, // address loanToken; // the token being loaned + RBTC.address, // address collateralToken; // the required collateral token + wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin + wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value + 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) + ]; + + await loanToken.setupLoanParams([params], false); + + const loanTokenAddress = await loanToken.loanTokenAddress(); + if (lender == (await sovryn.owner())) + await sovryn.setLoanPool([loanToken.address], [loanTokenAddress]); + + // const baseRate = wei("1", "ether"); + // const rateMultiplier = wei("20.25", "ether"); + // const targetLevel = wei("80", "ether"); + // const kinkLevel = wei("90", "ether"); + // const maxScaleRate = wei("100", "ether"); + // await loanToken.setDemandCurve(baseRate, rateMultiplier, baseRate, rateMultiplier, targetLevel, kinkLevel, maxScaleRate); + + await RBTC.mint(sovryn.address, wei("500", "ether")); + } + + before(async () => { + [lender, ...accounts] = accounts; + }); + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + describe("test lending using wRBTC as collateral", () => { + it("test lend to the pool", async () => { + await lend_to_the_pool(loanToken, lender, SUSD, RBTC, sovryn); + }); + + it("test cash out from the pool", async () => { + await cash_out_from_the_pool(loanToken, lender, SUSD, false); + }); + + it("test cash out from the pool more of lender balance should not fail", async () => { + // await cash_out_from_the_pool_more_of_lender_balance_should_not_fail(loanToken, lender, SUSD); + await cash_out_from_the_pool_uint256_max_should_withdraw_total_balance( + loanToken, + lender, + SUSD + ); + }); + }); }); diff --git a/tests/loan-token/LendingwRBTCloan.test.js b/tests/loan-token/LendingwRBTCloan.test.js index 11a80ba4b..8afc27272 100644 --- a/tests/loan-token/LendingwRBTCloan.test.js +++ b/tests/loan-token/LendingwRBTCloan.test.js @@ -24,197 +24,252 @@ const { waffle } = require("hardhat"); const { loadFixture } = waffle; const { expectRevert, expectEvent, constants, BN } = require("@openzeppelin/test-helpers"); const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getLoanTokenLogic, - getLoanToken, - getLoanTokenLogicWrbtc, - getLoanTokenWRBTC, - loan_pool_setup, - set_demand_curve, - getPriceFeeds, - getSovryn, - decodeLogs, - getSOV, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getLoanTokenLogic, + getLoanToken, + getLoanTokenLogicWrbtc, + getLoanTokenWRBTC, + loan_pool_setup, + set_demand_curve, + getPriceFeeds, + getSovryn, + decodeLogs, + getSOV, } = require("../Utils/initializer.js"); const LoanToken = artifacts.require("LoanToken"); const ILoanTokenLogicProxy = artifacts.require("ILoanTokenLogicProxy"); const ILoanTokenModules = artifacts.require("ILoanTokenModules"); -const { verify_start_conditions, verify_lending_result_and_itoken_price_change, cash_out_from_the_pool } = require("./helpers"); +const { + verify_start_conditions, + verify_lending_result_and_itoken_price_change, + cash_out_from_the_pool, +} = require("./helpers"); const wei = web3.utils.toWei; contract("LoanTokenLending", (accounts) => { - let lender, account1, account2, account3, account4; - let SUSD, WRBTC; - let sovryn, loanToken; - let baseRate; - - async function deploymentAndInitFixture(_wallets, _provider) { - // Deploying sovrynProtocol w/ generic function from initializer.js - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - await sovryn.setSovrynProtocolAddress(sovryn.address); - - await priceFeeds.setRates(SUSD.address, WRBTC.address, wei("0.01", "ether")); - await sovryn.setSupportedTokens([SUSD.address, WRBTC.address], [true, true]); - await sovryn.setFeesController(lender); - - // Custom tokens - await getSOV(sovryn, priceFeeds, SUSD, accounts); - - const initLoanTokenLogic = await getLoanTokenLogicWrbtc(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] - loanTokenLogicWrbtc = initLoanTokenLogic[0]; - loanTokenLogicBeaconWrbtc = initLoanTokenLogic[1]; - - loanToken = await LoanToken.new(lender, loanTokenLogicWrbtc.address, sovryn.address, WRBTC.address); - await loanToken.initialize(WRBTC.address, "iWRBTC", "iWRBTC"); // iToken - - /** Initialize the loan token logic proxy */ - loanToken = await ILoanTokenLogicProxy.at(loanToken.address); - await loanToken.setBeaconAddress(loanTokenLogicBeaconWrbtc.address); - - /** Use interface of LoanTokenModules */ - loanToken = await ILoanTokenModules.at(loanToken.address); - - params = [ - "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object - false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans - lender, // address owner; // owner of this object - WRBTC.address, // address loanToken; // the token being loaned - SUSD.address, // address collateralToken; // the required collateral token - wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin - wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value - 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) - ]; - - await loanToken.setupLoanParams([params], false); - await loanToken.setupLoanParams([params], true); - - const loanTokenAddress = await loanToken.loanTokenAddress(); - if (lender == (await sovryn.owner())) await sovryn.setLoanPool([loanToken.address], [loanTokenAddress]); - - await WRBTC.mint(sovryn.address, wei("500", "ether")); - - baseRate = wei("1", "ether"); - const rateMultiplier = wei("20.25", "ether"); - const targetLevel = wei("80", "ether"); - const kinkLevel = wei("90", "ether"); - const maxScaleRate = wei("100", "ether"); - - await loanToken.setDemandCurve(baseRate, rateMultiplier, baseRate, rateMultiplier, targetLevel, kinkLevel, maxScaleRate); - } - - before(async () => { - [lender, account1, account2, account3, account4, ...accounts] = accounts; - }); - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - describe("test lending using wRBTC as loanToken", () => { - it("Doesn't allow fallback function call", async () => { - await expectRevert(loanToken.sendTransaction({}), "target not active"); - }); - - it("test avgBorrowInterestRate() function", async () => { - expect(await loanToken.avgBorrowInterestRate()).to.be.a.bignumber.equal(new BN(0)); - }); - - it("test supplyInterestRate() function", async () => { - expect(await loanToken.supplyInterestRate()).to.be.a.bignumber.equal(new BN(0)); - }); - - it("test nextSupplyInterestRate() function", async () => { - const deposit_amount = new BN(1); - expect(await loanToken.nextSupplyInterestRate(deposit_amount)).to.be.a.bignumber.equal(new BN(0)); - }); - - it("test totalSupplyInterestRate() function", async () => { - const deposit_amount = new BN(1); - expect(await loanToken.totalSupplyInterestRate(deposit_amount)).to.be.a.bignumber.equal(new BN(0)); - }); - - it("test lend to the pool", async () => { - borrow_interest_rate = await loanToken.borrowInterestRate(); - expect(borrow_interest_rate.gt(baseRate)).to.be.true; - - const deposit_amount = new BN(wei("4", "ether")); - const loan_token_sent = new BN(wei("1", "ether")); - let initial_balance = new BN(0); - const actual_initial_balance = new BN(await web3.eth.getBalance(lender)); - - await verify_start_conditions(WRBTC, loanToken, lender, initial_balance, deposit_amount); - await loanToken.mintWithBTC(lender, false, { value: deposit_amount }); - - initial_balance = new BN(wei("5", "ether")); - await verify_lending_result_and_itoken_price_change( - WRBTC, - SUSD, - loanToken, - lender, - loan_token_sent, - deposit_amount, - sovryn, - true - ); - - const new_balance = new BN(await web3.eth.getBalance(lender)); - expect(new_balance.lt(actual_initial_balance)).to.be.true; - }); - - it("test cash out from the pool", async () => { - await cash_out_from_the_pool(loanToken, lender, WRBTC, true); - }); - - it("test cash out from the pool more of lender balance should not fail", async () => { - const total_deposit_amount = new BN(wei("200", "ether")); - await loanToken.mintWithBTC(lender, false, { value: total_deposit_amount.toString() }); - await expectRevert(loanToken.burnToBTC(lender, total_deposit_amount.mul(new BN(2)).toString(), false), "32"); - await loanToken.burnToBTC(lender, constants.MAX_UINT256, false); - expect(await loanToken.balanceOf(lender)).to.be.a.bignumber.equal(new BN(0)); - }); - }); - - describe("Test iRBTC withdrawal from RBTC loan token contract", () => { - it("test withdrawal from iRBTC contract", async () => { - await loanToken.mintWithBTC(lender, false, { value: 10000, gas: 22000 }); - const contractBalance = await web3.eth.getBalance(loanToken.address); - const balanceBefore = await web3.eth.getBalance(account1); - let tx = await loanToken.withdrawRBTCTo(account1, contractBalance); - expectEvent(tx, "WithdrawRBTCTo", { - to: account1, - amount: contractBalance, - }); - const balanceAfter = await web3.eth.getBalance(account1); - expect(new BN(balanceAfter).sub(new BN(balanceBefore))).to.be.a.bignumber.equal(new BN(contractBalance)); - }); - - it("shouldn't withdraw when zero address is passed", async () => { - await expectRevert(loanToken.withdrawRBTCTo(constants.ZERO_ADDRESS, 100), "receiver address invalid"); - }); - - it("shouldn't withdraw when triggered by anyone other than owner", async () => { - await expectRevert(loanToken.withdrawRBTCTo(account4, 100, { from: account4 }), "unauthorized"); - }); - - it("shouldn't withdraw if amount is 0", async () => { - await web3.eth.sendTransaction({ from: accounts[0].toString(), to: loanToken.address, value: 10000, gas: 22000 }); - await expectRevert(loanToken.withdrawRBTCTo(account4, 0), "non-zero withdraw amount expected"); - }); - - it("shouldn't withdraw if amount is invalid", async () => { - await web3.eth.sendTransaction({ from: accounts[0].toString(), to: loanToken.address, value: 10000, gas: 22000 }); - await expectRevert(loanToken.withdrawRBTCTo(account4, 20000), "withdraw amount cannot exceed balance"); - }); - }); + let lender, account1, account2, account3, account4; + let SUSD, WRBTC; + let sovryn, loanToken; + let baseRate; + + async function deploymentAndInitFixture(_wallets, _provider) { + // Deploying sovrynProtocol w/ generic function from initializer.js + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + await sovryn.setSovrynProtocolAddress(sovryn.address); + + await priceFeeds.setRates(SUSD.address, WRBTC.address, wei("0.01", "ether")); + await sovryn.setSupportedTokens([SUSD.address, WRBTC.address], [true, true]); + await sovryn.setFeesController(lender); + + // Custom tokens + await getSOV(sovryn, priceFeeds, SUSD, accounts); + + const initLoanTokenLogic = await getLoanTokenLogicWrbtc(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] + loanTokenLogicWrbtc = initLoanTokenLogic[0]; + loanTokenLogicBeaconWrbtc = initLoanTokenLogic[1]; + + loanToken = await LoanToken.new( + lender, + loanTokenLogicWrbtc.address, + sovryn.address, + WRBTC.address + ); + await loanToken.initialize(WRBTC.address, "iWRBTC", "iWRBTC"); // iToken + + /** Initialize the loan token logic proxy */ + loanToken = await ILoanTokenLogicProxy.at(loanToken.address); + await loanToken.setBeaconAddress(loanTokenLogicBeaconWrbtc.address); + + /** Use interface of LoanTokenModules */ + loanToken = await ILoanTokenModules.at(loanToken.address); + + params = [ + "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object + false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans + lender, // address owner; // owner of this object + WRBTC.address, // address loanToken; // the token being loaned + SUSD.address, // address collateralToken; // the required collateral token + wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin + wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value + 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) + ]; + + await loanToken.setupLoanParams([params], false); + await loanToken.setupLoanParams([params], true); + + const loanTokenAddress = await loanToken.loanTokenAddress(); + if (lender == (await sovryn.owner())) + await sovryn.setLoanPool([loanToken.address], [loanTokenAddress]); + + await WRBTC.mint(sovryn.address, wei("500", "ether")); + + baseRate = wei("1", "ether"); + const rateMultiplier = wei("20.25", "ether"); + const targetLevel = wei("80", "ether"); + const kinkLevel = wei("90", "ether"); + const maxScaleRate = wei("100", "ether"); + + await loanToken.setDemandCurve( + baseRate, + rateMultiplier, + baseRate, + rateMultiplier, + targetLevel, + kinkLevel, + maxScaleRate + ); + } + + before(async () => { + [lender, account1, account2, account3, account4, ...accounts] = accounts; + }); + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + describe("test lending using wRBTC as loanToken", () => { + it("Doesn't allow fallback function call", async () => { + await expectRevert(loanToken.sendTransaction({}), "target not active"); + }); + + it("test avgBorrowInterestRate() function", async () => { + expect(await loanToken.avgBorrowInterestRate()).to.be.a.bignumber.equal(new BN(0)); + }); + + it("test supplyInterestRate() function", async () => { + expect(await loanToken.supplyInterestRate()).to.be.a.bignumber.equal(new BN(0)); + }); + + it("test nextSupplyInterestRate() function", async () => { + const deposit_amount = new BN(1); + expect(await loanToken.nextSupplyInterestRate(deposit_amount)).to.be.a.bignumber.equal( + new BN(0) + ); + }); + + it("test totalSupplyInterestRate() function", async () => { + const deposit_amount = new BN(1); + expect( + await loanToken.totalSupplyInterestRate(deposit_amount) + ).to.be.a.bignumber.equal(new BN(0)); + }); + + it("test lend to the pool", async () => { + borrow_interest_rate = await loanToken.borrowInterestRate(); + expect(borrow_interest_rate.gt(baseRate)).to.be.true; + + const deposit_amount = new BN(wei("4", "ether")); + const loan_token_sent = new BN(wei("1", "ether")); + let initial_balance = new BN(0); + const actual_initial_balance = new BN(await web3.eth.getBalance(lender)); + + await verify_start_conditions( + WRBTC, + loanToken, + lender, + initial_balance, + deposit_amount + ); + await loanToken.mintWithBTC(lender, false, { value: deposit_amount }); + + initial_balance = new BN(wei("5", "ether")); + await verify_lending_result_and_itoken_price_change( + WRBTC, + SUSD, + loanToken, + lender, + loan_token_sent, + deposit_amount, + sovryn, + true + ); + + const new_balance = new BN(await web3.eth.getBalance(lender)); + expect(new_balance.lt(actual_initial_balance)).to.be.true; + }); + + it("test cash out from the pool", async () => { + await cash_out_from_the_pool(loanToken, lender, WRBTC, true); + }); + + it("test cash out from the pool more of lender balance should not fail", async () => { + const total_deposit_amount = new BN(wei("200", "ether")); + await loanToken.mintWithBTC(lender, false, { value: total_deposit_amount.toString() }); + await expectRevert( + loanToken.burnToBTC(lender, total_deposit_amount.mul(new BN(2)).toString(), false), + "32" + ); + await loanToken.burnToBTC(lender, constants.MAX_UINT256, false); + expect(await loanToken.balanceOf(lender)).to.be.a.bignumber.equal(new BN(0)); + }); + }); + + describe("Test iRBTC withdrawal from RBTC loan token contract", () => { + it("test withdrawal from iRBTC contract", async () => { + await loanToken.mintWithBTC(lender, false, { value: 10000, gas: 22000 }); + const contractBalance = await web3.eth.getBalance(loanToken.address); + const balanceBefore = await web3.eth.getBalance(account1); + let tx = await loanToken.withdrawRBTCTo(account1, contractBalance); + expectEvent(tx, "WithdrawRBTCTo", { + to: account1, + amount: contractBalance, + }); + const balanceAfter = await web3.eth.getBalance(account1); + expect(new BN(balanceAfter).sub(new BN(balanceBefore))).to.be.a.bignumber.equal( + new BN(contractBalance) + ); + }); + + it("shouldn't withdraw when zero address is passed", async () => { + await expectRevert( + loanToken.withdrawRBTCTo(constants.ZERO_ADDRESS, 100), + "receiver address invalid" + ); + }); + + it("shouldn't withdraw when triggered by anyone other than owner", async () => { + await expectRevert( + loanToken.withdrawRBTCTo(account4, 100, { from: account4 }), + "unauthorized" + ); + }); + + it("shouldn't withdraw if amount is 0", async () => { + await web3.eth.sendTransaction({ + from: accounts[0].toString(), + to: loanToken.address, + value: 10000, + gas: 22000, + }); + await expectRevert( + loanToken.withdrawRBTCTo(account4, 0), + "non-zero withdraw amount expected" + ); + }); + + it("shouldn't withdraw if amount is invalid", async () => { + await web3.eth.sendTransaction({ + from: accounts[0].toString(), + to: loanToken.address, + value: 10000, + gas: 22000, + }); + await expectRevert( + loanToken.withdrawRBTCTo(account4, 20000), + "withdraw amount cannot exceed balance" + ); + }); + }); }); diff --git a/tests/loan-token/LoanTokenBeacon.test.js b/tests/loan-token/LoanTokenBeacon.test.js index 1421a6b51..4324a04ff 100644 --- a/tests/loan-token/LoanTokenBeacon.test.js +++ b/tests/loan-token/LoanTokenBeacon.test.js @@ -7,209 +7,294 @@ const LoanTokenSettingsLowerAdmin = artifacts.require("LoanTokenSettingsLowerAdm const LoanTokenLogicLMV1Mockup = artifacts.require("LoanTokenLogicLMV1Mockup"); const LoanTokenLogicLMV2Mockup = artifacts.require("LoanTokenLogicLMV2Mockup"); -const { getSUSD, getRBTC, getWRBTC, getBZRX, getLoanTokenLogic, getPriceFeeds, getSovryn, CONSTANTS } = require("../Utils/initializer.js"); +const { + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getLoanTokenLogic, + getPriceFeeds, + getSovryn, + CONSTANTS, +} = require("../Utils/initializer.js"); contract("LoanTokenLogicBeacon", (accounts) => { - let owner; - let account1; - let loanTokenLogicBeacon; - let sovryn; - - before(async () => { - [owner, account1] = accounts; - }); - - beforeEach(async () => { - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - const priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - - loanTokenLogicBeacon = await LoanTokenLogicBeacon.new(); - }); - - describe("Loan Token Logic Beacon", () => { - it("Register module with non-admin address should fail", async () => { - await expectRevert(loanTokenLogicBeacon.registerLoanTokenModule(CONSTANTS.ZERO_ADDRESS, { from: account1 }), "unauthorized"); - }); - - it("Register module with 0 address should fail", async () => { - await expectRevert( - loanTokenLogicBeacon.registerLoanTokenModule(CONSTANTS.ZERO_ADDRESS), - "LoanTokenModuleAddress is not a contract" - ); - }); - - it("Cannot get implementation address in pause mode", async () => { - // Pause loan token logic beacon - await loanTokenLogicBeacon.pause(); - const sig1 = web3.eth.abi.encodeFunctionSignature("testFunction1"); - - await expectRevert(loanTokenLogicBeacon.getTarget(sig1), "LoanTokenLogicBeacon:paused mode"); - }); - - it("Update the module address", async () => { - const initLoanTokenLogic = await getLoanTokenLogic(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] - loanTokenLogic = initLoanTokenLogic[0]; - loanTokenLogicBeacon = initLoanTokenLogic[1]; - - // Validate the current active module index - loanTokenSettingsLowerAdmin = await LoanTokenSettingsLowerAdmin.new(); - loanTokenLogicLM = await LoanTokenLogicLM.new(); - - const listSigsLowerAdmin = await loanTokenSettingsLowerAdmin.getListFunctionSignatures(); - const listSigsLM = await loanTokenLogicLM.getListFunctionSignatures(); - const moduleNameLowerSettings = listSigsLowerAdmin[1]; - const moduleNameLM = listSigsLM[1]; - - const prevLoanTokenLogicLowerAdminAddress = await loanTokenLogicBeacon.getTarget( - web3.eth.abi.encodeFunctionSignature("setAdmin(address)") - ); - const prevLoanTokenLogicLMAddress = await loanTokenLogicBeacon.getTarget( - web3.eth.abi.encodeFunctionSignature("borrowInterestRate()") - ); - - expect((await loanTokenLogicBeacon.activeModuleIndex(moduleNameLowerSettings)).toString()).to.equal(new BN(0).toString()); - expect((await loanTokenLogicBeacon.activeModuleIndex(moduleNameLM)).toString()).to.equal(new BN(0).toString()); - - // Double check all module for lower settings - for (let i = 0; i < listSigsLowerAdmin[0].length; i++) { - expect((await loanTokenLogicBeacon.getActiveFuncSignatureList(moduleNameLowerSettings))[i]).to.be.equal( - listSigsLowerAdmin[0][i] - ); - } - - // Double check all module for LM - for (let i = 0; i < listSigsLM[0].length; i++) { - expect((await loanTokenLogicBeacon.getActiveFuncSignatureList(moduleNameLM))[i]).to.be.equal(listSigsLM[0][i]); - } - - expect((await loanTokenLogicBeacon.getModuleUpgradeLogLength(moduleNameLowerSettings)).toString()).to.equal( - new BN(1).toString() - ); - expect((await loanTokenLogicBeacon.getModuleUpgradeLogLength(moduleNameLM)).toString()).to.equal(new BN(1).toString()); - - const log1LowerAdmin = await loanTokenLogicBeacon.moduleUpgradeLog(moduleNameLowerSettings, 0); - const log1LM = await loanTokenLogicBeacon.moduleUpgradeLog(moduleNameLM, 0); - - expect(log1LowerAdmin[0]).to.equal(prevLoanTokenLogicLowerAdminAddress); - expect(log1LM[0]).to.equal(prevLoanTokenLogicLMAddress); - - await expectRevert(loanTokenLogicBeacon.moduleUpgradeLog(moduleNameLowerSettings, 1), "invalid opcode"); - await expectRevert(loanTokenLogicBeacon.moduleUpgradeLog(moduleNameLM, 1), "invalid opcode"); - - const sig1 = web3.eth.abi.encodeFunctionSignature("testFunction1"); - - expect(await loanTokenLogicBeacon.getTarget(sig1)).to.equal(CONSTANTS.ZERO_ADDRESS); - - /** Register New Loan Token Logic LM to the Beacon */ - loanTokenLogicLM = await LoanTokenLogicLMV1Mockup.new(); - await loanTokenLogicBeacon.registerLoanTokenModule(loanTokenLogicLM.address); - - // The totalSupply function signature should not be exist in this v1 mockup - expect(await loanTokenLogicBeacon.getTarget(web3.eth.abi.encodeFunctionSignature("totalSupply()"))).to.equal( - CONSTANTS.ZERO_ADDRESS - ); - - expect((await loanTokenLogicBeacon.activeModuleIndex(moduleNameLM)).toString()).to.equal(new BN(1).toString()); - - expect((await loanTokenLogicBeacon.getModuleUpgradeLogLength(moduleNameLM)).toString()).to.equal(new BN(2).toString()); - - const log2LM = await loanTokenLogicBeacon.moduleUpgradeLog(moduleNameLM, 1); - expect(log2LM[0]).to.equal(loanTokenLogicLM.address); - - expect(await loanTokenLogicBeacon.getTarget(web3.eth.abi.encodeFunctionSignature("borrowInterestRate()"))).to.equal( - loanTokenLogicLM.address - ); - }); - - it("Rollback the module address", async () => { - const initLoanTokenLogic = await getLoanTokenLogic(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] - loanTokenLogic = initLoanTokenLogic[0]; - loanTokenLogicBeacon = initLoanTokenLogic[1]; - - // Validate the current active module index - loanTokenSettingsLowerAdmin = await LoanTokenSettingsLowerAdmin.new(); - loanTokenLogicLM = await LoanTokenLogicLM.new(); - - const listSigsLowerAdmin = await loanTokenSettingsLowerAdmin.getListFunctionSignatures(); - const listSigsLM = await loanTokenLogicLM.getListFunctionSignatures(); - const moduleNameLowerSettings = listSigsLowerAdmin[1]; - const moduleNameLM = listSigsLM[1]; - - const prevLoanTokenLogicLowerAdminAddress = await loanTokenLogicBeacon.getTarget( - web3.eth.abi.encodeFunctionSignature("setAdmin(address)") - ); - const prevLoanTokenLogicLMAddress = await loanTokenLogicBeacon.getTarget( - web3.eth.abi.encodeFunctionSignature("borrowInterestRate()") - ); - - expect((await loanTokenLogicBeacon.activeModuleIndex(moduleNameLowerSettings)).toString()).to.equal(new BN(0).toString()); - expect((await loanTokenLogicBeacon.activeModuleIndex(moduleNameLM)).toString()).to.equal(new BN(0).toString()); - - expect((await loanTokenLogicBeacon.getModuleUpgradeLogLength(moduleNameLowerSettings)).toString()).to.equal( - new BN(1).toString() - ); - expect((await loanTokenLogicBeacon.getModuleUpgradeLogLength(moduleNameLM)).toString()).to.equal(new BN(1).toString()); - - const log1LowerAdmin = await loanTokenLogicBeacon.moduleUpgradeLog(moduleNameLowerSettings, 0); - const log1LM = await loanTokenLogicBeacon.moduleUpgradeLog(moduleNameLM, 0); - - expect(log1LowerAdmin[0]).to.equal(prevLoanTokenLogicLowerAdminAddress); - expect(log1LM[0]).to.equal(prevLoanTokenLogicLMAddress); - - await expectRevert(loanTokenLogicBeacon.moduleUpgradeLog(moduleNameLowerSettings, 1), "invalid opcode"); - await expectRevert(loanTokenLogicBeacon.moduleUpgradeLog(moduleNameLM, 1), "invalid opcode"); - - // There should not be testNewFunction signature registered in the real LM - expect(await loanTokenLogicBeacon.getTarget(web3.eth.abi.encodeFunctionSignature("testNewFunction()"))).to.equal( - CONSTANTS.ZERO_ADDRESS - ); - - /** Register New Loan Token Logic LM to the Beacon */ - loanTokenLogicLM = await LoanTokenLogicLMV2Mockup.new(); - await loanTokenLogicBeacon.registerLoanTokenModule(loanTokenLogicLM.address); - - // There should be testNewFunction signature registered in v2Mockup - expect(await loanTokenLogicBeacon.getTarget(web3.eth.abi.encodeFunctionSignature("testNewFunction()"))).to.equal( - loanTokenLogicLM.address - ); - - expect((await loanTokenLogicBeacon.activeModuleIndex(moduleNameLM)).toString()).to.equal(new BN(1).toString()); - - expect((await loanTokenLogicBeacon.getModuleUpgradeLogLength(moduleNameLM)).toString()).to.equal(new BN(2).toString()); - - const log2LM = await loanTokenLogicBeacon.moduleUpgradeLog(moduleNameLM, 1); - expect(log2LM[0]).to.equal(loanTokenLogicLM.address); - - expect(await loanTokenLogicBeacon.getTarget(web3.eth.abi.encodeFunctionSignature("borrowInterestRate()"))).to.equal( - loanTokenLogicLM.address - ); - - /** Rollback */ - await loanTokenLogicBeacon.rollback(moduleNameLM, 0); - expect((await loanTokenLogicBeacon.activeModuleIndex(moduleNameLM)).toString()).to.equal(new BN(0).toString()); - expect(await loanTokenLogicBeacon.getTarget(web3.eth.abi.encodeFunctionSignature("borrowInterestRate()"))).to.equal(log1LM[0]); - - /// After rolledback, the testNewFunction signature should not be exist anymore - expect(await loanTokenLogicBeacon.getTarget(web3.eth.abi.encodeFunctionSignature("testNewFunction()"))).to.equal( - CONSTANTS.ZERO_ADDRESS - ); - }); - - it("Registering module without getListFunctionSignatures() in the target should fail", async () => { - const initLoanTokenLogic = await getLoanTokenLogic(true); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] - loanTokenLogic = initLoanTokenLogic[0]; - loanTokenLogicBeacon = initLoanTokenLogic[1]; - - /** Register New Loan Token Logic LM to the Beacon */ - await expectRevert( - loanTokenLogicBeacon.registerLoanTokenModule(SUSD.address), - "function selector was not recognized and there's no fallback function" - ); - }); - }); + let owner; + let account1; + let loanTokenLogicBeacon; + let sovryn; + + before(async () => { + [owner, account1] = accounts; + }); + + beforeEach(async () => { + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + const priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + + loanTokenLogicBeacon = await LoanTokenLogicBeacon.new(); + }); + + describe("Loan Token Logic Beacon", () => { + it("Register module with non-admin address should fail", async () => { + await expectRevert( + loanTokenLogicBeacon.registerLoanTokenModule(CONSTANTS.ZERO_ADDRESS, { + from: account1, + }), + "unauthorized" + ); + }); + + it("Register module with 0 address should fail", async () => { + await expectRevert( + loanTokenLogicBeacon.registerLoanTokenModule(CONSTANTS.ZERO_ADDRESS), + "LoanTokenModuleAddress is not a contract" + ); + }); + + it("Cannot get implementation address in pause mode", async () => { + // Pause loan token logic beacon + await loanTokenLogicBeacon.pause(); + const sig1 = web3.eth.abi.encodeFunctionSignature("testFunction1"); + + await expectRevert( + loanTokenLogicBeacon.getTarget(sig1), + "LoanTokenLogicBeacon:paused mode" + ); + }); + + it("Update the module address", async () => { + const initLoanTokenLogic = await getLoanTokenLogic(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] + loanTokenLogic = initLoanTokenLogic[0]; + loanTokenLogicBeacon = initLoanTokenLogic[1]; + + // Validate the current active module index + loanTokenSettingsLowerAdmin = await LoanTokenSettingsLowerAdmin.new(); + loanTokenLogicLM = await LoanTokenLogicLM.new(); + + const listSigsLowerAdmin = + await loanTokenSettingsLowerAdmin.getListFunctionSignatures(); + const listSigsLM = await loanTokenLogicLM.getListFunctionSignatures(); + const moduleNameLowerSettings = listSigsLowerAdmin[1]; + const moduleNameLM = listSigsLM[1]; + + const prevLoanTokenLogicLowerAdminAddress = await loanTokenLogicBeacon.getTarget( + web3.eth.abi.encodeFunctionSignature("setAdmin(address)") + ); + const prevLoanTokenLogicLMAddress = await loanTokenLogicBeacon.getTarget( + web3.eth.abi.encodeFunctionSignature("borrowInterestRate()") + ); + + expect( + (await loanTokenLogicBeacon.activeModuleIndex(moduleNameLowerSettings)).toString() + ).to.equal(new BN(0).toString()); + expect( + (await loanTokenLogicBeacon.activeModuleIndex(moduleNameLM)).toString() + ).to.equal(new BN(0).toString()); + + // Double check all module for lower settings + for (let i = 0; i < listSigsLowerAdmin[0].length; i++) { + expect( + ( + await loanTokenLogicBeacon.getActiveFuncSignatureList( + moduleNameLowerSettings + ) + )[i] + ).to.be.equal(listSigsLowerAdmin[0][i]); + } + + // Double check all module for LM + for (let i = 0; i < listSigsLM[0].length; i++) { + expect( + (await loanTokenLogicBeacon.getActiveFuncSignatureList(moduleNameLM))[i] + ).to.be.equal(listSigsLM[0][i]); + } + + expect( + ( + await loanTokenLogicBeacon.getModuleUpgradeLogLength(moduleNameLowerSettings) + ).toString() + ).to.equal(new BN(1).toString()); + expect( + (await loanTokenLogicBeacon.getModuleUpgradeLogLength(moduleNameLM)).toString() + ).to.equal(new BN(1).toString()); + + const log1LowerAdmin = await loanTokenLogicBeacon.moduleUpgradeLog( + moduleNameLowerSettings, + 0 + ); + const log1LM = await loanTokenLogicBeacon.moduleUpgradeLog(moduleNameLM, 0); + + expect(log1LowerAdmin[0]).to.equal(prevLoanTokenLogicLowerAdminAddress); + expect(log1LM[0]).to.equal(prevLoanTokenLogicLMAddress); + + await expectRevert( + loanTokenLogicBeacon.moduleUpgradeLog(moduleNameLowerSettings, 1), + "invalid opcode" + ); + await expectRevert( + loanTokenLogicBeacon.moduleUpgradeLog(moduleNameLM, 1), + "invalid opcode" + ); + + const sig1 = web3.eth.abi.encodeFunctionSignature("testFunction1"); + + expect(await loanTokenLogicBeacon.getTarget(sig1)).to.equal(CONSTANTS.ZERO_ADDRESS); + + /** Register New Loan Token Logic LM to the Beacon */ + loanTokenLogicLM = await LoanTokenLogicLMV1Mockup.new(); + await loanTokenLogicBeacon.registerLoanTokenModule(loanTokenLogicLM.address); + + // The totalSupply function signature should not be exist in this v1 mockup + expect( + await loanTokenLogicBeacon.getTarget( + web3.eth.abi.encodeFunctionSignature("totalSupply()") + ) + ).to.equal(CONSTANTS.ZERO_ADDRESS); + + expect( + (await loanTokenLogicBeacon.activeModuleIndex(moduleNameLM)).toString() + ).to.equal(new BN(1).toString()); + + expect( + (await loanTokenLogicBeacon.getModuleUpgradeLogLength(moduleNameLM)).toString() + ).to.equal(new BN(2).toString()); + + const log2LM = await loanTokenLogicBeacon.moduleUpgradeLog(moduleNameLM, 1); + expect(log2LM[0]).to.equal(loanTokenLogicLM.address); + + expect( + await loanTokenLogicBeacon.getTarget( + web3.eth.abi.encodeFunctionSignature("borrowInterestRate()") + ) + ).to.equal(loanTokenLogicLM.address); + }); + + it("Rollback the module address", async () => { + const initLoanTokenLogic = await getLoanTokenLogic(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] + loanTokenLogic = initLoanTokenLogic[0]; + loanTokenLogicBeacon = initLoanTokenLogic[1]; + + // Validate the current active module index + loanTokenSettingsLowerAdmin = await LoanTokenSettingsLowerAdmin.new(); + loanTokenLogicLM = await LoanTokenLogicLM.new(); + + const listSigsLowerAdmin = + await loanTokenSettingsLowerAdmin.getListFunctionSignatures(); + const listSigsLM = await loanTokenLogicLM.getListFunctionSignatures(); + const moduleNameLowerSettings = listSigsLowerAdmin[1]; + const moduleNameLM = listSigsLM[1]; + + const prevLoanTokenLogicLowerAdminAddress = await loanTokenLogicBeacon.getTarget( + web3.eth.abi.encodeFunctionSignature("setAdmin(address)") + ); + const prevLoanTokenLogicLMAddress = await loanTokenLogicBeacon.getTarget( + web3.eth.abi.encodeFunctionSignature("borrowInterestRate()") + ); + + expect( + (await loanTokenLogicBeacon.activeModuleIndex(moduleNameLowerSettings)).toString() + ).to.equal(new BN(0).toString()); + expect( + (await loanTokenLogicBeacon.activeModuleIndex(moduleNameLM)).toString() + ).to.equal(new BN(0).toString()); + + expect( + ( + await loanTokenLogicBeacon.getModuleUpgradeLogLength(moduleNameLowerSettings) + ).toString() + ).to.equal(new BN(1).toString()); + expect( + (await loanTokenLogicBeacon.getModuleUpgradeLogLength(moduleNameLM)).toString() + ).to.equal(new BN(1).toString()); + + const log1LowerAdmin = await loanTokenLogicBeacon.moduleUpgradeLog( + moduleNameLowerSettings, + 0 + ); + const log1LM = await loanTokenLogicBeacon.moduleUpgradeLog(moduleNameLM, 0); + + expect(log1LowerAdmin[0]).to.equal(prevLoanTokenLogicLowerAdminAddress); + expect(log1LM[0]).to.equal(prevLoanTokenLogicLMAddress); + + await expectRevert( + loanTokenLogicBeacon.moduleUpgradeLog(moduleNameLowerSettings, 1), + "invalid opcode" + ); + await expectRevert( + loanTokenLogicBeacon.moduleUpgradeLog(moduleNameLM, 1), + "invalid opcode" + ); + + // There should not be testNewFunction signature registered in the real LM + expect( + await loanTokenLogicBeacon.getTarget( + web3.eth.abi.encodeFunctionSignature("testNewFunction()") + ) + ).to.equal(CONSTANTS.ZERO_ADDRESS); + + /** Register New Loan Token Logic LM to the Beacon */ + loanTokenLogicLM = await LoanTokenLogicLMV2Mockup.new(); + await loanTokenLogicBeacon.registerLoanTokenModule(loanTokenLogicLM.address); + + // There should be testNewFunction signature registered in v2Mockup + expect( + await loanTokenLogicBeacon.getTarget( + web3.eth.abi.encodeFunctionSignature("testNewFunction()") + ) + ).to.equal(loanTokenLogicLM.address); + + expect( + (await loanTokenLogicBeacon.activeModuleIndex(moduleNameLM)).toString() + ).to.equal(new BN(1).toString()); + + expect( + (await loanTokenLogicBeacon.getModuleUpgradeLogLength(moduleNameLM)).toString() + ).to.equal(new BN(2).toString()); + + const log2LM = await loanTokenLogicBeacon.moduleUpgradeLog(moduleNameLM, 1); + expect(log2LM[0]).to.equal(loanTokenLogicLM.address); + + expect( + await loanTokenLogicBeacon.getTarget( + web3.eth.abi.encodeFunctionSignature("borrowInterestRate()") + ) + ).to.equal(loanTokenLogicLM.address); + + /** Rollback */ + await loanTokenLogicBeacon.rollback(moduleNameLM, 0); + expect( + (await loanTokenLogicBeacon.activeModuleIndex(moduleNameLM)).toString() + ).to.equal(new BN(0).toString()); + expect( + await loanTokenLogicBeacon.getTarget( + web3.eth.abi.encodeFunctionSignature("borrowInterestRate()") + ) + ).to.equal(log1LM[0]); + + /// After rolledback, the testNewFunction signature should not be exist anymore + expect( + await loanTokenLogicBeacon.getTarget( + web3.eth.abi.encodeFunctionSignature("testNewFunction()") + ) + ).to.equal(CONSTANTS.ZERO_ADDRESS); + }); + + it("Registering module without getListFunctionSignatures() in the target should fail", async () => { + const initLoanTokenLogic = await getLoanTokenLogic(true); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] + loanTokenLogic = initLoanTokenLogic[0]; + loanTokenLogicBeacon = initLoanTokenLogic[1]; + + /** Register New Loan Token Logic LM to the Beacon */ + await expectRevert( + loanTokenLogicBeacon.registerLoanTokenModule(SUSD.address), + "function selector was not recognized and there's no fallback function" + ); + }); + }); }); diff --git a/tests/loan-token/LoanTokenTest.js b/tests/loan-token/LoanTokenTest.js index f02aabaaa..f4c649d5b 100644 --- a/tests/loan-token/LoanTokenTest.js +++ b/tests/loan-token/LoanTokenTest.js @@ -14,20 +14,20 @@ const { constants, expectRevert, BN } = require("@openzeppelin/test-helpers"); const { ZERO_ADDRESS } = require("@openzeppelin/test-helpers/src/constants"); const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getLoanTokenLogic, - getLoanToken, - getLoanTokenLogicWrbtc, - getLoanTokenWRBTC, - loan_pool_setup, - set_demand_curve, - getPriceFeeds, - getSovryn, - decodeLogs, - getSOV, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getLoanTokenLogic, + getLoanToken, + getLoanTokenLogicWrbtc, + getLoanTokenWRBTC, + loan_pool_setup, + set_demand_curve, + getPriceFeeds, + getSovryn, + decodeLogs, + getSOV, } = require("../Utils/initializer.js"); const GovernorAlpha = artifacts.require("GovernorAlphaMockup"); @@ -46,119 +46,145 @@ const TestCoverage = artifacts.require("TestCoverage"); const TWO_DAYS = 86400 * 2; contract("LoanTokenUpgrade", (accounts) => { - const name = "Test token"; - const symbol = "TST"; - - let root, account1, account2, account3, account4; - let staking, gov, timelock; - let loanTokenSettings, loanToken, SUSD; - - before(async () => { - [root, ...accounts] = accounts; - }); - - /// @dev In case more tests were being added to this file, - /// the beforeEach hook should be calling a fixture - /// to avoid repeated deployments. - beforeEach(async () => { - // Deploying sovrynProtocol w/ generic function from initializer.js - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - await sovryn.setSovrynProtocolAddress(sovryn.address); - - // Staking - let stakingLogic = await StakingLogic.new(SUSD.address); - staking = await StakingProxy.new(SUSD.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingLogic.at(staking.address); - - // Governor - timelock = await Timelock.new(root, TWO_DAYS); - gov = await GovernorAlpha.new(timelock.address, staking.address, root, 4, 0); - await timelock.harnessSetAdmin(gov.address); - - // Settings - loanTokenSettings = await PreviousLoanTokenSettings.new(); - loanToken = await PreviousLoanToken.new(root, loanTokenSettings.address, loanTokenSettings.address, SUSD.address); - loanToken = await PreviousLoanTokenSettings.at(loanToken.address); - - await sovryn.transferOwnership(timelock.address); - }); - - describe("change settings", () => { - it("admin field should be readable", async () => { - let previousSovrynContractAddress = await loanToken.sovrynContractAddress(); - let previousWrbtcTokenAddress = await loanToken.wrbtcTokenAddress(); - - let newLoanTokenSettings = await LoanTokenSettings.new(); - - let loanTokenProxy = await PreviousLoanToken.at(loanToken.address); - await loanTokenProxy.setTarget(newLoanTokenSettings.address); - - loanToken = await LoanTokenSettings.at(loanToken.address); - - // check that previous admin is address(0) - let admin = await loanToken.admin(); - assert.equal(admin, constants.ZERO_ADDRESS); - - // await expectRevert(loanToken.changeLoanTokenNameAndSymbol("newName", "newSymbol", { from: account1 }), "unauthorized"); - - // change admin - await loanToken.setAdmin(root); - - admin = await loanToken.admin(); - assert.equal(admin, root); - - // await loanToken.changeLoanTokenNameAndSymbol("newName", "newSymbol"); - - let sovrynContractAddress = await loanToken.sovrynContractAddress(); - let wrbtcTokenAddress = await loanToken.wrbtcTokenAddress(); - - assert.equal(sovrynContractAddress, previousSovrynContractAddress); - assert.equal(wrbtcTokenAddress, previousWrbtcTokenAddress); - }); - }); - - describe("Test coverage for LoanToken.sol", () => { - it("Call constructor w/ target not a contract", async () => { - await expectRevert(LoanToken.new(root, ZERO_ADDRESS, loanTokenSettings.address, SUSD.address), "target not a contract"); - }); - it("Call constructor w/ protocol not a contract", async () => { - await expectRevert(LoanToken.new(root, loanTokenSettings.address, ZERO_ADDRESS, SUSD.address), "sovryn not a contract"); - }); - it("Call constructor w/ wrbtc not a contract", async () => { - await expectRevert( - LoanToken.new(root, loanTokenSettings.address, loanTokenSettings.address, ZERO_ADDRESS), - "wrbtc not a contract" - ); - }); - it("Call LoanToken::setTarget", async () => { - let newLloanToken = await LoanToken.new(root, loanTokenSettings.address, loanTokenSettings.address, SUSD.address); - let newLoanTokenSettings = await LoanTokenSettings.new(); - await newLloanToken.setTarget(newLoanTokenSettings.address); - }); - }); - - describe("Test coverage for AdvancedToken::_mint", () => { - it("Call _mint w/ address 0 as receiver", async () => { - testCoverage = await TestCoverage.new(); - let tokenAmount = new BN(1); - let assetAmount = new BN(1); - let price = new BN(1); - await expectRevert(testCoverage.testMint(ZERO_ADDRESS, tokenAmount, assetAmount, price), "15"); - }); - }); - - describe("Test coverage for LoanTokenLogicStorage::stringToBytes32", () => { - it("stringToBytes32 when tempEmptyStringTest.length == 0", async () => { - testCoverage = await TestCoverage.new(); - let result = await testCoverage.testStringToBytes32(""); - // console.log("result: ", result); - expect(result).to.be.equal("0x0000000000000000000000000000000000000000000000000000000000000000"); - }); - }); + const name = "Test token"; + const symbol = "TST"; + + let root, account1, account2, account3, account4; + let staking, gov, timelock; + let loanTokenSettings, loanToken, SUSD; + + before(async () => { + [root, ...accounts] = accounts; + }); + + /// @dev In case more tests were being added to this file, + /// the beforeEach hook should be calling a fixture + /// to avoid repeated deployments. + beforeEach(async () => { + // Deploying sovrynProtocol w/ generic function from initializer.js + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + await sovryn.setSovrynProtocolAddress(sovryn.address); + + // Staking + let stakingLogic = await StakingLogic.new(SUSD.address); + staking = await StakingProxy.new(SUSD.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); + + // Governor + timelock = await Timelock.new(root, TWO_DAYS); + gov = await GovernorAlpha.new(timelock.address, staking.address, root, 4, 0); + await timelock.harnessSetAdmin(gov.address); + + // Settings + loanTokenSettings = await PreviousLoanTokenSettings.new(); + loanToken = await PreviousLoanToken.new( + root, + loanTokenSettings.address, + loanTokenSettings.address, + SUSD.address + ); + loanToken = await PreviousLoanTokenSettings.at(loanToken.address); + + await sovryn.transferOwnership(timelock.address); + }); + + describe("change settings", () => { + it("admin field should be readable", async () => { + let previousSovrynContractAddress = await loanToken.sovrynContractAddress(); + let previousWrbtcTokenAddress = await loanToken.wrbtcTokenAddress(); + + let newLoanTokenSettings = await LoanTokenSettings.new(); + + let loanTokenProxy = await PreviousLoanToken.at(loanToken.address); + await loanTokenProxy.setTarget(newLoanTokenSettings.address); + + loanToken = await LoanTokenSettings.at(loanToken.address); + + // check that previous admin is address(0) + let admin = await loanToken.admin(); + assert.equal(admin, constants.ZERO_ADDRESS); + + // await expectRevert(loanToken.changeLoanTokenNameAndSymbol("newName", "newSymbol", { from: account1 }), "unauthorized"); + + // change admin + await loanToken.setAdmin(root); + + admin = await loanToken.admin(); + assert.equal(admin, root); + + // await loanToken.changeLoanTokenNameAndSymbol("newName", "newSymbol"); + + let sovrynContractAddress = await loanToken.sovrynContractAddress(); + let wrbtcTokenAddress = await loanToken.wrbtcTokenAddress(); + + assert.equal(sovrynContractAddress, previousSovrynContractAddress); + assert.equal(wrbtcTokenAddress, previousWrbtcTokenAddress); + }); + }); + + describe("Test coverage for LoanToken.sol", () => { + it("Call constructor w/ target not a contract", async () => { + await expectRevert( + LoanToken.new(root, ZERO_ADDRESS, loanTokenSettings.address, SUSD.address), + "target not a contract" + ); + }); + it("Call constructor w/ protocol not a contract", async () => { + await expectRevert( + LoanToken.new(root, loanTokenSettings.address, ZERO_ADDRESS, SUSD.address), + "sovryn not a contract" + ); + }); + it("Call constructor w/ wrbtc not a contract", async () => { + await expectRevert( + LoanToken.new( + root, + loanTokenSettings.address, + loanTokenSettings.address, + ZERO_ADDRESS + ), + "wrbtc not a contract" + ); + }); + it("Call LoanToken::setTarget", async () => { + let newLloanToken = await LoanToken.new( + root, + loanTokenSettings.address, + loanTokenSettings.address, + SUSD.address + ); + let newLoanTokenSettings = await LoanTokenSettings.new(); + await newLloanToken.setTarget(newLoanTokenSettings.address); + }); + }); + + describe("Test coverage for AdvancedToken::_mint", () => { + it("Call _mint w/ address 0 as receiver", async () => { + testCoverage = await TestCoverage.new(); + let tokenAmount = new BN(1); + let assetAmount = new BN(1); + let price = new BN(1); + await expectRevert( + testCoverage.testMint(ZERO_ADDRESS, tokenAmount, assetAmount, price), + "15" + ); + }); + }); + + describe("Test coverage for LoanTokenLogicStorage::stringToBytes32", () => { + it("stringToBytes32 when tempEmptyStringTest.length == 0", async () => { + testCoverage = await TestCoverage.new(); + let result = await testCoverage.testStringToBytes32(""); + // console.log("result: ", result); + expect(result).to.be.equal( + "0x0000000000000000000000000000000000000000000000000000000000000000" + ); + }); + }); }); diff --git a/tests/loan-token/TradingTestToken.test.js b/tests/loan-token/TradingTestToken.test.js index 3b3734a04..c05b9029b 100644 --- a/tests/loan-token/TradingTestToken.test.js +++ b/tests/loan-token/TradingTestToken.test.js @@ -19,39 +19,39 @@ const { loadFixture } = waffle; const { expectRevert, BN } = require("@openzeppelin/test-helpers"); const { - margin_trading_sending_loan_tokens, - margin_trading_sov_reward_payment, - margin_trading_sov_reward_payment_with_special_rebates, - margin_trading_sending_collateral_tokens, - margin_trading_sending_collateral_tokens_sov_reward_payment, - margin_trading_sending_collateral_tokens_sov_reward_payment_with_special_rebates, - close_complete_margin_trade, - close_complete_margin_trade_sov_reward_payment, - close_complete_margin_trade_sov_reward_payment_with_special_rebates, - close_partial_margin_trade, - close_partial_margin_trade_sov_reward_payment, - close_partial_margin_trade_sov_reward_payment_with_special_rebates, + margin_trading_sending_loan_tokens, + margin_trading_sov_reward_payment, + margin_trading_sov_reward_payment_with_special_rebates, + margin_trading_sending_collateral_tokens, + margin_trading_sending_collateral_tokens_sov_reward_payment, + margin_trading_sending_collateral_tokens_sov_reward_payment_with_special_rebates, + close_complete_margin_trade, + close_complete_margin_trade_sov_reward_payment, + close_complete_margin_trade_sov_reward_payment_with_special_rebates, + close_partial_margin_trade, + close_partial_margin_trade_sov_reward_payment, + close_partial_margin_trade_sov_reward_payment_with_special_rebates, } = require("./tradingFunctions"); const FeesEvents = artifacts.require("FeesEvents"); const LoanOpenings = artifacts.require("LoanOpenings"); const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getSOV, - getLoanTokenLogic, - getLoanToken, - getLoanTokenWRBTC, - loan_pool_setup, - set_demand_curve, - lend_to_pool, - getPriceFeeds, - getSovryn, - open_margin_trade_position, - decodeLogs, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getSOV, + getLoanTokenLogic, + getLoanToken, + getLoanTokenWRBTC, + loan_pool_setup, + set_demand_curve, + lend_to_pool, + getPriceFeeds, + getSovryn, + open_margin_trade_position, + decodeLogs, } = require("../Utils/initializer.js"); const { ZERO_ADDRESS, ZERO_BYTES32 } = require("@openzeppelin/test-helpers/src/constants"); @@ -62,41 +62,41 @@ const hunEth = new BN(wei("100", "ether")); const TINY_AMOUNT = new BN(25).mul(new BN(10).pow(new BN(13))); // 25 * 10**13 contract("LoanTokenTrading", (accounts) => { - let owner; - let sovryn, SUSD, WRBTC, RBTC, BZRX, loanToken, loanTokenWRBTC, SOV, priceFeeds; + let owner; + let sovryn, SUSD, WRBTC, RBTC, BZRX, loanToken, loanTokenWRBTC, SOV, priceFeeds; - async function deploymentAndInitFixture(_wallets, _provider) { - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + async function deploymentAndInitFixture(_wallets, _provider) { + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - loanToken = await getLoanToken(owner, sovryn, WRBTC, SUSD, true); - loanTokenWRBTC = await getLoanTokenWRBTC(owner, sovryn, WRBTC, SUSD, true); - await loan_pool_setup(sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC); + loanToken = await getLoanToken(owner, sovryn, WRBTC, SUSD, true); + loanTokenWRBTC = await getLoanTokenWRBTC(owner, sovryn, WRBTC, SUSD, true); + await loan_pool_setup(sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC); - SOV = await getSOV(sovryn, priceFeeds, SUSD, accounts); - } + SOV = await getSOV(sovryn, priceFeeds, SUSD, accounts); + } - before(async () => { - [owner] = accounts; - }); + before(async () => { + [owner] = accounts; + }); - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); - describe("Test the loan token trading logic with 2 TestTokens.", () => { - // verifies that the loan token address is set on the contract - it("Test loanAddress", async () => { - const loanTokenAddress = await loanToken.loanTokenAddress(); - expect(loanTokenAddress).to.equal(SUSD.address); - }); + describe("Test the loan token trading logic with 2 TestTokens.", () => { + // verifies that the loan token address is set on the contract + it("Test loanAddress", async () => { + const loanTokenAddress = await loanToken.loanTokenAddress(); + expect(loanTokenAddress).to.equal(SUSD.address); + }); - /* + /* tests margin trading sending loan tokens. process is handled by the shared function margin_trading_sending_loan_tokens 1. approve the transfer @@ -104,154 +104,186 @@ contract("LoanTokenTrading", (accounts) => { 3. verify the trade event and balances are correct 4. retrieve the loan from the smart contract and make sure all values are set as expected */ - it("Test margin trading sending loan tokens", async () => { - await expectRevert( - loanToken.marginTrade( - ZERO_BYTES32, // loanId (0 for new loans) - oneEth.toString(), // leverageAmount - oneEth.toString(), // loanTokenSent - "0", // no collateral token sent - WRBTC.address, // collateralTokenAddress - owner, // trader, - 0, // slippage - "0x" // loanDataBytes (only required with ether) - ), - "principal too small" - ); - - await priceFeeds.setRates(WRBTC.address, SUSD.address, new BN(10).pow(new BN(20)).toString()); - await priceFeeds.setRates(RBTC.address, SUSD.address, new BN(10).pow(new BN(20)).toString()); - - await margin_trading_sending_loan_tokens(accounts, sovryn, loanToken, SUSD, RBTC, priceFeeds, false); - await margin_trading_sov_reward_payment(accounts, loanToken, SUSD, RBTC, SOV, FeesEvents, sovryn); - await margin_trading_sov_reward_payment_with_special_rebates(accounts, loanToken, SUSD, RBTC, SOV, FeesEvents, sovryn); - }); - - it("Test margin trading sending loan tokens tiny amount", async () => { - // Send the transaction - await expectRevert( - loanToken.marginTrade( - "0x0", // loanId (0 for new loans) - new BN(2).mul(oneEth), // leverageAmount - TINY_AMOUNT, // loanTokenSent - new BN(0), // no collateral token sent - RBTC.address, // collateralTokenAddress - accounts[0], // trader, - 0, // slippage - "0x", // loanDataBytes (only required with ether) - { from: accounts[2] } - ), - "principal too small" - ); - }); - - /* + it("Test margin trading sending loan tokens", async () => { + await expectRevert( + loanToken.marginTrade( + ZERO_BYTES32, // loanId (0 for new loans) + oneEth.toString(), // leverageAmount + oneEth.toString(), // loanTokenSent + "0", // no collateral token sent + WRBTC.address, // collateralTokenAddress + owner, // trader, + 0, // slippage + "0x" // loanDataBytes (only required with ether) + ), + "principal too small" + ); + + await priceFeeds.setRates( + WRBTC.address, + SUSD.address, + new BN(10).pow(new BN(20)).toString() + ); + await priceFeeds.setRates( + RBTC.address, + SUSD.address, + new BN(10).pow(new BN(20)).toString() + ); + + await margin_trading_sending_loan_tokens( + accounts, + sovryn, + loanToken, + SUSD, + RBTC, + priceFeeds, + false + ); + await margin_trading_sov_reward_payment( + accounts, + loanToken, + SUSD, + RBTC, + SOV, + FeesEvents, + sovryn + ); + await margin_trading_sov_reward_payment_with_special_rebates( + accounts, + loanToken, + SUSD, + RBTC, + SOV, + FeesEvents, + sovryn + ); + }); + + it("Test margin trading sending loan tokens tiny amount", async () => { + // Send the transaction + await expectRevert( + loanToken.marginTrade( + "0x0", // loanId (0 for new loans) + new BN(2).mul(oneEth), // leverageAmount + TINY_AMOUNT, // loanTokenSent + new BN(0), // no collateral token sent + RBTC.address, // collateralTokenAddress + accounts[0], // trader, + 0, // slippage + "0x", // loanDataBytes (only required with ether) + { from: accounts[2] } + ), + "principal too small" + ); + }); + + /* tests margin trading sending collateral tokens as collateral. process: 1. send the margin trade tx with the passed parameter (NOTE: the token transfer needs to be approved already) 2. TODO verify the trade event and balances are correct */ - it("Test margin trading sending collateral tokens", async () => { - const loanSize = new BN(10000).mul(oneEth); - await SUSD.mint(loanToken.address, loanSize.mul(new BN(12))); - - // address loanToken, address collateralToken, uint256 newPrincipal,uint256 marginAmount, bool isTorqueLoan - const collateralTokenSent = await sovryn.getRequiredCollateral( - SUSD.address, //loanToken - RBTC.address, //collateralToken - loanSize.mul(new BN(2)), //newPrincipal loanSize*2 ethers - new BN(50).mul(oneEth), //leverage amount (5x leverage) - false //false means we are trading (true if borrowing) - the loan is stored and processed in the loanToken contract - ); - - await RBTC.mint(accounts[0], collateralTokenSent); - await RBTC.mint(accounts[2], collateralTokenSent); - // important! WRBTC is being held by the loanToken contract itself, all other tokens are transfered directly from - // the sender and need approval - await RBTC.approve(loanToken.address, collateralTokenSent); - await RBTC.approve(loanToken.address, collateralTokenSent, { from: accounts[2] }); - - const leverageAmount = new BN(5).mul(oneEth); // 5x leverage - const value = 0; - await margin_trading_sending_collateral_tokens( - accounts, - loanToken, - SUSD, - RBTC, - loanSize, - collateralTokenSent, - leverageAmount, - value, - priceFeeds - ); - await margin_trading_sending_collateral_tokens_sov_reward_payment( - accounts[2], - loanToken, - SUSD, - RBTC, - collateralTokenSent, - leverageAmount, - value, - FeesEvents, - SOV, - sovryn - ); - }); - - /* + it("Test margin trading sending collateral tokens", async () => { + const loanSize = new BN(10000).mul(oneEth); + await SUSD.mint(loanToken.address, loanSize.mul(new BN(12))); + + // address loanToken, address collateralToken, uint256 newPrincipal,uint256 marginAmount, bool isTorqueLoan + const collateralTokenSent = await sovryn.getRequiredCollateral( + SUSD.address, //loanToken + RBTC.address, //collateralToken + loanSize.mul(new BN(2)), //newPrincipal loanSize*2 ethers + new BN(50).mul(oneEth), //leverage amount (5x leverage) + false //false means we are trading (true if borrowing) - the loan is stored and processed in the loanToken contract + ); + + await RBTC.mint(accounts[0], collateralTokenSent); + await RBTC.mint(accounts[2], collateralTokenSent); + // important! WRBTC is being held by the loanToken contract itself, all other tokens are transfered directly from + // the sender and need approval + await RBTC.approve(loanToken.address, collateralTokenSent); + await RBTC.approve(loanToken.address, collateralTokenSent, { from: accounts[2] }); + + const leverageAmount = new BN(5).mul(oneEth); // 5x leverage + const value = 0; + await margin_trading_sending_collateral_tokens( + accounts, + loanToken, + SUSD, + RBTC, + loanSize, + collateralTokenSent, + leverageAmount, + value, + priceFeeds + ); + await margin_trading_sending_collateral_tokens_sov_reward_payment( + accounts[2], + loanToken, + SUSD, + RBTC, + collateralTokenSent, + leverageAmount, + value, + FeesEvents, + SOV, + sovryn + ); + }); + + /* tests margin trading sending collateral tokens as collateral. process: 1. send the margin trade tx with the passed parameter (NOTE: the token transfer needs to be approved already) 2. TODO verify the trade event and balances are correct */ - it("Test margin trading sending collateral tokens with special rebates", async () => { - const loanSize = new BN(10000).mul(oneEth); - await SUSD.mint(loanToken.address, loanSize.mul(new BN(12))); - - // address loanToken, address collateralToken, uint256 newPrincipal,uint256 marginAmount, bool isTorqueLoan - const collateralTokenSent = await sovryn.getRequiredCollateral( - SUSD.address, //loanToken - RBTC.address, //collateralToken - loanSize.mul(new BN(2)), //newPrincipal loanSize*2 ethers - new BN(50).mul(oneEth), //leverage amount (5x leverage) - false //false means we are trading (true if borrowing) - the loan is stored and processed in the loanToken contract - ); - - await RBTC.mint(accounts[0], collateralTokenSent); - await RBTC.mint(accounts[2], collateralTokenSent); - // important! WRBTC is being held by the loanToken contract itself, all other tokens are transfered directly from - // the sender and need approval - await RBTC.approve(loanToken.address, collateralTokenSent); - await RBTC.approve(loanToken.address, collateralTokenSent, { from: accounts[2] }); - - const leverageAmount = new BN(5).mul(oneEth); // 5x leverage - const value = 0; - await margin_trading_sending_collateral_tokens( - accounts, - loanToken, - SUSD, - RBTC, - loanSize, - collateralTokenSent, - leverageAmount, - value, - priceFeeds - ); - - await margin_trading_sending_collateral_tokens_sov_reward_payment_with_special_rebates( - accounts[2], - loanToken, - SUSD, - RBTC, - collateralTokenSent, - leverageAmount, - value, - FeesEvents, - SOV, - sovryn - ); - }); - /* + it("Test margin trading sending collateral tokens with special rebates", async () => { + const loanSize = new BN(10000).mul(oneEth); + await SUSD.mint(loanToken.address, loanSize.mul(new BN(12))); + + // address loanToken, address collateralToken, uint256 newPrincipal,uint256 marginAmount, bool isTorqueLoan + const collateralTokenSent = await sovryn.getRequiredCollateral( + SUSD.address, //loanToken + RBTC.address, //collateralToken + loanSize.mul(new BN(2)), //newPrincipal loanSize*2 ethers + new BN(50).mul(oneEth), //leverage amount (5x leverage) + false //false means we are trading (true if borrowing) - the loan is stored and processed in the loanToken contract + ); + + await RBTC.mint(accounts[0], collateralTokenSent); + await RBTC.mint(accounts[2], collateralTokenSent); + // important! WRBTC is being held by the loanToken contract itself, all other tokens are transfered directly from + // the sender and need approval + await RBTC.approve(loanToken.address, collateralTokenSent); + await RBTC.approve(loanToken.address, collateralTokenSent, { from: accounts[2] }); + + const leverageAmount = new BN(5).mul(oneEth); // 5x leverage + const value = 0; + await margin_trading_sending_collateral_tokens( + accounts, + loanToken, + SUSD, + RBTC, + loanSize, + collateralTokenSent, + leverageAmount, + value, + priceFeeds + ); + + await margin_trading_sending_collateral_tokens_sov_reward_payment_with_special_rebates( + accounts[2], + loanToken, + SUSD, + RBTC, + collateralTokenSent, + leverageAmount, + value, + FeesEvents, + SOV, + sovryn + ); + }); + /* should completely close a position. first with returning loan tokens, then with returning collateral tokens to the sender. process is handled by the shared function close_complete_margin_trade @@ -261,467 +293,512 @@ contract("LoanTokenTrading", (accounts) => { 4. sends the closing tx from the trader 5. verifies the result */ - it("Test close complete margin trade", async () => { - await close_complete_margin_trade( - sovryn, - loanToken, - set_demand_curve, - lend_to_pool, - open_margin_trade_position, - priceFeeds, - true, - RBTC, - WRBTC, - SUSD, - accounts - ); - await close_complete_margin_trade( - sovryn, - loanToken, - set_demand_curve, - lend_to_pool, - open_margin_trade_position, - priceFeeds, - false, - RBTC, - WRBTC, - SUSD, - accounts - ); - }); - - it("Test close complete margin trade sov reward payment", async () => { - await close_complete_margin_trade_sov_reward_payment( - sovryn, - set_demand_curve, - lend_to_pool, - open_margin_trade_position, - true, - FeesEvents, - loanToken, - RBTC, - WRBTC, - SUSD, - SOV, - accounts - ); - }); - - it("Test close complete margin trade sov reward payment with special rebates", async () => { - await close_complete_margin_trade_sov_reward_payment_with_special_rebates( - sovryn, - set_demand_curve, - lend_to_pool, - open_margin_trade_position, - true, - FeesEvents, - loanToken, - RBTC, - WRBTC, - SUSD, - SOV, - accounts - ); - }); - - it("Test close complete margin trade sov reward payment false", async () => { - await close_complete_margin_trade_sov_reward_payment( - sovryn, - set_demand_curve, - lend_to_pool, - open_margin_trade_position, - false, - FeesEvents, - loanToken, - RBTC, - WRBTC, - SUSD, - SOV, - accounts - ); - }); - - it("Test close complete margin trade sov reward payment false with special rebates", async () => { - await close_complete_margin_trade_sov_reward_payment_with_special_rebates( - sovryn, - set_demand_curve, - lend_to_pool, - open_margin_trade_position, - false, - FeesEvents, - loanToken, - RBTC, - WRBTC, - SUSD, - SOV, - accounts - ); - }); - - it("Test close partial margin trade", async () => { - await close_partial_margin_trade( - sovryn, - loanToken, - set_demand_curve, - lend_to_pool, - open_margin_trade_position, - priceFeeds, - true, - RBTC, - WRBTC, - SUSD, - accounts - ); - await close_partial_margin_trade( - sovryn, - loanToken, - set_demand_curve, - lend_to_pool, - open_margin_trade_position, - priceFeeds, - false, - RBTC, - WRBTC, - SUSD, - accounts - ); - }); - - it("Test close partial margin trade sov reward payment", async () => { - await close_partial_margin_trade_sov_reward_payment( - sovryn, - set_demand_curve, - lend_to_pool, - open_margin_trade_position, - true, - FeesEvents, - loanToken, - RBTC, - WRBTC, - SUSD, - SOV, - accounts - ); - }); - - it("Test close partial margin trade sov reward payment with special rebates", async () => { - await close_partial_margin_trade_sov_reward_payment_with_special_rebates( - sovryn, - set_demand_curve, - lend_to_pool, - open_margin_trade_position, - true, - FeesEvents, - loanToken, - RBTC, - WRBTC, - SUSD, - SOV, - accounts - ); - }); - - it("Test close partial margin trade sov reward payment false", async () => { - await close_partial_margin_trade_sov_reward_payment( - sovryn, - set_demand_curve, - lend_to_pool, - open_margin_trade_position, - false, - FeesEvents, - loanToken, - RBTC, - WRBTC, - SUSD, - SOV, - accounts - ); - }); - - it("Test close partial margin trade sov reward payment false with special rebates", async () => { - await close_partial_margin_trade_sov_reward_payment_with_special_rebates( - sovryn, - set_demand_curve, - lend_to_pool, - open_margin_trade_position, - false, - FeesEvents, - loanToken, - RBTC, - WRBTC, - SUSD, - SOV, - accounts - ); - }); - - // verifies that the loan size is computed correctly - it("Test getMarginBorrowAmountAndRate", async () => { - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, accounts[0]); - const deposit = hunEth; - const borrowAmount = await loanToken.getMarginBorrowAmountAndRate(oneEth.mul(new BN(4)), deposit); - const monthly_interest = borrowAmount[1].mul(new BN(28)).div(new BN(365)); - // divide by 1000 because of rounding - const actualAmount = borrowAmount[0].div(new BN(1000)); - const expectedAmount = deposit.mul(new BN(4)).mul(hunEth).div(hunEth.sub(monthly_interest)).div(new BN(1000)); - expect(actualAmount.eq(expectedAmount)).to.be.true; - }); - - // test the correct max escrow amount is returned (considering that the function is actually returning a bit less than the max) - it("Test getMaxEscrowAmount", async () => { - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, accounts[0]); - - const maxEscrowAmount1x = await loanToken.getMaxEscrowAmount(oneEth); - const maxEscrowAmount4x = await loanToken.getMaxEscrowAmount(oneEth.mul(new BN(4))); - expect(maxEscrowAmount1x.eq(maxEscrowAmount4x.mul(new BN(4)))).to.be.true; - }); - - it("Test increasing position of other trader should fail", async () => { - // prepare the test - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, accounts[0]); - // trader=accounts[1] on this call - const [loan_id] = await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, accounts[1]); - - // deposit collateral to add margin to the loan created above - await RBTC.approve(sovryn.address, oneEth); - await sovryn.depositCollateral(loan_id, oneEth); - await RBTC.transfer(accounts[2], oneEth); - await RBTC.approve(loanToken.address, oneEth, { from: accounts[2] }); - - await expectRevert( - loanToken.marginTrade( - loan_id, // loanId (0 for new loans) - new BN(2).mul(oneEth), // leverageAmount - 0, // loanTokenSent - 1000, // no collateral token sent - RBTC.address, // collateralTokenAddress - accounts[1], // trader, - 0, // slippage - "0x", // loanDataBytes (only required with ether) - { from: accounts[2] } - ), - "401 use of existing loan" - ); - }); - - /// @dev For test coverage - it("Should revert when collateralTokenAddress != loanTokenAddress", async () => { - // prepare the test - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, accounts[0]); - // trader=accounts[1] on this call - const [loan_id] = await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, accounts[1]); - - // deposit collateral to add margin to the loan created above - await RBTC.approve(sovryn.address, oneEth); - await sovryn.depositCollateral(loan_id, oneEth); - await RBTC.transfer(accounts[2], oneEth); - await RBTC.approve(loanToken.address, oneEth, { from: accounts[2] }); - - await expectRevert( - loanToken.marginTrade( - loan_id, // loanId (0 for new loans) - new BN(2).mul(oneEth), // leverageAmount - 0, // loanTokenSent - 1000, // no collateral token sent - SUSD.address, // collateralTokenAddress != loanTokenAddress - accounts[1], // trader, - 0, - "0x", // loanDataBytes (only required with ether) - { from: accounts[2] } - ), - "11" - ); - }); - - it("Test increasing position of margin trade using collateral", async () => { - // prepare the test - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, accounts[0]); - - const collateralTokenSent = new BN(wei("1", "ether")); - const leverage = 2; - - const [loan_id] = await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, accounts[0]); - - // deposit collateral to add margin to the loan created above - await RBTC.approve(sovryn.address, oneEth); - await sovryn.depositCollateral(loan_id, oneEth); - await RBTC.approve(loanToken.address, oneEth); - - let sovryn_before_collateral_token_balance = await RBTC.balanceOf(sovryn.address); - let previous_trader_collateral_token_balance = await RBTC.balanceOf(accounts[0]); - - let { receipt } = await loanToken.marginTrade( - loan_id, // loanId (0 for new loans) - new BN(leverage).mul(oneEth), // leverageAmount - 0, // loanTokenSent - collateralTokenSent, // collateral token sent - RBTC.address, // collateralTokenAddress - accounts[0], // trader, - 0, - "0x", // loanDataBytes (only required with ether) - { from: accounts[0] } - ); - - const decode = decodeLogs(receipt.rawLogs, LoanOpenings, "Trade"); - - // Verify event - let sovryn_after_collateral_token_balance = await RBTC.balanceOf(sovryn.address); - let latest_trader_collateral_token_balance = await RBTC.balanceOf(accounts[0]); - - const sovryn_collateral_token_balance_diff = sovryn_after_collateral_token_balance - .sub(sovryn_before_collateral_token_balance) - .toString(); - const trader_collateral_token_balance_diff = previous_trader_collateral_token_balance - .sub(latest_trader_collateral_token_balance) - .toString(); - const args = decode[0].args; - - expect(collateralTokenSent.toString()).to.equal(trader_collateral_token_balance_diff); - expect(args["user"]).to.equal(accounts[0]); - expect(args["lender"]).to.equal(loanToken.address); - expect(args["loanId"]).to.equal(loan_id); - expect(args["collateralToken"]).to.equal(RBTC.address); - expect(args["loanToken"]).to.equal(SUSD.address); - - // For margin trade using collateral, the positionSize can be checked by getting the additional collateral token that is transferred into the protocol (difference between latest & previous balance) - expect(args["positionSize"]).to.equal(sovryn_collateral_token_balance_diff); - }); - - it("Test increasing position of margin trade using underlying token", async () => { - // prepare the test - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, accounts[0]); - - const loanTokenSent = new BN(wei("2", "ether")); - const leverage = 2; - - const [loan_id] = await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, accounts[0]); - - // deposit collateral to add margin to the loan created above - await SUSD.approve(sovryn.address, loanTokenSent); - // await sovryn.depositCollateral(loan_id, oneEth); - await SUSD.approve(loanToken.address, loanTokenSent); - - const sovryn_before_collateral_token_balance = await RBTC.balanceOf(sovryn.address); - const trader_before_underlying_token_balance = await SUSD.balanceOf(accounts[0]); - - let { receipt } = await loanToken.marginTrade( - loan_id, // loanId (0 for new loans) - new BN(leverage).mul(oneEth), // leverageAmount - loanTokenSent, // loanTokenSent (Note 1 RBTC was set to 10000 SUSD) - 0, // no collateral token sent - RBTC.address, // collateralTokenAddress - accounts[0], // trader, - 0, - "0x", // loanDataBytes (only required with ether) - { from: accounts[0] } - ); - - const sovryn_after_collateral_token_balance = await RBTC.balanceOf(sovryn.address); - const trader_after_underlying_token_balance = await SUSD.balanceOf(accounts[0]); - - const decode = decodeLogs(receipt.rawLogs, LoanOpenings, "Trade"); - const args = decode[0].args; - const sovryn_collateral_token_balance_diff = sovryn_after_collateral_token_balance - .sub(sovryn_before_collateral_token_balance) - .toString(); - const trader_underlying_token_balance_diff = trader_before_underlying_token_balance - .sub(trader_after_underlying_token_balance) - .toString(); - - expect(loanTokenSent.toString()).to.equal(trader_underlying_token_balance_diff); - expect(args["user"]).to.equal(accounts[0]); - expect(args["lender"]).to.equal(loanToken.address); - expect(args["loanId"]).to.equal(loan_id); - expect(args["collateralToken"]).to.equal(RBTC.address); - expect(args["loanToken"]).to.equal(SUSD.address); - - // For margin trade using underlying token, the positionSize can be checked by getting the additional underlying token that is transferred into the protocol (difference between latest & previous balance) - expect(args["positionSize"]).to.equal(sovryn_collateral_token_balance_diff); - }); - - it("checkPriceDivergence should success if min position size is less than or equal to collateral", async () => { - await set_demand_curve(loanToken); - await SUSD.transfer(loanToken.address, wei("500", "ether")); - - await loanToken.checkPriceDivergence( - new BN(2).mul(oneEth), - wei("0.01", "ether"), - wei("0.01", "ether"), - RBTC.address, - wei("0.02", "ether") - ); - }); - - /// @dev For test coverage, it's required to perform a margin trade using WRBTC as collateral - it("Check marginTrade w/ collateralToken as address(0)", async () => { - await set_demand_curve(loanToken); - await SUSD.transfer(loanToken.address, wei("1000000", "ether")); - await WRBTC.mint(accounts[2], oneEth); - await WRBTC.approve(loanToken.address, oneEth, { from: accounts[2] }); - - await loanToken.marginTrade( - "0x0", // loanId (0 for new loans) - wei("2", "ether"), // leverageAmount - 0, // loanTokenSent (SUSD) - wei("1", "ether"), // collateral token sent - ZERO_ADDRESS, // collateralTokenAddress (address 0 means collateral is WRBTC) - accounts[1], // trader, - 2000, - "0x", // loanDataBytes (only required with ether) - { from: accounts[2] } - ); - }); - - it("Check marginTrade with minPositionSize > 0 ", async () => { - // await set_demand_curve(loanToken); - await SUSD.transfer(loanToken.address, wei("1000000", "ether")); - await RBTC.transfer(accounts[2], oneEth); - await RBTC.approve(loanToken.address, oneEth, { from: accounts[2] }); - - await expectRevert( - loanToken.marginTrade( - "0x0", // loanId (0 for new loans) - wei("2", "ether"), // leverageAmount - 0, // loanTokenSent (SUSD) - 10000, // collateral token sent - RBTC.address, // collateralTokenAddress (RBTC) - accounts[1], // trader, - 20000, // slippage - "0x", // loanDataBytes (only required with ether) - { from: accounts[2] } - ), - "principal too small" - ); - - await priceFeeds.setRates(WRBTC.address, SUSD.address, new BN(10).pow(new BN(20)).toString()); - await priceFeeds.setRates(RBTC.address, SUSD.address, new BN(10).pow(new BN(20)).toString()); - - await loanToken.marginTrade( - "0x0", // loanId (0 for new loans) - wei("2", "ether"), // leverageAmount - 0, // loanTokenSent (SUSD) - oneEth.toString(), // collateral token sent - RBTC.address, // collateralTokenAddress (RBTC) - accounts[1], // trader, - oneEth.mul(new BN(2)).toString(), // slippage - "0x", // loanDataBytes (only required with ether) - { from: accounts[2] } - ); - }); - - it("checkPriceDivergence should revert if min position size is greater than collateral", async () => { - await set_demand_curve(loanToken); - - await expectRevert( - loanToken.checkPriceDivergence(new BN(2).mul(oneEth), wei("2", "ether"), 0, RBTC.address, wei("1", "ether")), - "coll too low" - ); - }); - }); + it("Test close complete margin trade", async () => { + await close_complete_margin_trade( + sovryn, + loanToken, + set_demand_curve, + lend_to_pool, + open_margin_trade_position, + priceFeeds, + true, + RBTC, + WRBTC, + SUSD, + accounts + ); + await close_complete_margin_trade( + sovryn, + loanToken, + set_demand_curve, + lend_to_pool, + open_margin_trade_position, + priceFeeds, + false, + RBTC, + WRBTC, + SUSD, + accounts + ); + }); + + it("Test close complete margin trade sov reward payment", async () => { + await close_complete_margin_trade_sov_reward_payment( + sovryn, + set_demand_curve, + lend_to_pool, + open_margin_trade_position, + true, + FeesEvents, + loanToken, + RBTC, + WRBTC, + SUSD, + SOV, + accounts + ); + }); + + it("Test close complete margin trade sov reward payment with special rebates", async () => { + await close_complete_margin_trade_sov_reward_payment_with_special_rebates( + sovryn, + set_demand_curve, + lend_to_pool, + open_margin_trade_position, + true, + FeesEvents, + loanToken, + RBTC, + WRBTC, + SUSD, + SOV, + accounts + ); + }); + + it("Test close complete margin trade sov reward payment false", async () => { + await close_complete_margin_trade_sov_reward_payment( + sovryn, + set_demand_curve, + lend_to_pool, + open_margin_trade_position, + false, + FeesEvents, + loanToken, + RBTC, + WRBTC, + SUSD, + SOV, + accounts + ); + }); + + it("Test close complete margin trade sov reward payment false with special rebates", async () => { + await close_complete_margin_trade_sov_reward_payment_with_special_rebates( + sovryn, + set_demand_curve, + lend_to_pool, + open_margin_trade_position, + false, + FeesEvents, + loanToken, + RBTC, + WRBTC, + SUSD, + SOV, + accounts + ); + }); + + it("Test close partial margin trade", async () => { + await close_partial_margin_trade( + sovryn, + loanToken, + set_demand_curve, + lend_to_pool, + open_margin_trade_position, + priceFeeds, + true, + RBTC, + WRBTC, + SUSD, + accounts + ); + await close_partial_margin_trade( + sovryn, + loanToken, + set_demand_curve, + lend_to_pool, + open_margin_trade_position, + priceFeeds, + false, + RBTC, + WRBTC, + SUSD, + accounts + ); + }); + + it("Test close partial margin trade sov reward payment", async () => { + await close_partial_margin_trade_sov_reward_payment( + sovryn, + set_demand_curve, + lend_to_pool, + open_margin_trade_position, + true, + FeesEvents, + loanToken, + RBTC, + WRBTC, + SUSD, + SOV, + accounts + ); + }); + + it("Test close partial margin trade sov reward payment with special rebates", async () => { + await close_partial_margin_trade_sov_reward_payment_with_special_rebates( + sovryn, + set_demand_curve, + lend_to_pool, + open_margin_trade_position, + true, + FeesEvents, + loanToken, + RBTC, + WRBTC, + SUSD, + SOV, + accounts + ); + }); + + it("Test close partial margin trade sov reward payment false", async () => { + await close_partial_margin_trade_sov_reward_payment( + sovryn, + set_demand_curve, + lend_to_pool, + open_margin_trade_position, + false, + FeesEvents, + loanToken, + RBTC, + WRBTC, + SUSD, + SOV, + accounts + ); + }); + + it("Test close partial margin trade sov reward payment false with special rebates", async () => { + await close_partial_margin_trade_sov_reward_payment_with_special_rebates( + sovryn, + set_demand_curve, + lend_to_pool, + open_margin_trade_position, + false, + FeesEvents, + loanToken, + RBTC, + WRBTC, + SUSD, + SOV, + accounts + ); + }); + + // verifies that the loan size is computed correctly + it("Test getMarginBorrowAmountAndRate", async () => { + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, accounts[0]); + const deposit = hunEth; + const borrowAmount = await loanToken.getMarginBorrowAmountAndRate( + oneEth.mul(new BN(4)), + deposit + ); + const monthly_interest = borrowAmount[1].mul(new BN(28)).div(new BN(365)); + // divide by 1000 because of rounding + const actualAmount = borrowAmount[0].div(new BN(1000)); + const expectedAmount = deposit + .mul(new BN(4)) + .mul(hunEth) + .div(hunEth.sub(monthly_interest)) + .div(new BN(1000)); + expect(actualAmount.eq(expectedAmount)).to.be.true; + }); + + // test the correct max escrow amount is returned (considering that the function is actually returning a bit less than the max) + it("Test getMaxEscrowAmount", async () => { + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, accounts[0]); + + const maxEscrowAmount1x = await loanToken.getMaxEscrowAmount(oneEth); + const maxEscrowAmount4x = await loanToken.getMaxEscrowAmount(oneEth.mul(new BN(4))); + expect(maxEscrowAmount1x.eq(maxEscrowAmount4x.mul(new BN(4)))).to.be.true; + }); + + it("Test increasing position of other trader should fail", async () => { + // prepare the test + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, accounts[0]); + // trader=accounts[1] on this call + const [loan_id] = await open_margin_trade_position( + loanToken, + RBTC, + WRBTC, + SUSD, + accounts[1] + ); + + // deposit collateral to add margin to the loan created above + await RBTC.approve(sovryn.address, oneEth); + await sovryn.depositCollateral(loan_id, oneEth); + await RBTC.transfer(accounts[2], oneEth); + await RBTC.approve(loanToken.address, oneEth, { from: accounts[2] }); + + await expectRevert( + loanToken.marginTrade( + loan_id, // loanId (0 for new loans) + new BN(2).mul(oneEth), // leverageAmount + 0, // loanTokenSent + 1000, // no collateral token sent + RBTC.address, // collateralTokenAddress + accounts[1], // trader, + 0, // slippage + "0x", // loanDataBytes (only required with ether) + { from: accounts[2] } + ), + "401 use of existing loan" + ); + }); + + /// @dev For test coverage + it("Should revert when collateralTokenAddress != loanTokenAddress", async () => { + // prepare the test + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, accounts[0]); + // trader=accounts[1] on this call + const [loan_id] = await open_margin_trade_position( + loanToken, + RBTC, + WRBTC, + SUSD, + accounts[1] + ); + + // deposit collateral to add margin to the loan created above + await RBTC.approve(sovryn.address, oneEth); + await sovryn.depositCollateral(loan_id, oneEth); + await RBTC.transfer(accounts[2], oneEth); + await RBTC.approve(loanToken.address, oneEth, { from: accounts[2] }); + + await expectRevert( + loanToken.marginTrade( + loan_id, // loanId (0 for new loans) + new BN(2).mul(oneEth), // leverageAmount + 0, // loanTokenSent + 1000, // no collateral token sent + SUSD.address, // collateralTokenAddress != loanTokenAddress + accounts[1], // trader, + 0, + "0x", // loanDataBytes (only required with ether) + { from: accounts[2] } + ), + "11" + ); + }); + + it("Test increasing position of margin trade using collateral", async () => { + // prepare the test + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, accounts[0]); + + const collateralTokenSent = new BN(wei("1", "ether")); + const leverage = 2; + + const [loan_id] = await open_margin_trade_position( + loanToken, + RBTC, + WRBTC, + SUSD, + accounts[0] + ); + + // deposit collateral to add margin to the loan created above + await RBTC.approve(sovryn.address, oneEth); + await sovryn.depositCollateral(loan_id, oneEth); + await RBTC.approve(loanToken.address, oneEth); + + let sovryn_before_collateral_token_balance = await RBTC.balanceOf(sovryn.address); + let previous_trader_collateral_token_balance = await RBTC.balanceOf(accounts[0]); + + let { receipt } = await loanToken.marginTrade( + loan_id, // loanId (0 for new loans) + new BN(leverage).mul(oneEth), // leverageAmount + 0, // loanTokenSent + collateralTokenSent, // collateral token sent + RBTC.address, // collateralTokenAddress + accounts[0], // trader, + 0, + "0x", // loanDataBytes (only required with ether) + { from: accounts[0] } + ); + + const decode = decodeLogs(receipt.rawLogs, LoanOpenings, "Trade"); + + // Verify event + let sovryn_after_collateral_token_balance = await RBTC.balanceOf(sovryn.address); + let latest_trader_collateral_token_balance = await RBTC.balanceOf(accounts[0]); + + const sovryn_collateral_token_balance_diff = sovryn_after_collateral_token_balance + .sub(sovryn_before_collateral_token_balance) + .toString(); + const trader_collateral_token_balance_diff = previous_trader_collateral_token_balance + .sub(latest_trader_collateral_token_balance) + .toString(); + const args = decode[0].args; + + expect(collateralTokenSent.toString()).to.equal(trader_collateral_token_balance_diff); + expect(args["user"]).to.equal(accounts[0]); + expect(args["lender"]).to.equal(loanToken.address); + expect(args["loanId"]).to.equal(loan_id); + expect(args["collateralToken"]).to.equal(RBTC.address); + expect(args["loanToken"]).to.equal(SUSD.address); + + // For margin trade using collateral, the positionSize can be checked by getting the additional collateral token that is transferred into the protocol (difference between latest & previous balance) + expect(args["positionSize"]).to.equal(sovryn_collateral_token_balance_diff); + }); + + it("Test increasing position of margin trade using underlying token", async () => { + // prepare the test + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, accounts[0]); + + const loanTokenSent = new BN(wei("2", "ether")); + const leverage = 2; + + const [loan_id] = await open_margin_trade_position( + loanToken, + RBTC, + WRBTC, + SUSD, + accounts[0] + ); + + // deposit collateral to add margin to the loan created above + await SUSD.approve(sovryn.address, loanTokenSent); + // await sovryn.depositCollateral(loan_id, oneEth); + await SUSD.approve(loanToken.address, loanTokenSent); + + const sovryn_before_collateral_token_balance = await RBTC.balanceOf(sovryn.address); + const trader_before_underlying_token_balance = await SUSD.balanceOf(accounts[0]); + + let { receipt } = await loanToken.marginTrade( + loan_id, // loanId (0 for new loans) + new BN(leverage).mul(oneEth), // leverageAmount + loanTokenSent, // loanTokenSent (Note 1 RBTC was set to 10000 SUSD) + 0, // no collateral token sent + RBTC.address, // collateralTokenAddress + accounts[0], // trader, + 0, + "0x", // loanDataBytes (only required with ether) + { from: accounts[0] } + ); + + const sovryn_after_collateral_token_balance = await RBTC.balanceOf(sovryn.address); + const trader_after_underlying_token_balance = await SUSD.balanceOf(accounts[0]); + + const decode = decodeLogs(receipt.rawLogs, LoanOpenings, "Trade"); + const args = decode[0].args; + const sovryn_collateral_token_balance_diff = sovryn_after_collateral_token_balance + .sub(sovryn_before_collateral_token_balance) + .toString(); + const trader_underlying_token_balance_diff = trader_before_underlying_token_balance + .sub(trader_after_underlying_token_balance) + .toString(); + + expect(loanTokenSent.toString()).to.equal(trader_underlying_token_balance_diff); + expect(args["user"]).to.equal(accounts[0]); + expect(args["lender"]).to.equal(loanToken.address); + expect(args["loanId"]).to.equal(loan_id); + expect(args["collateralToken"]).to.equal(RBTC.address); + expect(args["loanToken"]).to.equal(SUSD.address); + + // For margin trade using underlying token, the positionSize can be checked by getting the additional underlying token that is transferred into the protocol (difference between latest & previous balance) + expect(args["positionSize"]).to.equal(sovryn_collateral_token_balance_diff); + }); + + it("checkPriceDivergence should success if min position size is less than or equal to collateral", async () => { + await set_demand_curve(loanToken); + await SUSD.transfer(loanToken.address, wei("500", "ether")); + + await loanToken.checkPriceDivergence( + new BN(2).mul(oneEth), + wei("0.01", "ether"), + wei("0.01", "ether"), + RBTC.address, + wei("0.02", "ether") + ); + }); + + /// @dev For test coverage, it's required to perform a margin trade using WRBTC as collateral + it("Check marginTrade w/ collateralToken as address(0)", async () => { + await set_demand_curve(loanToken); + await SUSD.transfer(loanToken.address, wei("1000000", "ether")); + await WRBTC.mint(accounts[2], oneEth); + await WRBTC.approve(loanToken.address, oneEth, { from: accounts[2] }); + + await loanToken.marginTrade( + "0x0", // loanId (0 for new loans) + wei("2", "ether"), // leverageAmount + 0, // loanTokenSent (SUSD) + wei("1", "ether"), // collateral token sent + ZERO_ADDRESS, // collateralTokenAddress (address 0 means collateral is WRBTC) + accounts[1], // trader, + 2000, + "0x", // loanDataBytes (only required with ether) + { from: accounts[2] } + ); + }); + + it("Check marginTrade with minPositionSize > 0 ", async () => { + // await set_demand_curve(loanToken); + await SUSD.transfer(loanToken.address, wei("1000000", "ether")); + await RBTC.transfer(accounts[2], oneEth); + await RBTC.approve(loanToken.address, oneEth, { from: accounts[2] }); + + await expectRevert( + loanToken.marginTrade( + "0x0", // loanId (0 for new loans) + wei("2", "ether"), // leverageAmount + 0, // loanTokenSent (SUSD) + 10000, // collateral token sent + RBTC.address, // collateralTokenAddress (RBTC) + accounts[1], // trader, + 20000, // slippage + "0x", // loanDataBytes (only required with ether) + { from: accounts[2] } + ), + "principal too small" + ); + + await priceFeeds.setRates( + WRBTC.address, + SUSD.address, + new BN(10).pow(new BN(20)).toString() + ); + await priceFeeds.setRates( + RBTC.address, + SUSD.address, + new BN(10).pow(new BN(20)).toString() + ); + + await loanToken.marginTrade( + "0x0", // loanId (0 for new loans) + wei("2", "ether"), // leverageAmount + 0, // loanTokenSent (SUSD) + oneEth.toString(), // collateral token sent + RBTC.address, // collateralTokenAddress (RBTC) + accounts[1], // trader, + oneEth.mul(new BN(2)).toString(), // slippage + "0x", // loanDataBytes (only required with ether) + { from: accounts[2] } + ); + }); + + it("checkPriceDivergence should revert if min position size is greater than collateral", async () => { + await set_demand_curve(loanToken); + + await expectRevert( + loanToken.checkPriceDivergence( + new BN(2).mul(oneEth), + wei("2", "ether"), + 0, + RBTC.address, + wei("1", "ether") + ), + "coll too low" + ); + }); + }); }); diff --git a/tests/loan-token/TradingwRBTCCollateral.test.js b/tests/loan-token/TradingwRBTCCollateral.test.js index a7b4fdcb9..9927c9ca1 100644 --- a/tests/loan-token/TradingwRBTCCollateral.test.js +++ b/tests/loan-token/TradingwRBTCCollateral.test.js @@ -16,32 +16,32 @@ const { loadFixture } = waffle; const { BN, expectRevert, constants } = require("@openzeppelin/test-helpers"); const { - margin_trading_sending_loan_tokens, - margin_trading_sov_reward_payment, - margin_trading_sov_reward_payment_with_special_rebates, - margin_trading_sending_collateral_tokens, - margin_trading_sending_collateral_tokens_sov_reward_payment, - margin_trading_sending_collateral_tokens_sov_reward_payment_with_special_rebates, - close_complete_margin_trade, - close_partial_margin_trade, + margin_trading_sending_loan_tokens, + margin_trading_sov_reward_payment, + margin_trading_sov_reward_payment_with_special_rebates, + margin_trading_sending_collateral_tokens, + margin_trading_sending_collateral_tokens_sov_reward_payment, + margin_trading_sending_collateral_tokens_sov_reward_payment_with_special_rebates, + close_complete_margin_trade, + close_partial_margin_trade, } = require("./tradingFunctions"); const FeesEvents = artifacts.require("FeesEvents"); const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getSOV, - getLoanToken, - getLoanTokenWRBTC, - loan_pool_setup, - getPriceFeeds, - getSovryn, - lend_to_pool, - set_demand_curve, - open_margin_trade_position, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getSOV, + getLoanToken, + getLoanTokenWRBTC, + loan_pool_setup, + getPriceFeeds, + getSovryn, + lend_to_pool, + set_demand_curve, + open_margin_trade_position, } = require("../Utils/initializer.js"); const wei = web3.utils.toWei; @@ -49,35 +49,35 @@ const wei = web3.utils.toWei; const oneEth = new BN(wei("1", "ether")); contract("LoanTokenTrading", (accounts) => { - let owner; - let sovryn, SUSD, WRBTC, RBTC, BZRX, loanToken, loanTokenWRBTC, SOV, priceFeeds; + let owner; + let sovryn, SUSD, WRBTC, RBTC, BZRX, loanToken, loanTokenWRBTC, SOV, priceFeeds; - async function deploymentAndInitFixture(_wallets, _provider) { - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + async function deploymentAndInitFixture(_wallets, _provider) { + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - loanToken = await getLoanToken(owner, sovryn, WRBTC, SUSD); - loanTokenWRBTC = await getLoanTokenWRBTC(owner, sovryn, WRBTC, SUSD); - await loan_pool_setup(sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC); + loanToken = await getLoanToken(owner, sovryn, WRBTC, SUSD); + loanTokenWRBTC = await getLoanTokenWRBTC(owner, sovryn, WRBTC, SUSD); + await loan_pool_setup(sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC); - SOV = await getSOV(sovryn, priceFeeds, SUSD, accounts); - } + SOV = await getSOV(sovryn, priceFeeds, SUSD, accounts); + } - before(async () => { - [owner] = accounts; - }); + before(async () => { + [owner] = accounts; + }); - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); - describe("test the loan token trading logic with wBTC as collateral token and the sUSD test token as underlying loan token. ", () => { - /* tests margin trading sending loan tokens. + describe("test the loan token trading logic with wBTC as collateral token and the sUSD test token as underlying loan token. ", () => { + /* tests margin trading sending loan tokens. process is handled by the shared function margin_trading_sending_loan_tokens 1. approve the transfer 2. send the margin trade tx @@ -85,106 +85,138 @@ contract("LoanTokenTrading", (accounts) => { 4. retrieve the loan from the smart contract and make sure all values are set as expected */ - it("Test margin trading sending loan tokens", async () => { - await expectRevert( - loanToken.marginTrade( - constants.ZERO_BYTES32, // loanId (0 for new loans) - oneEth.toString(), // leverageAmount - oneEth.toString(), // loanTokenSent - "0", // no collateral token sent - WRBTC.address, // collateralTokenAddress - owner, // trader, - 0, // slippage - "0x" // loanDataBytes (only required with ether) - ), - "principal too small" - ); + it("Test margin trading sending loan tokens", async () => { + await expectRevert( + loanToken.marginTrade( + constants.ZERO_BYTES32, // loanId (0 for new loans) + oneEth.toString(), // leverageAmount + oneEth.toString(), // loanTokenSent + "0", // no collateral token sent + WRBTC.address, // collateralTokenAddress + owner, // trader, + 0, // slippage + "0x" // loanDataBytes (only required with ether) + ), + "principal too small" + ); - await priceFeeds.setRates(WRBTC.address, SUSD.address, new BN(10).pow(new BN(20)).toString()); - await priceFeeds.setRates(RBTC.address, SUSD.address, new BN(10).pow(new BN(20)).toString()); + await priceFeeds.setRates( + WRBTC.address, + SUSD.address, + new BN(10).pow(new BN(20)).toString() + ); + await priceFeeds.setRates( + RBTC.address, + SUSD.address, + new BN(10).pow(new BN(20)).toString() + ); - await margin_trading_sending_loan_tokens(accounts, sovryn, loanToken, SUSD, WRBTC, priceFeeds, false); - await margin_trading_sov_reward_payment(accounts, loanToken, SUSD, WRBTC, SOV, FeesEvents, sovryn); - await margin_trading_sov_reward_payment_with_special_rebates(accounts, loanToken, SUSD, WRBTC, SOV, FeesEvents, sovryn); - }); + await margin_trading_sending_loan_tokens( + accounts, + sovryn, + loanToken, + SUSD, + WRBTC, + priceFeeds, + false + ); + await margin_trading_sov_reward_payment( + accounts, + loanToken, + SUSD, + WRBTC, + SOV, + FeesEvents, + sovryn + ); + await margin_trading_sov_reward_payment_with_special_rebates( + accounts, + loanToken, + SUSD, + WRBTC, + SOV, + FeesEvents, + sovryn + ); + }); - it("Test margin trading sending collateral tokens", async () => { - const loanSize = oneEth.mul(new BN(10000)); - await SUSD.mint(loanToken.address, loanSize.mul(new BN(12))); - const collateralTokenSent = await sovryn.getRequiredCollateral( - SUSD.address, - WRBTC.address, - loanSize.mul(new BN(2)), - new BN(50).mul(oneEth), - false - ); - const leverageAmount = new BN(5).mul(oneEth); - await margin_trading_sending_collateral_tokens( - accounts, - loanToken, - SUSD, - WRBTC, - loanSize, - collateralTokenSent, - leverageAmount, - collateralTokenSent, - priceFeeds - ); - await WRBTC.mint(accounts[2], collateralTokenSent); - await WRBTC.approve(loanToken.address, collateralTokenSent, { from: accounts[2] }); - await margin_trading_sending_collateral_tokens_sov_reward_payment( - accounts[2], - loanToken, - SUSD, - WRBTC, - collateralTokenSent, - leverageAmount, - 0, - FeesEvents, - SOV, - sovryn - ); - }); + it("Test margin trading sending collateral tokens", async () => { + const loanSize = oneEth.mul(new BN(10000)); + await SUSD.mint(loanToken.address, loanSize.mul(new BN(12))); + const collateralTokenSent = await sovryn.getRequiredCollateral( + SUSD.address, + WRBTC.address, + loanSize.mul(new BN(2)), + new BN(50).mul(oneEth), + false + ); + const leverageAmount = new BN(5).mul(oneEth); + await margin_trading_sending_collateral_tokens( + accounts, + loanToken, + SUSD, + WRBTC, + loanSize, + collateralTokenSent, + leverageAmount, + collateralTokenSent, + priceFeeds + ); + await WRBTC.mint(accounts[2], collateralTokenSent); + await WRBTC.approve(loanToken.address, collateralTokenSent, { from: accounts[2] }); + await margin_trading_sending_collateral_tokens_sov_reward_payment( + accounts[2], + loanToken, + SUSD, + WRBTC, + collateralTokenSent, + leverageAmount, + 0, + FeesEvents, + SOV, + sovryn + ); + }); - it("Test margin trading sending collateral tokens with special rebates", async () => { - const loanSize = oneEth.mul(new BN(10000)); - await SUSD.mint(loanToken.address, loanSize.mul(new BN(12))); - const collateralTokenSent = await sovryn.getRequiredCollateral( - SUSD.address, - WRBTC.address, - loanSize.mul(new BN(2)), - new BN(50).mul(oneEth), - false - ); - const leverageAmount = new BN(5).mul(oneEth); - await margin_trading_sending_collateral_tokens( - accounts, - loanToken, - SUSD, - WRBTC, - loanSize, - collateralTokenSent, - leverageAmount, - collateralTokenSent, - priceFeeds - ); - await WRBTC.mint(accounts[2], collateralTokenSent); - await WRBTC.approve(loanToken.address, collateralTokenSent, { from: accounts[2] }); - await margin_trading_sending_collateral_tokens_sov_reward_payment_with_special_rebates( - accounts[2], - loanToken, - SUSD, - WRBTC, - collateralTokenSent, - leverageAmount, - 0, - FeesEvents, - SOV, - sovryn - ); - }); + it("Test margin trading sending collateral tokens with special rebates", async () => { + const loanSize = oneEth.mul(new BN(10000)); + await SUSD.mint(loanToken.address, loanSize.mul(new BN(12))); + const collateralTokenSent = await sovryn.getRequiredCollateral( + SUSD.address, + WRBTC.address, + loanSize.mul(new BN(2)), + new BN(50).mul(oneEth), + false + ); + const leverageAmount = new BN(5).mul(oneEth); + await margin_trading_sending_collateral_tokens( + accounts, + loanToken, + SUSD, + WRBTC, + loanSize, + collateralTokenSent, + leverageAmount, + collateralTokenSent, + priceFeeds + ); + await WRBTC.mint(accounts[2], collateralTokenSent); + await WRBTC.approve(loanToken.address, collateralTokenSent, { from: accounts[2] }); + await margin_trading_sending_collateral_tokens_sov_reward_payment_with_special_rebates( + accounts[2], + loanToken, + SUSD, + WRBTC, + collateralTokenSent, + leverageAmount, + 0, + FeesEvents, + SOV, + sovryn + ); + }); - /* should completely close a position. + /* should completely close a position. first with returning loan tokens, then with returning collateral tokens to the sender. process is handled by the shared function close_complete_margin_trade 1. prepares the test by setting up the interest rates, lending to the pool and opening a position @@ -194,36 +226,36 @@ contract("LoanTokenTrading", (accounts) => { 5. verifies the result */ - it("Test close complete margin trade", async () => { - await close_complete_margin_trade( - sovryn, - loanToken, - set_demand_curve, - lend_to_pool, - open_margin_trade_position, - priceFeeds, - true, - RBTC, - WRBTC, - SUSD, - accounts - ); - await close_complete_margin_trade( - sovryn, - loanToken, - set_demand_curve, - lend_to_pool, - open_margin_trade_position, - priceFeeds, - false, - RBTC, - WRBTC, - SUSD, - accounts - ); - }); + it("Test close complete margin trade", async () => { + await close_complete_margin_trade( + sovryn, + loanToken, + set_demand_curve, + lend_to_pool, + open_margin_trade_position, + priceFeeds, + true, + RBTC, + WRBTC, + SUSD, + accounts + ); + await close_complete_margin_trade( + sovryn, + loanToken, + set_demand_curve, + lend_to_pool, + open_margin_trade_position, + priceFeeds, + false, + RBTC, + WRBTC, + SUSD, + accounts + ); + }); - /* should partially close a position. + /* should partially close a position. first with returning loan tokens, then with returning collateral tokens to the sender. process is handled by the shared function close_partial_margin_trade 1. prepares the test by setting up the interest rates, lending to the pool and opening a position @@ -233,33 +265,33 @@ contract("LoanTokenTrading", (accounts) => { 5. verifies the result */ - it("Test close partial margin trade", async () => { - await close_partial_margin_trade( - sovryn, - loanToken, - set_demand_curve, - lend_to_pool, - open_margin_trade_position, - priceFeeds, - true, - RBTC, - WRBTC, - SUSD, - accounts - ); - await close_partial_margin_trade( - sovryn, - loanToken, - set_demand_curve, - lend_to_pool, - open_margin_trade_position, - priceFeeds, - false, - RBTC, - WRBTC, - SUSD, - accounts - ); - }); - }); + it("Test close partial margin trade", async () => { + await close_partial_margin_trade( + sovryn, + loanToken, + set_demand_curve, + lend_to_pool, + open_margin_trade_position, + priceFeeds, + true, + RBTC, + WRBTC, + SUSD, + accounts + ); + await close_partial_margin_trade( + sovryn, + loanToken, + set_demand_curve, + lend_to_pool, + open_margin_trade_position, + priceFeeds, + false, + RBTC, + WRBTC, + SUSD, + accounts + ); + }); + }); }); diff --git a/tests/loan-token/TradingwRBTCLoan.test.js b/tests/loan-token/TradingwRBTCLoan.test.js index ac312a2a1..b16b86fb3 100644 --- a/tests/loan-token/TradingwRBTCLoan.test.js +++ b/tests/loan-token/TradingwRBTCLoan.test.js @@ -16,32 +16,32 @@ const { loadFixture } = waffle; const { BN } = require("@openzeppelin/test-helpers"); const { - margin_trading_sending_loan_tokens, - margin_trading_sov_reward_payment, - margin_trading_sov_reward_payment_with_special_rebates, - margin_trading_sending_collateral_tokens, - margin_trading_sending_collateral_tokens_sov_reward_payment, - margin_trading_sending_collateral_tokens_sov_reward_payment_with_special_rebates, - close_complete_margin_trade_wrbtc, - close_partial_margin_trade_wrbtc, + margin_trading_sending_loan_tokens, + margin_trading_sov_reward_payment, + margin_trading_sov_reward_payment_with_special_rebates, + margin_trading_sending_collateral_tokens, + margin_trading_sending_collateral_tokens_sov_reward_payment, + margin_trading_sending_collateral_tokens_sov_reward_payment_with_special_rebates, + close_complete_margin_trade_wrbtc, + close_partial_margin_trade_wrbtc, } = require("./tradingFunctions"); const FeesEvents = artifacts.require("FeesEvents"); const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getSOV, - getLoanToken, - getLoanTokenWRBTC, - loan_pool_setup, - set_demand_curve, - lend_to_pool_iBTC, - getPriceFeedsRBTC, - getSovryn, - open_margin_trade_position_iBTC, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getSOV, + getLoanToken, + getLoanTokenWRBTC, + loan_pool_setup, + set_demand_curve, + lend_to_pool_iBTC, + getPriceFeedsRBTC, + getSovryn, + open_margin_trade_position_iBTC, } = require("../Utils/initializer.js"); const wei = web3.utils.toWei; @@ -49,35 +49,35 @@ const wei = web3.utils.toWei; const oneEth = new BN(wei("1", "ether")); contract("LoanTokenTrading", (accounts) => { - let owner; - let sovryn, SUSD, WRBTC, RBTC, BZRX, loanToken, loanTokenWRBTC, SOV, priceFeeds; + let owner; + let sovryn, SUSD, WRBTC, RBTC, BZRX, loanToken, loanTokenWRBTC, SOV, priceFeeds; - async function deploymentAndInitFixture(_wallets, _provider) { - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeedsRBTC(WRBTC, SUSD, RBTC, sovryn, BZRX); + async function deploymentAndInitFixture(_wallets, _provider) { + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeedsRBTC(WRBTC, SUSD, RBTC, sovryn, BZRX); - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - loanToken = await getLoanToken(owner, sovryn, WRBTC, SUSD); - loanTokenWRBTC = await getLoanTokenWRBTC(owner, sovryn, WRBTC, SUSD); - await loan_pool_setup(sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC); + loanToken = await getLoanToken(owner, sovryn, WRBTC, SUSD); + loanTokenWRBTC = await getLoanTokenWRBTC(owner, sovryn, WRBTC, SUSD); + await loan_pool_setup(sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC); - SOV = await getSOV(sovryn, priceFeeds, SUSD, accounts); - } + SOV = await getSOV(sovryn, priceFeeds, SUSD, accounts); + } - before(async () => { - [owner] = accounts; - }); + before(async () => { + [owner] = accounts; + }); - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); - describe("test the loan token trading logic with SUSD test token as collateral token and the wBTC as underlying loan token. ", () => { - /* + describe("test the loan token trading logic with SUSD test token as collateral token and the wBTC as underlying loan token. ", () => { + /* tests margin trading sending loan tokens. process is handled by the shared function margin_trading_sending_loan_tokens 1. approve the transfer @@ -85,166 +85,198 @@ contract("LoanTokenTrading", (accounts) => { 3. verify the trade event and balances are correct 4. retrieve the loan from the smart contract and make sure all values are set as expected */ - it("Test margin trading sending loan tokens", async () => { - await margin_trading_sending_loan_tokens(accounts, sovryn, loanTokenWRBTC, WRBTC, SUSD, priceFeeds, false); - await margin_trading_sov_reward_payment(accounts, loanTokenWRBTC, WRBTC, SUSD, SOV, FeesEvents, sovryn); - await margin_trading_sov_reward_payment_with_special_rebates(accounts, loanTokenWRBTC, WRBTC, SUSD, SOV, FeesEvents, sovryn); - }); - it("Test margin trading sending collateral tokens", async () => { - const loanSize = oneEth; - // make sure there are sufficient funds on the contract - await loanTokenWRBTC.mintWithBTC(accounts[0], false, { value: loanSize.mul(new BN(6)) }); - await loanTokenWRBTC.mintWithBTC(accounts[2], false, { value: loanSize.mul(new BN(6)) }); - // compute the amount of collateral tokens needed - const collateralTokenSent = await sovryn.getRequiredCollateral( - WRBTC.address, - SUSD.address, - loanSize.mul(new BN(2)), - new BN(50).mul(oneEth), - false - ); - await SUSD.mint(accounts[0], collateralTokenSent); - await SUSD.mint(accounts[2], collateralTokenSent); - // important! WRBTC is being held by the loanToken contract itself, all other tokens are transfered directly from - // the sender and need approval - await SUSD.approve(loanTokenWRBTC.address, collateralTokenSent); - await SUSD.approve(loanTokenWRBTC.address, collateralTokenSent, { from: accounts[2] }); - const leverageAmount = new BN(5).mul(oneEth); - const value = 0; - await margin_trading_sending_collateral_tokens( - accounts, - loanTokenWRBTC, - WRBTC, - SUSD, - loanSize, - collateralTokenSent, - leverageAmount, - value, - priceFeeds - ); - await margin_trading_sending_collateral_tokens_sov_reward_payment( - accounts[2], - loanTokenWRBTC, - WRBTC, - SUSD, - collateralTokenSent, - leverageAmount, - value, - FeesEvents, - SOV, - sovryn - ); - }); - - it("Test margin trading sending collateral tokens with special rebates", async () => { - const loanSize = oneEth; - // make sure there are sufficient funds on the contract - await loanTokenWRBTC.mintWithBTC(accounts[0], false, { value: loanSize.mul(new BN(6)).toString() }); - await loanTokenWRBTC.mintWithBTC(accounts[2], false, { value: loanSize.mul(new BN(6)).toString() }); - // compute the amount of collateral tokens needed - const collateralTokenSent = await sovryn.getRequiredCollateral( - WRBTC.address, - SUSD.address, - loanSize.mul(new BN(2)), - new BN(50).mul(oneEth), - false - ); - - await SUSD.mint(accounts[0], collateralTokenSent); - await SUSD.mint(accounts[2], collateralTokenSent); - // important! WRBTC is being held by the loanToken contract itself, all other tokens are transfered directly from - // the sender and need approval - await SUSD.approve(loanTokenWRBTC.address, collateralTokenSent); - await SUSD.approve(loanTokenWRBTC.address, collateralTokenSent, { from: accounts[2] }); - const leverageAmount = new BN(5).mul(oneEth); - const value = 0; - - await margin_trading_sending_collateral_tokens( - accounts, - loanTokenWRBTC, - WRBTC, - SUSD, - loanSize, - collateralTokenSent, - leverageAmount, - value, - priceFeeds - ); - - await margin_trading_sending_collateral_tokens_sov_reward_payment_with_special_rebates( - accounts[2], - loanTokenWRBTC, - WRBTC, - SUSD, - collateralTokenSent, - leverageAmount, - value, - FeesEvents, - SOV, - sovryn - ); - }); - - it("Test close complete margin trade", async () => { - await close_complete_margin_trade_wrbtc( - sovryn, - loanToken, - loanTokenWRBTC, - set_demand_curve, - lend_to_pool_iBTC, - open_margin_trade_position_iBTC, - priceFeeds, - true, - RBTC, - WRBTC, - SUSD, - accounts - ); - await close_complete_margin_trade_wrbtc( - sovryn, - loanToken, - loanTokenWRBTC, - set_demand_curve, - lend_to_pool_iBTC, - open_margin_trade_position_iBTC, - priceFeeds, - false, - RBTC, - WRBTC, - SUSD, - accounts - ); - }); - - it("Test close partial margin trade", async () => { - await close_partial_margin_trade_wrbtc( - sovryn, - loanToken, - loanTokenWRBTC, - set_demand_curve, - lend_to_pool_iBTC, - open_margin_trade_position_iBTC, - priceFeeds, - true, - RBTC, - WRBTC, - SUSD, - accounts - ); - await close_partial_margin_trade_wrbtc( - sovryn, - loanToken, - loanTokenWRBTC, - set_demand_curve, - lend_to_pool_iBTC, - open_margin_trade_position_iBTC, - priceFeeds, - false, - RBTC, - WRBTC, - SUSD, - accounts - ); - }); - }); + it("Test margin trading sending loan tokens", async () => { + await margin_trading_sending_loan_tokens( + accounts, + sovryn, + loanTokenWRBTC, + WRBTC, + SUSD, + priceFeeds, + false + ); + await margin_trading_sov_reward_payment( + accounts, + loanTokenWRBTC, + WRBTC, + SUSD, + SOV, + FeesEvents, + sovryn + ); + await margin_trading_sov_reward_payment_with_special_rebates( + accounts, + loanTokenWRBTC, + WRBTC, + SUSD, + SOV, + FeesEvents, + sovryn + ); + }); + it("Test margin trading sending collateral tokens", async () => { + const loanSize = oneEth; + // make sure there are sufficient funds on the contract + await loanTokenWRBTC.mintWithBTC(accounts[0], false, { + value: loanSize.mul(new BN(6)), + }); + await loanTokenWRBTC.mintWithBTC(accounts[2], false, { + value: loanSize.mul(new BN(6)), + }); + // compute the amount of collateral tokens needed + const collateralTokenSent = await sovryn.getRequiredCollateral( + WRBTC.address, + SUSD.address, + loanSize.mul(new BN(2)), + new BN(50).mul(oneEth), + false + ); + await SUSD.mint(accounts[0], collateralTokenSent); + await SUSD.mint(accounts[2], collateralTokenSent); + // important! WRBTC is being held by the loanToken contract itself, all other tokens are transfered directly from + // the sender and need approval + await SUSD.approve(loanTokenWRBTC.address, collateralTokenSent); + await SUSD.approve(loanTokenWRBTC.address, collateralTokenSent, { from: accounts[2] }); + const leverageAmount = new BN(5).mul(oneEth); + const value = 0; + await margin_trading_sending_collateral_tokens( + accounts, + loanTokenWRBTC, + WRBTC, + SUSD, + loanSize, + collateralTokenSent, + leverageAmount, + value, + priceFeeds + ); + await margin_trading_sending_collateral_tokens_sov_reward_payment( + accounts[2], + loanTokenWRBTC, + WRBTC, + SUSD, + collateralTokenSent, + leverageAmount, + value, + FeesEvents, + SOV, + sovryn + ); + }); + + it("Test margin trading sending collateral tokens with special rebates", async () => { + const loanSize = oneEth; + // make sure there are sufficient funds on the contract + await loanTokenWRBTC.mintWithBTC(accounts[0], false, { + value: loanSize.mul(new BN(6)).toString(), + }); + await loanTokenWRBTC.mintWithBTC(accounts[2], false, { + value: loanSize.mul(new BN(6)).toString(), + }); + // compute the amount of collateral tokens needed + const collateralTokenSent = await sovryn.getRequiredCollateral( + WRBTC.address, + SUSD.address, + loanSize.mul(new BN(2)), + new BN(50).mul(oneEth), + false + ); + + await SUSD.mint(accounts[0], collateralTokenSent); + await SUSD.mint(accounts[2], collateralTokenSent); + // important! WRBTC is being held by the loanToken contract itself, all other tokens are transfered directly from + // the sender and need approval + await SUSD.approve(loanTokenWRBTC.address, collateralTokenSent); + await SUSD.approve(loanTokenWRBTC.address, collateralTokenSent, { from: accounts[2] }); + const leverageAmount = new BN(5).mul(oneEth); + const value = 0; + + await margin_trading_sending_collateral_tokens( + accounts, + loanTokenWRBTC, + WRBTC, + SUSD, + loanSize, + collateralTokenSent, + leverageAmount, + value, + priceFeeds + ); + + await margin_trading_sending_collateral_tokens_sov_reward_payment_with_special_rebates( + accounts[2], + loanTokenWRBTC, + WRBTC, + SUSD, + collateralTokenSent, + leverageAmount, + value, + FeesEvents, + SOV, + sovryn + ); + }); + + it("Test close complete margin trade", async () => { + await close_complete_margin_trade_wrbtc( + sovryn, + loanToken, + loanTokenWRBTC, + set_demand_curve, + lend_to_pool_iBTC, + open_margin_trade_position_iBTC, + priceFeeds, + true, + RBTC, + WRBTC, + SUSD, + accounts + ); + await close_complete_margin_trade_wrbtc( + sovryn, + loanToken, + loanTokenWRBTC, + set_demand_curve, + lend_to_pool_iBTC, + open_margin_trade_position_iBTC, + priceFeeds, + false, + RBTC, + WRBTC, + SUSD, + accounts + ); + }); + + it("Test close partial margin trade", async () => { + await close_partial_margin_trade_wrbtc( + sovryn, + loanToken, + loanTokenWRBTC, + set_demand_curve, + lend_to_pool_iBTC, + open_margin_trade_position_iBTC, + priceFeeds, + true, + RBTC, + WRBTC, + SUSD, + accounts + ); + await close_partial_margin_trade_wrbtc( + sovryn, + loanToken, + loanTokenWRBTC, + set_demand_curve, + lend_to_pool_iBTC, + open_margin_trade_position_iBTC, + priceFeeds, + false, + RBTC, + WRBTC, + SUSD, + accounts + ); + }); + }); }); diff --git a/tests/loan-token/helpers.js b/tests/loan-token/helpers.js index 81c0fbded..fa1ef322b 100644 --- a/tests/loan-token/helpers.js +++ b/tests/loan-token/helpers.js @@ -5,167 +5,224 @@ const wei = web3.utils.toWei; const oneEth = new BN(wei("1", "ether")); const hunEth = new BN(wei("100", "ether")); -const verify_start_conditions = async (underlyingToken, loanToken, lender, initial_balance, deposit_amount) => { - expect(initial_balance.eq(await underlyingToken.balanceOf(lender))).to.be.true; - expect(new BN(0).eq(await loanToken.totalSupply())).to.be.true; - expect(new BN(0).eq(await loanToken.profitOf(lender))).to.be.true; - expect(new BN(0).eq(await loanToken.checkpointPrice(lender))).to.be.true; - expect(new BN(0).eq(await loanToken.totalSupplyInterestRate(deposit_amount))).to.be.true; +const verify_start_conditions = async ( + underlyingToken, + loanToken, + lender, + initial_balance, + deposit_amount +) => { + expect(initial_balance.eq(await underlyingToken.balanceOf(lender))).to.be.true; + expect(new BN(0).eq(await loanToken.totalSupply())).to.be.true; + expect(new BN(0).eq(await loanToken.profitOf(lender))).to.be.true; + expect(new BN(0).eq(await loanToken.checkpointPrice(lender))).to.be.true; + expect(new BN(0).eq(await loanToken.totalSupplyInterestRate(deposit_amount))).to.be.true; }; const get_itoken_price = (assets_deposited, earned_interests, total_supply) => { - return assets_deposited - .add(earned_interests) - .mul(new BN(wei("1", "ether"))) - .div(total_supply); + return assets_deposited + .add(earned_interests) + .mul(new BN(wei("1", "ether"))) + .div(total_supply); }; const lend_btc_before_cashout = async (loanToken, total_deposit_amount, lender) => { - const initial_balance = new BN(await web3.eth.getBalance(lender)); - expect(initial_balance.gt(total_deposit_amount)).to.be.true; + const initial_balance = new BN(await web3.eth.getBalance(lender)); + expect(initial_balance.gt(total_deposit_amount)).to.be.true; - expect(await loanToken.checkpointPrice(lender)).to.be.a.bignumber.equal(new BN(0)); - await loanToken.mintWithBTC(lender, false, { value: total_deposit_amount }); - expect(await loanToken.checkpointPrice(lender)).to.be.a.bignumber.equal(await loanToken.initialPrice()); + expect(await loanToken.checkpointPrice(lender)).to.be.a.bignumber.equal(new BN(0)); + await loanToken.mintWithBTC(lender, false, { value: total_deposit_amount }); + expect(await loanToken.checkpointPrice(lender)).to.be.a.bignumber.equal( + await loanToken.initialPrice() + ); - const loan_token_initial_balance = total_deposit_amount.div(await loanToken.initialPrice()).mul(new BN(wei("1", "ether"))); + const loan_token_initial_balance = total_deposit_amount + .div(await loanToken.initialPrice()) + .mul(new BN(wei("1", "ether"))); - expect(await loanToken.balanceOf(lender)).to.be.a.bignumber.equal(loan_token_initial_balance); - expect(await loanToken.totalSupply()).to.be.a.bignumber.equal(total_deposit_amount); + expect(await loanToken.balanceOf(lender)).to.be.a.bignumber.equal(loan_token_initial_balance); + expect(await loanToken.totalSupply()).to.be.a.bignumber.equal(total_deposit_amount); - return initial_balance; + return initial_balance; }; -const lend_tokens_before_cashout = async (loanToken, underlyingToken, total_deposit_amount, lender) => { - const initial_balance = await underlyingToken.balanceOf(lender); - expect(initial_balance.gt(total_deposit_amount)).to.be.true; - - await underlyingToken.approve(loanToken.address, total_deposit_amount); - - expect(await loanToken.checkpointPrice(lender)).to.be.a.bignumber.equal(new BN(0)); - await loanToken.mint(lender, total_deposit_amount); - expect(await loanToken.checkpointPrice(lender)).to.be.a.bignumber.equal(await loanToken.initialPrice()); - const loan_token_initial_balance = total_deposit_amount.div(await loanToken.initialPrice()).mul(new BN(wei("1", "ether"))); - expect(await loanToken.balanceOf(lender)).to.be.a.bignumber.equal(loan_token_initial_balance); - expect(await loanToken.totalSupply()).to.be.a.bignumber.equal(total_deposit_amount); - - return initial_balance; +const lend_tokens_before_cashout = async ( + loanToken, + underlyingToken, + total_deposit_amount, + lender +) => { + const initial_balance = await underlyingToken.balanceOf(lender); + expect(initial_balance.gt(total_deposit_amount)).to.be.true; + + await underlyingToken.approve(loanToken.address, total_deposit_amount); + + expect(await loanToken.checkpointPrice(lender)).to.be.a.bignumber.equal(new BN(0)); + await loanToken.mint(lender, total_deposit_amount); + expect(await loanToken.checkpointPrice(lender)).to.be.a.bignumber.equal( + await loanToken.initialPrice() + ); + const loan_token_initial_balance = total_deposit_amount + .div(await loanToken.initialPrice()) + .mul(new BN(wei("1", "ether"))); + expect(await loanToken.balanceOf(lender)).to.be.a.bignumber.equal(loan_token_initial_balance); + expect(await loanToken.totalSupply()).to.be.a.bignumber.equal(total_deposit_amount); + + return initial_balance; }; const verify_lending_result_and_itoken_price_change = async ( - underlyingToken, - collateralToken, - loanToken, - lender, - loan_token_sent, - deposit_amount, - sovryn, - sendValue = false + underlyingToken, + collateralToken, + loanToken, + lender, + loan_token_sent, + deposit_amount, + sovryn, + sendValue = false ) => { - // verify the result - - expect(await loanToken.balanceOf(lender)).to.be.a.bignumber.equal(deposit_amount.div(await loanToken.initialPrice()).mul(oneEth)); - const earned_interests_1 = new BN(0); // Shouldn't be earned interests - const price1 = get_itoken_price(deposit_amount, earned_interests_1, await loanToken.totalSupply()); - expect(await loanToken.tokenPrice()).to.be.a.bignumber.equal(price1); - expect(await loanToken.checkpointPrice(lender)).to.be.a.bignumber.equal(await loanToken.initialPrice()); - expect(await loanToken.totalSupplyInterestRate(deposit_amount)).to.be.a.bignumber.equal(new BN(0)); - - // Should borrow money to get an interest rate different of zero (interest rate depends on the total borrowed amount) - let value = sendValue ? loan_token_sent.toString() : "0"; - await loanToken.marginTrade( - constants.ZERO_BYTES32, // loanId (0 for new loans) - oneEth, // leverageAmount - loan_token_sent.toString(), // loanTokenSent - 0, // no collateral token sent - collateralToken.address, // collateralTokenAddress - lender, // trader, - 0, - "0x", // loanDataBytes (only required with ether), - { - value: value, - } - ); - - // time travel for interest to accumulate - await increaseTime(10000); - - // verify the token price changed according to the gained interest - const price_2 = await loanToken.tokenPrice(); - const lender_interest_data = await sovryn.getLenderInterestData(loanToken.address, underlyingToken.address); - const earned_interest_2 = new BN(lender_interest_data["interestUnPaid"]) - .mul(hunEth.sub(new BN(lender_interest_data["interestFeePercent"]))) - .div(hunEth); - expect(price_2).to.be.a.bignumber.equal(get_itoken_price(deposit_amount, earned_interest_2, await loanToken.totalSupply())); + // verify the result + + expect(await loanToken.balanceOf(lender)).to.be.a.bignumber.equal( + deposit_amount.div(await loanToken.initialPrice()).mul(oneEth) + ); + const earned_interests_1 = new BN(0); // Shouldn't be earned interests + const price1 = get_itoken_price( + deposit_amount, + earned_interests_1, + await loanToken.totalSupply() + ); + expect(await loanToken.tokenPrice()).to.be.a.bignumber.equal(price1); + expect(await loanToken.checkpointPrice(lender)).to.be.a.bignumber.equal( + await loanToken.initialPrice() + ); + expect(await loanToken.totalSupplyInterestRate(deposit_amount)).to.be.a.bignumber.equal( + new BN(0) + ); + + // Should borrow money to get an interest rate different of zero (interest rate depends on the total borrowed amount) + let value = sendValue ? loan_token_sent.toString() : "0"; + await loanToken.marginTrade( + constants.ZERO_BYTES32, // loanId (0 for new loans) + oneEth, // leverageAmount + loan_token_sent.toString(), // loanTokenSent + 0, // no collateral token sent + collateralToken.address, // collateralTokenAddress + lender, // trader, + 0, + "0x", // loanDataBytes (only required with ether), + { + value: value, + } + ); + + // time travel for interest to accumulate + await increaseTime(10000); + + // verify the token price changed according to the gained interest + const price_2 = await loanToken.tokenPrice(); + const lender_interest_data = await sovryn.getLenderInterestData( + loanToken.address, + underlyingToken.address + ); + const earned_interest_2 = new BN(lender_interest_data["interestUnPaid"]) + .mul(hunEth.sub(new BN(lender_interest_data["interestFeePercent"]))) + .div(hunEth); + expect(price_2).to.be.a.bignumber.equal( + get_itoken_price(deposit_amount, earned_interest_2, await loanToken.totalSupply()) + ); }; const lend_to_the_pool = async (loanToken, lender, underlyingToken, collateralToken, sovryn) => { - const baseRate = wei("1", "ether"); - const rateMultiplier = wei("20.25", "ether"); - const targetLevel = wei("80", "ether"); - const kinkLevel = wei("90", "ether"); - const maxScaleRate = wei("100", "ether"); - - await loanToken.setDemandCurve(baseRate, rateMultiplier, baseRate, rateMultiplier, targetLevel, kinkLevel, maxScaleRate); - - borrow_interest_rate = await loanToken.borrowInterestRate(); - expect(borrow_interest_rate.gt(baseRate)).to.be.true; - - const deposit_amount = new BN(wei("400", "ether")); - const loan_token_sent = new BN(wei("100", "ether")); - const total_deposit_amount = deposit_amount.add(loan_token_sent); - const initial_balance = await underlyingToken.balanceOf(lender); - - await underlyingToken.approve(loanToken.address, total_deposit_amount.toString()); - - await verify_start_conditions(underlyingToken, loanToken, lender, initial_balance, deposit_amount); - await loanToken.mint(lender, deposit_amount); - - expect(await underlyingToken.balanceOf(lender)).to.be.a.bignumber.equal(initial_balance.sub(deposit_amount)); - - await verify_lending_result_and_itoken_price_change( - underlyingToken, - collateralToken, - loanToken, - lender, - loan_token_sent, - deposit_amount, - sovryn - ); + const baseRate = wei("1", "ether"); + const rateMultiplier = wei("20.25", "ether"); + const targetLevel = wei("80", "ether"); + const kinkLevel = wei("90", "ether"); + const maxScaleRate = wei("100", "ether"); + + await loanToken.setDemandCurve( + baseRate, + rateMultiplier, + baseRate, + rateMultiplier, + targetLevel, + kinkLevel, + maxScaleRate + ); + + borrow_interest_rate = await loanToken.borrowInterestRate(); + expect(borrow_interest_rate.gt(baseRate)).to.be.true; + + const deposit_amount = new BN(wei("400", "ether")); + const loan_token_sent = new BN(wei("100", "ether")); + const total_deposit_amount = deposit_amount.add(loan_token_sent); + const initial_balance = await underlyingToken.balanceOf(lender); + + await underlyingToken.approve(loanToken.address, total_deposit_amount.toString()); + + await verify_start_conditions( + underlyingToken, + loanToken, + lender, + initial_balance, + deposit_amount + ); + await loanToken.mint(lender, deposit_amount); + + expect(await underlyingToken.balanceOf(lender)).to.be.a.bignumber.equal( + initial_balance.sub(deposit_amount) + ); + + await verify_lending_result_and_itoken_price_change( + underlyingToken, + collateralToken, + loanToken, + lender, + loan_token_sent, + deposit_amount, + sovryn + ); }; const cash_out_from_the_pool = async (loanToken, lender, underlyingToken, lendBTC) => { - const amount_withdrawn = new BN(wei("100", "ether")); - const total_deposit_amount = amount_withdrawn.mul(new BN(2)); - let initial_balance, balance_after_lending; - if (lendBTC) { - initial_balance = await lend_btc_before_cashout(loanToken, total_deposit_amount, lender); - balance_after_lending = new BN(await web3.eth.getBalance(lender)); - await loanToken.burnToBTC(lender, amount_withdrawn.toString(), false); - } else { - initial_balance = await lend_tokens_before_cashout(loanToken, underlyingToken, total_deposit_amount, lender); - await loanToken.burn(lender, amount_withdrawn.toString()); - } - - expect(await loanToken.checkpointPrice(lender)).to.be.a.bignumber.equal(await loanToken.initialPrice()); - - expect(await loanToken.totalSupply()).to.be.a.bignumber.equal(amount_withdrawn); - expect(await loanToken.tokenPrice()).to.be.a.bignumber.equal( - get_itoken_price(amount_withdrawn, new BN(0), await loanToken.totalSupply()) - ); - expect(await loanToken.balanceOf(lender)).to.be.a.bignumber.equal(amount_withdrawn); - if (lendBTC) { - expect(new BN(await web3.eth.getBalance(lender)).gt(balance_after_lending)).to.be.true; - expect( - new BN(await web3.eth.getBalance(lender)).lt( - initial_balance.sub(amount_withdrawn.mul(await loanToken.tokenPrice()).div(oneEth)) - ) - ).to.be.true; - } else { - expect(await underlyingToken.balanceOf(lender)).to.be.a.bignumber.equal( - initial_balance.sub(amount_withdrawn.mul(await loanToken.tokenPrice()).div(oneEth)) - ); - } + const amount_withdrawn = new BN(wei("100", "ether")); + const total_deposit_amount = amount_withdrawn.mul(new BN(2)); + let initial_balance, balance_after_lending; + if (lendBTC) { + initial_balance = await lend_btc_before_cashout(loanToken, total_deposit_amount, lender); + balance_after_lending = new BN(await web3.eth.getBalance(lender)); + await loanToken.burnToBTC(lender, amount_withdrawn.toString(), false); + } else { + initial_balance = await lend_tokens_before_cashout( + loanToken, + underlyingToken, + total_deposit_amount, + lender + ); + await loanToken.burn(lender, amount_withdrawn.toString()); + } + + expect(await loanToken.checkpointPrice(lender)).to.be.a.bignumber.equal( + await loanToken.initialPrice() + ); + + expect(await loanToken.totalSupply()).to.be.a.bignumber.equal(amount_withdrawn); + expect(await loanToken.tokenPrice()).to.be.a.bignumber.equal( + get_itoken_price(amount_withdrawn, new BN(0), await loanToken.totalSupply()) + ); + expect(await loanToken.balanceOf(lender)).to.be.a.bignumber.equal(amount_withdrawn); + if (lendBTC) { + expect(new BN(await web3.eth.getBalance(lender)).gt(balance_after_lending)).to.be.true; + expect( + new BN(await web3.eth.getBalance(lender)).lt( + initial_balance.sub(amount_withdrawn.mul(await loanToken.tokenPrice()).div(oneEth)) + ) + ).to.be.true; + } else { + expect(await underlyingToken.balanceOf(lender)).to.be.a.bignumber.equal( + initial_balance.sub(amount_withdrawn.mul(await loanToken.tokenPrice()).div(oneEth)) + ); + } }; /* @@ -186,41 +243,51 @@ const cash_out_from_the_pool_more_of_lender_balance_should_not_fail = async (loa expect(await underlyingToken.balanceOf(lender)).to.be.a.bignumber.equal(initial_balance); };*/ -const cash_out_from_the_pool_uint256_max_should_withdraw_total_balance = async (loanToken, lender, underlyingToken) => { - const initial_balance = await underlyingToken.balanceOf(lender); - const amount_withdrawn = new BN(wei("100", "ether")); - const total_deposit_amount = amount_withdrawn.mul(new BN(2)); +const cash_out_from_the_pool_uint256_max_should_withdraw_total_balance = async ( + loanToken, + lender, + underlyingToken +) => { + const initial_balance = await underlyingToken.balanceOf(lender); + const amount_withdrawn = new BN(wei("100", "ether")); + const total_deposit_amount = amount_withdrawn.mul(new BN(2)); - expect(initial_balance.gt(total_deposit_amount)).to.be.true; + expect(initial_balance.gt(total_deposit_amount)).to.be.true; - await underlyingToken.approve(loanToken.address, total_deposit_amount.toString()); - await loanToken.mint(lender, total_deposit_amount.toString()); - await expectRevert(loanToken.burn(lender, total_deposit_amount.mul(new BN(2))), "32"); - await loanToken.burn(lender, constants.MAX_UINT256); + await underlyingToken.approve(loanToken.address, total_deposit_amount.toString()); + await loanToken.mint(lender, total_deposit_amount.toString()); + await expectRevert(loanToken.burn(lender, total_deposit_amount.mul(new BN(2))), "32"); + await loanToken.burn(lender, constants.MAX_UINT256); - expect(await loanToken.balanceOf(lender)).to.be.a.bignumber.equal(new BN(0)); - expect(await loanToken.tokenPrice()).to.be.a.bignumber.equal(await loanToken.initialPrice()); - expect(await underlyingToken.balanceOf(lender)).to.be.a.bignumber.equal(initial_balance); + expect(await loanToken.balanceOf(lender)).to.be.a.bignumber.equal(new BN(0)); + expect(await loanToken.tokenPrice()).to.be.a.bignumber.equal(await loanToken.initialPrice()); + expect(await underlyingToken.balanceOf(lender)).to.be.a.bignumber.equal(initial_balance); }; -const set_fee_tokens_held = async (sovryn, underlyingToken, lendingFee, tradingFee, borrowingFee) => { - let totalFeeAmount = lendingFee.add(tradingFee).add(borrowingFee); - await underlyingToken.transfer(sovryn.address, totalFeeAmount); - await sovryn.setLendingFeeTokensHeld(underlyingToken.address, lendingFee); - await sovryn.setTradingFeeTokensHeld(underlyingToken.address, tradingFee); - await sovryn.setBorrowingFeeTokensHeld(underlyingToken.address, borrowingFee); - return totalFeeAmount; +const set_fee_tokens_held = async ( + sovryn, + underlyingToken, + lendingFee, + tradingFee, + borrowingFee +) => { + let totalFeeAmount = lendingFee.add(tradingFee).add(borrowingFee); + await underlyingToken.transfer(sovryn.address, totalFeeAmount); + await sovryn.setLendingFeeTokensHeld(underlyingToken.address, lendingFee); + await sovryn.setTradingFeeTokensHeld(underlyingToken.address, tradingFee); + await sovryn.setBorrowingFeeTokensHeld(underlyingToken.address, borrowingFee); + return totalFeeAmount; }; module.exports = { - verify_start_conditions, - get_itoken_price, - lend_btc_before_cashout, - lend_tokens_before_cashout, - verify_lending_result_and_itoken_price_change, - lend_to_the_pool, - cash_out_from_the_pool, - //cash_out_from_the_pool_more_of_lender_balance_should_not_fail, - cash_out_from_the_pool_uint256_max_should_withdraw_total_balance, - set_fee_tokens_held, + verify_start_conditions, + get_itoken_price, + lend_btc_before_cashout, + lend_tokens_before_cashout, + verify_lending_result_and_itoken_price_change, + lend_to_the_pool, + cash_out_from_the_pool, + //cash_out_from_the_pool_more_of_lender_balance_should_not_fail, + cash_out_from_the_pool_uint256_max_should_withdraw_total_balance, + set_fee_tokens_held, }; diff --git a/tests/loan-token/tokenFunctionality.test.js b/tests/loan-token/tokenFunctionality.test.js index cb517477e..48df5ead3 100644 --- a/tests/loan-token/tokenFunctionality.test.js +++ b/tests/loan-token/tokenFunctionality.test.js @@ -17,17 +17,17 @@ const { loadFixture } = waffle; const { expectRevert, BN, expectEvent } = require("@openzeppelin/test-helpers"); const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getLoanTokenLogic, - getLoanToken, - getLoanTokenWRBTC, - loan_pool_setup, - getPriceFeeds, - getSovryn, - CONSTANTS, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getLoanTokenLogic, + getLoanToken, + getLoanTokenWRBTC, + loan_pool_setup, + getPriceFeeds, + getSovryn, + CONSTANTS, } = require("../Utils/initializer.js"); const wei = web3.utils.toWei; @@ -35,86 +35,102 @@ const wei = web3.utils.toWei; const hunEth = new BN(wei("100", "ether")); const initialize_test_transfer = async (SUSD, accounts, _loan_token) => { - const sender = accounts[0]; - const receiver = accounts[1]; - const amount_to_buy = hunEth; - await SUSD.approve(_loan_token.address, amount_to_buy); - await _loan_token.mint(sender, amount_to_buy); - const sender_initial_balance = await _loan_token.balanceOf(sender); - const amount_sent = sender_initial_balance.div(new BN(2)); - - return { amount_sent, receiver, sender }; + const sender = accounts[0]; + const receiver = accounts[1]; + const amount_to_buy = hunEth; + await SUSD.approve(_loan_token.address, amount_to_buy); + await _loan_token.mint(sender, amount_to_buy); + const sender_initial_balance = await _loan_token.balanceOf(sender); + const amount_sent = sender_initial_balance.div(new BN(2)); + + return { amount_sent, receiver, sender }; }; contract("LoanTokenFunctionality", (accounts) => { - let owner; - let sovryn, SUSD, WRBTC, RBTC, BZRX, loanToken, loanTokenWRBTC; - let amount_sent, receiver, sender; - - async function deploymentAndInitFixture(_wallets, _provider) { - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - const priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - - loanToken = await getLoanToken(owner, sovryn, WRBTC, SUSD); - loanTokenWRBTC = await getLoanTokenWRBTC(owner, sovryn, WRBTC, SUSD); - await loan_pool_setup(sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC); - - let r = await initialize_test_transfer(SUSD, accounts, loanToken); - amount_sent = r.amount_sent; - receiver = r.receiver; - sender = r.sender; - } - - before(async () => { - [owner] = accounts; - }); - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - describe("Test token transfer functionality of the loan token contract", () => { - it("Test transfer", async () => { - const tx = await loanToken.transfer(receiver, amount_sent.toString()); - expect((await loanToken.balanceOf(sender)).eq(amount_sent)).to.be.true; - expect((await loanToken.balanceOf(receiver)).eq(amount_sent)).to.be.true; - expect((await loanToken.checkpointPrice(sender)).eq(await loanToken.initialPrice())).to.be.true; - expect((await loanToken.checkpointPrice(receiver)).eq(await loanToken.initialPrice())).to.be.true; - - expectEvent(tx, "Transfer", { from: sender, to: receiver, value: amount_sent.toString() }); - }); - - it("Test transfer with insufficient balance", async () => { - expectRevert(loanToken.transfer(sender, amount_sent.toString(), { from: receiver }), "16"); - }); - - it("Test transfer to zero account should fail", async () => { - expectRevert(loanToken.transfer(CONSTANTS.ZERO_ADDRESS, amount_sent.toString()), "15"); - }); - - it("Test transfer to self", async () => { - const initial_balance = await loanToken.balanceOf(sender); - // transfer the tokens to the sender - await loanToken.transfer(sender, amount_sent); - expect((await loanToken.balanceOf(sender)).eq(initial_balance)).to.be.true; - }); - - it("Test transfer to from", async () => { - await loanToken.approve(receiver, amount_sent); - - expect((await loanToken.allowance(sender, receiver)).eq(amount_sent)).to.be.true; - - let tx = await loanToken.transferFrom(sender, receiver, amount_sent, { from: receiver }); - expect((await loanToken.balanceOf(sender)).eq(amount_sent)).to.be.true; - expect((await loanToken.balanceOf(receiver)).eq(amount_sent)).to.be.true; - - // Expect the AllowanceUpdate event triggered at TestToken::transferFrom - expectEvent(tx, "AllowanceUpdate", { owner: sender, spender: receiver, valueBefore: amount_sent, valueAfter: new BN(0) }); - }); - }); + let owner; + let sovryn, SUSD, WRBTC, RBTC, BZRX, loanToken, loanTokenWRBTC; + let amount_sent, receiver, sender; + + async function deploymentAndInitFixture(_wallets, _provider) { + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + const priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + + loanToken = await getLoanToken(owner, sovryn, WRBTC, SUSD); + loanTokenWRBTC = await getLoanTokenWRBTC(owner, sovryn, WRBTC, SUSD); + await loan_pool_setup(sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC); + + let r = await initialize_test_transfer(SUSD, accounts, loanToken); + amount_sent = r.amount_sent; + receiver = r.receiver; + sender = r.sender; + } + + before(async () => { + [owner] = accounts; + }); + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + describe("Test token transfer functionality of the loan token contract", () => { + it("Test transfer", async () => { + const tx = await loanToken.transfer(receiver, amount_sent.toString()); + expect((await loanToken.balanceOf(sender)).eq(amount_sent)).to.be.true; + expect((await loanToken.balanceOf(receiver)).eq(amount_sent)).to.be.true; + expect((await loanToken.checkpointPrice(sender)).eq(await loanToken.initialPrice())).to + .be.true; + expect((await loanToken.checkpointPrice(receiver)).eq(await loanToken.initialPrice())) + .to.be.true; + + expectEvent(tx, "Transfer", { + from: sender, + to: receiver, + value: amount_sent.toString(), + }); + }); + + it("Test transfer with insufficient balance", async () => { + expectRevert( + loanToken.transfer(sender, amount_sent.toString(), { from: receiver }), + "16" + ); + }); + + it("Test transfer to zero account should fail", async () => { + expectRevert(loanToken.transfer(CONSTANTS.ZERO_ADDRESS, amount_sent.toString()), "15"); + }); + + it("Test transfer to self", async () => { + const initial_balance = await loanToken.balanceOf(sender); + // transfer the tokens to the sender + await loanToken.transfer(sender, amount_sent); + expect((await loanToken.balanceOf(sender)).eq(initial_balance)).to.be.true; + }); + + it("Test transfer to from", async () => { + await loanToken.approve(receiver, amount_sent); + + expect((await loanToken.allowance(sender, receiver)).eq(amount_sent)).to.be.true; + + let tx = await loanToken.transferFrom(sender, receiver, amount_sent, { + from: receiver, + }); + expect((await loanToken.balanceOf(sender)).eq(amount_sent)).to.be.true; + expect((await loanToken.balanceOf(receiver)).eq(amount_sent)).to.be.true; + + // Expect the AllowanceUpdate event triggered at TestToken::transferFrom + expectEvent(tx, "AllowanceUpdate", { + owner: sender, + spender: receiver, + valueBefore: amount_sent, + valueAfter: new BN(0), + }); + }); + }); }); diff --git a/tests/loan-token/tradingFunctions.js b/tests/loan-token/tradingFunctions.js index 556cf6c0b..a3b73d412 100644 --- a/tests/loan-token/tradingFunctions.js +++ b/tests/loan-token/tradingFunctions.js @@ -20,218 +20,264 @@ process: 3. verify the trade event and balances are correct 4. retrieve the loan from the smart contract and make sure all values are set as expected */ -const margin_trading_sending_loan_tokens = async (accounts, sovryn, loanToken, underlyingToken, collateralToken, priceFeeds, sendValue) => { - // preparation - const loan_token_sent = oneEth.mul(new BN(10)); - await underlyingToken.mint(loanToken.address, loan_token_sent.mul(new BN(3))); - await underlyingToken.mint(accounts[0], loan_token_sent); - await underlyingToken.approve(loanToken.address, loan_token_sent); - const value = sendValue ? loan_token_sent : 0; - - // send the transaction - const leverage_amount = oneEth.mul(new BN(2)); - const collateral_sent = new BN(0); - const { receipt } = await loanToken.marginTrade( - constants.ZERO_BYTES32, //loanId (0 for new loans) - leverage_amount.toString(), // leverageAmount - loan_token_sent.toString(), // loanTokenSent - collateral_sent.toString(), // no collateral token sent - collateralToken.address, // collateralTokenAddress - accounts[0], // trader, - 0, // slippage - "0x", // loanDataBytes (only required with ether) - { value: value } - ); - - // check the balances and the trade event - const sovryn_after_collateral_token_balance = await collateralToken.balanceOf(sovryn.address); - const loantoken_after_underlying_token_balance = await underlyingToken.balanceOf(loanToken.address); - const decode = decodeLogs(receipt.rawLogs, LoanOpenings, "Trade"); - const args = decode[0].args; - expect(args["borrowedAmount"]).to.equal(loan_token_sent.mul(new BN(2)).toString()); - expect(args["positionSize"]).to.equal(sovryn_after_collateral_token_balance.toString()); - expect(loan_token_sent.mul(new BN(3)).sub(new BN(args["borrowedAmount"]))).to.be.a.bignumber.eq( - loantoken_after_underlying_token_balance - ); - - // compute the expected values of the loan object - const start_margin = new BN(10).pow(new BN(38)).div(leverage_amount); - const total_deposit = loan_token_sent.add(collateral_sent); - const { rate: trade_rate, precision: trade_rate_precision } = await priceFeeds.queryRate( - underlyingToken.address, - collateralToken.address - ); - const { rate: collateral_to_loan_rate, precision: collateral_to_loan_precision } = await priceFeeds.queryRate( - collateralToken.address, - underlyingToken.address - ); - - const collateral_to_loan_swap_rate = collateral_to_loan_precision.mul(trade_rate_precision).div(new BN(args["entryPrice"])); - const interest_rate = await loanToken.nextBorrowInterestRate(total_deposit.mul(hunEth).div(start_margin)); - const principal = loan_token_sent.mul(BN2); - const seconds_per_day = new BN(24 * 60 * 60); - const max_loan_duration = seconds_per_day.mul(new BN(28)); - const seconds_per_year = seconds_per_day.mul(new BN(365)); - const borrow_amount = total_deposit - .mul(new BN(10).pow(new BN(40))) - .div(interest_rate.div(seconds_per_year).mul(max_loan_duration).div(start_margin).mul(hunEth).add(hunEth)) - .div(start_margin); - const owed_per_day = borrow_amount.mul(interest_rate).div(new BN(365).mul(hunEth)); - const interest_amount_required = new BN(28).mul(owed_per_day).div(seconds_per_day); - const trading_fee = loan_token_sent - .add(borrow_amount) - .mul(new BN(15).mul(new BN(10).pow(new BN(16)))) - .div(hunEth); // 0.15% fee - const collateral = collateral_sent - .add(loan_token_sent) - .add(borrow_amount) - .sub(interest_amount_required) - .sub(trading_fee) - .mul(trade_rate) - .div(trade_rate_precision); - //current_margin = (collateral * collateral_to_loan_rate / 1e18 - principal) / principal * 1e20 - const current_margin = collateral.mul(collateral_to_loan_rate).div(oneEth).sub(principal).mul(hunEth).div(principal); - //TODO: problem: rounding error somewhere - - const loan_id = args["loanId"]; - const loan = await sovryn.getLoan(loan_id); - const loanV2 = await sovryn.getLoanV2(loan_id); - const end_timestamp = loan["endTimestamp"]; - const num = await blockNumber(); - let currentBlock = await web3.eth.getBlock(num); - const block_timestamp = currentBlock.timestamp; - - const interest_deposit_remaining = - end_timestamp >= block_timestamp ? new BN(end_timestamp).sub(new BN(block_timestamp)).mul(owed_per_day).div(seconds_per_day) : 0; - // assert the loan object is set as expected - expect(loan["loanToken"]).to.equal(underlyingToken.address); - expect(loan["collateralToken"]).to.equal(collateralToken.address); - expect(loan["principal"]).to.equal(principal.toString()); - expect(loan["collateral"]).to.equal(collateral.toString()); - - expect(loan["interestOwedPerDay"]).to.equal(owed_per_day.toString()); - expect(loan["interestDepositRemaining"]).to.eq(interest_deposit_remaining.toString()); - expect(loan["startRate"]).to.eq(collateral_to_loan_swap_rate.toString()); - expect(loan["startMargin"]).to.eq(start_margin.toString()); - expect(loan["maintenanceMargin"]).to.eq(new BN(15).mul(oneEth).toString()); - expect(loan["currentMargin"]).to.eq(current_margin.toString()); - expect(loan["maxLoanTerm"]).to.eq(max_loan_duration.toString()); // In the SC is hardcoded to 28 days - - expect(new BN(block_timestamp).add(max_loan_duration).sub(new BN(end_timestamp)).lt(new BN(1))).to.be.true; - expect(loan["maxLiquidatable"]).to.eq("0"); - expect(loan["maxSeizable"]).to.eq("0"); - - // validate loanV2 data struct - expect(loanV2["loanToken"]).to.equal(underlyingToken.address); - expect(loanV2["collateralToken"]).to.equal(collateralToken.address); - expect(loanV2["principal"]).to.equal(principal.toString()); - expect(loanV2["collateral"]).to.equal(collateral.toString()); - - expect(loanV2["interestOwedPerDay"]).to.equal(owed_per_day.toString()); - expect(loanV2["interestDepositRemaining"]).to.eq(interest_deposit_remaining.toString()); - expect(loanV2["startRate"]).to.eq(collateral_to_loan_swap_rate.toString()); - expect(loanV2["startMargin"]).to.eq(start_margin.toString()); - expect(loanV2["maintenanceMargin"]).to.eq(new BN(15).mul(oneEth).toString()); - expect(loanV2["currentMargin"]).to.eq(current_margin.toString()); - expect(loanV2["maxLoanTerm"]).to.eq(max_loan_duration.toString()); // In the SC is hardcoded to 28 days - - expect(loanV2["maxLiquidatable"]).to.eq("0"); - expect(loanV2["maxSeizable"]).to.eq("0"); - expect(loanV2["borrower"]).to.eq(accounts[0]); - expect(loanV2["creationTimestamp"]).to.eq(block_timestamp.toString()); +const margin_trading_sending_loan_tokens = async ( + accounts, + sovryn, + loanToken, + underlyingToken, + collateralToken, + priceFeeds, + sendValue +) => { + // preparation + const loan_token_sent = oneEth.mul(new BN(10)); + await underlyingToken.mint(loanToken.address, loan_token_sent.mul(new BN(3))); + await underlyingToken.mint(accounts[0], loan_token_sent); + await underlyingToken.approve(loanToken.address, loan_token_sent); + const value = sendValue ? loan_token_sent : 0; + + // send the transaction + const leverage_amount = oneEth.mul(new BN(2)); + const collateral_sent = new BN(0); + const { receipt } = await loanToken.marginTrade( + constants.ZERO_BYTES32, //loanId (0 for new loans) + leverage_amount.toString(), // leverageAmount + loan_token_sent.toString(), // loanTokenSent + collateral_sent.toString(), // no collateral token sent + collateralToken.address, // collateralTokenAddress + accounts[0], // trader, + 0, // slippage + "0x", // loanDataBytes (only required with ether) + { value: value } + ); + + // check the balances and the trade event + const sovryn_after_collateral_token_balance = await collateralToken.balanceOf(sovryn.address); + const loantoken_after_underlying_token_balance = await underlyingToken.balanceOf( + loanToken.address + ); + const decode = decodeLogs(receipt.rawLogs, LoanOpenings, "Trade"); + const args = decode[0].args; + expect(args["borrowedAmount"]).to.equal(loan_token_sent.mul(new BN(2)).toString()); + expect(args["positionSize"]).to.equal(sovryn_after_collateral_token_balance.toString()); + expect( + loan_token_sent.mul(new BN(3)).sub(new BN(args["borrowedAmount"])) + ).to.be.a.bignumber.eq(loantoken_after_underlying_token_balance); + + // compute the expected values of the loan object + const start_margin = new BN(10).pow(new BN(38)).div(leverage_amount); + const total_deposit = loan_token_sent.add(collateral_sent); + const { rate: trade_rate, precision: trade_rate_precision } = await priceFeeds.queryRate( + underlyingToken.address, + collateralToken.address + ); + const { rate: collateral_to_loan_rate, precision: collateral_to_loan_precision } = + await priceFeeds.queryRate(collateralToken.address, underlyingToken.address); + + const collateral_to_loan_swap_rate = collateral_to_loan_precision + .mul(trade_rate_precision) + .div(new BN(args["entryPrice"])); + const interest_rate = await loanToken.nextBorrowInterestRate( + total_deposit.mul(hunEth).div(start_margin) + ); + const principal = loan_token_sent.mul(BN2); + const seconds_per_day = new BN(24 * 60 * 60); + const max_loan_duration = seconds_per_day.mul(new BN(28)); + const seconds_per_year = seconds_per_day.mul(new BN(365)); + const borrow_amount = total_deposit + .mul(new BN(10).pow(new BN(40))) + .div( + interest_rate + .div(seconds_per_year) + .mul(max_loan_duration) + .div(start_margin) + .mul(hunEth) + .add(hunEth) + ) + .div(start_margin); + const owed_per_day = borrow_amount.mul(interest_rate).div(new BN(365).mul(hunEth)); + const interest_amount_required = new BN(28).mul(owed_per_day).div(seconds_per_day); + const trading_fee = loan_token_sent + .add(borrow_amount) + .mul(new BN(15).mul(new BN(10).pow(new BN(16)))) + .div(hunEth); // 0.15% fee + const collateral = collateral_sent + .add(loan_token_sent) + .add(borrow_amount) + .sub(interest_amount_required) + .sub(trading_fee) + .mul(trade_rate) + .div(trade_rate_precision); + //current_margin = (collateral * collateral_to_loan_rate / 1e18 - principal) / principal * 1e20 + const current_margin = collateral + .mul(collateral_to_loan_rate) + .div(oneEth) + .sub(principal) + .mul(hunEth) + .div(principal); + //TODO: problem: rounding error somewhere + + const loan_id = args["loanId"]; + const loan = await sovryn.getLoan(loan_id); + const loanV2 = await sovryn.getLoanV2(loan_id); + const end_timestamp = loan["endTimestamp"]; + const num = await blockNumber(); + let currentBlock = await web3.eth.getBlock(num); + const block_timestamp = currentBlock.timestamp; + + const interest_deposit_remaining = + end_timestamp >= block_timestamp + ? new BN(end_timestamp) + .sub(new BN(block_timestamp)) + .mul(owed_per_day) + .div(seconds_per_day) + : 0; + // assert the loan object is set as expected + expect(loan["loanToken"]).to.equal(underlyingToken.address); + expect(loan["collateralToken"]).to.equal(collateralToken.address); + expect(loan["principal"]).to.equal(principal.toString()); + expect(loan["collateral"]).to.equal(collateral.toString()); + + expect(loan["interestOwedPerDay"]).to.equal(owed_per_day.toString()); + expect(loan["interestDepositRemaining"]).to.eq(interest_deposit_remaining.toString()); + expect(loan["startRate"]).to.eq(collateral_to_loan_swap_rate.toString()); + expect(loan["startMargin"]).to.eq(start_margin.toString()); + expect(loan["maintenanceMargin"]).to.eq(new BN(15).mul(oneEth).toString()); + expect(loan["currentMargin"]).to.eq(current_margin.toString()); + expect(loan["maxLoanTerm"]).to.eq(max_loan_duration.toString()); // In the SC is hardcoded to 28 days + + expect(new BN(block_timestamp).add(max_loan_duration).sub(new BN(end_timestamp)).lt(new BN(1))) + .to.be.true; + expect(loan["maxLiquidatable"]).to.eq("0"); + expect(loan["maxSeizable"]).to.eq("0"); + + // validate loanV2 data struct + expect(loanV2["loanToken"]).to.equal(underlyingToken.address); + expect(loanV2["collateralToken"]).to.equal(collateralToken.address); + expect(loanV2["principal"]).to.equal(principal.toString()); + expect(loanV2["collateral"]).to.equal(collateral.toString()); + + expect(loanV2["interestOwedPerDay"]).to.equal(owed_per_day.toString()); + expect(loanV2["interestDepositRemaining"]).to.eq(interest_deposit_remaining.toString()); + expect(loanV2["startRate"]).to.eq(collateral_to_loan_swap_rate.toString()); + expect(loanV2["startMargin"]).to.eq(start_margin.toString()); + expect(loanV2["maintenanceMargin"]).to.eq(new BN(15).mul(oneEth).toString()); + expect(loanV2["currentMargin"]).to.eq(current_margin.toString()); + expect(loanV2["maxLoanTerm"]).to.eq(max_loan_duration.toString()); // In the SC is hardcoded to 28 days + + expect(loanV2["maxLiquidatable"]).to.eq("0"); + expect(loanV2["maxSeizable"]).to.eq("0"); + expect(loanV2["borrower"]).to.eq(accounts[0]); + expect(loanV2["creationTimestamp"]).to.eq(block_timestamp.toString()); }; -const margin_trading_sov_reward_payment = async (accounts, loanToken, underlyingToken, collateralToken, SOV, FeesEvents, sovryn) => { - // preparation - const loan_token_sent = oneEth.mul(new BN(10)); - await underlyingToken.mint(loanToken.address, loan_token_sent.mul(new BN(3))); - const trader = accounts[0]; - await underlyingToken.mint(trader, loan_token_sent); - await underlyingToken.approve(loanToken.address, loan_token_sent); - - // send the transaction - const leverage_amount = oneEth.mul(BN2); - const collateral_sent = new BN(0); - lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); - const sov_initial_balance = (await SOV.balanceOf(trader)).add(await lockedSOV.getLockedBalance(trader)); - - const { receipt } = await loanToken.marginTrade( - constants.ZERO_BYTES32, // loanId (0 for new loans) - leverage_amount.toString(), // leverageAmount - loan_token_sent.toString(), // loanTokenSent - collateral_sent.toString(), // no collateral token sent - collateralToken.address, // collateralTokenAddress - trader, // trader, - 0, // slippage - "0x" // loanDataBytes (only required with ether) - ); - - await increaseTime(10 * 24 * 60 * 60); - - const loan_id = constants.ZERO_BYTES32; // is zero because is a new loan - await verify_sov_reward_payment( - receipt.rawLogs, - FeesEvents, - SOV, - trader, - loan_id, - sov_initial_balance, - 1, - underlyingToken.address, - collateralToken.address, - sovryn - ); +const margin_trading_sov_reward_payment = async ( + accounts, + loanToken, + underlyingToken, + collateralToken, + SOV, + FeesEvents, + sovryn +) => { + // preparation + const loan_token_sent = oneEth.mul(new BN(10)); + await underlyingToken.mint(loanToken.address, loan_token_sent.mul(new BN(3))); + const trader = accounts[0]; + await underlyingToken.mint(trader, loan_token_sent); + await underlyingToken.approve(loanToken.address, loan_token_sent); + + // send the transaction + const leverage_amount = oneEth.mul(BN2); + const collateral_sent = new BN(0); + lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); + const sov_initial_balance = (await SOV.balanceOf(trader)).add( + await lockedSOV.getLockedBalance(trader) + ); + + const { receipt } = await loanToken.marginTrade( + constants.ZERO_BYTES32, // loanId (0 for new loans) + leverage_amount.toString(), // leverageAmount + loan_token_sent.toString(), // loanTokenSent + collateral_sent.toString(), // no collateral token sent + collateralToken.address, // collateralTokenAddress + trader, // trader, + 0, // slippage + "0x" // loanDataBytes (only required with ether) + ); + + await increaseTime(10 * 24 * 60 * 60); + + const loan_id = constants.ZERO_BYTES32; // is zero because is a new loan + await verify_sov_reward_payment( + receipt.rawLogs, + FeesEvents, + SOV, + trader, + loan_id, + sov_initial_balance, + 1, + underlyingToken.address, + collateralToken.address, + sovryn + ); }; const margin_trading_sov_reward_payment_with_special_rebates = async ( - accounts, - loanToken, - underlyingToken, - collateralToken, - SOV, - FeesEvents, - sovryn + accounts, + loanToken, + underlyingToken, + collateralToken, + SOV, + FeesEvents, + sovryn ) => { - // preparation - const loan_token_sent = oneEth; - await underlyingToken.mint(loanToken.address, loan_token_sent.mul(new BN(3))); - const trader = accounts[0]; - await underlyingToken.mint(trader, loan_token_sent); - await underlyingToken.approve(loanToken.address, loan_token_sent); - await sovryn.setSpecialRebates(underlyingToken.address, collateralToken.address, wei("300", "ether")); - - // send the transaction - const leverage_amount = oneEth.mul(BN2); - const collateral_sent = new BN(0); - lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); - const sov_initial_balance = (await SOV.balanceOf(trader)).add(await lockedSOV.getLockedBalance(trader)); - - const { receipt } = await loanToken.marginTrade( - constants.ZERO_BYTES32, // loanId (0 for new loans) - leverage_amount.toString(), // leverageAmount - loan_token_sent.toString(), // loanTokenSent - collateral_sent.toString(), // no collateral token sent - collateralToken.address, // collateralTokenAddress - trader, // trader, - 0, // slippage - "0x" // loanDataBytes (only required with ether) - ); - - await increaseTime(10 * 24 * 60 * 60); - - const loan_id = constants.ZERO_BYTES32; // is zero because is a new loan - await verify_sov_reward_payment( - receipt.rawLogs, - FeesEvents, - SOV, - trader, - loan_id, - sov_initial_balance, - 1, - underlyingToken.address, - collateralToken.address, - sovryn - ); + // preparation + const loan_token_sent = oneEth; + await underlyingToken.mint(loanToken.address, loan_token_sent.mul(new BN(3))); + const trader = accounts[0]; + await underlyingToken.mint(trader, loan_token_sent); + await underlyingToken.approve(loanToken.address, loan_token_sent); + await sovryn.setSpecialRebates( + underlyingToken.address, + collateralToken.address, + wei("300", "ether") + ); + + // send the transaction + const leverage_amount = oneEth.mul(BN2); + const collateral_sent = new BN(0); + lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); + const sov_initial_balance = (await SOV.balanceOf(trader)).add( + await lockedSOV.getLockedBalance(trader) + ); + + const { receipt } = await loanToken.marginTrade( + constants.ZERO_BYTES32, // loanId (0 for new loans) + leverage_amount.toString(), // leverageAmount + loan_token_sent.toString(), // loanTokenSent + collateral_sent.toString(), // no collateral token sent + collateralToken.address, // collateralTokenAddress + trader, // trader, + 0, // slippage + "0x" // loanDataBytes (only required with ether) + ); + + await increaseTime(10 * 24 * 60 * 60); + + const loan_id = constants.ZERO_BYTES32; // is zero because is a new loan + await verify_sov_reward_payment( + receipt.rawLogs, + FeesEvents, + SOV, + trader, + loan_id, + sov_initial_balance, + 1, + underlyingToken.address, + collateralToken.address, + sovryn + ); }; /* @@ -241,144 +287,158 @@ process: 2. TODO verify the trade event and balances are correct */ const margin_trading_sending_collateral_tokens = async ( - accounts, - loanToken, - underlyingToken, - collateralToken, - loanSize, - collateralTokenSent, - leverageAmount, - value, - priceFeeds + accounts, + loanToken, + underlyingToken, + collateralToken, + loanSize, + collateralTokenSent, + leverageAmount, + value, + priceFeeds ) => { - await get_estimated_margin_details(loanToken, collateralToken, loanSize, collateralTokenSent, leverageAmount); - - const { rate } = await priceFeeds.queryRate(underlyingToken.address, collateralToken.address); - const { receipt } = await loanToken.marginTrade( - constants.ZERO_BYTES32, //loanId (0 for new loans) - leverageAmount, // leverageAmount - 0, - collateralTokenSent, - collateralToken.address, - accounts[0], - 0, - "0x", - { value: value } - ); - - const decode = decodeLogs(receipt.rawLogs, LoanOpenings, "Trade"); - const args = decode[0].args; - - expect(args["positionSize"]).to.eq( - new BN(args["borrowedAmount"]).mul(new BN(args["entryPrice"]) /*.addn(1)*/).div(oneEth).add(collateralTokenSent).toString() - ); //addn(1) - rounding error if used with p3.9 from bzx peckshield-audit-report-bZxV2-v1.0rc1.pdf; cannot be applied solely as it drives to some other tests failure - - expect(args["borrowedAmount"]).to.eq( - loanSize - .mul(collateralTokenSent) - .mul(leverageAmount) - .div(new BN(10).pow(new BN(36))) - .toString() - ); - expect(args["interestRate"]).to.eq("0"); - expect(args["entryPrice"]).to.eq( - rate - .mul(new BN(9985)) - .div(new BN(10000)) /*.sub(new BN(1))*/ - .toString() - ); //9985 == (1-0.15/100); sub(1) - rounding error // p3.9 from bzx peckshield-audit-report-bZxV2-v1.0rc1.pdf; cannot be applied solely as it drives to some other tests failure - expect(args["entryLeverage"]).to.eq(leverageAmount.toString()); + await get_estimated_margin_details( + loanToken, + collateralToken, + loanSize, + collateralTokenSent, + leverageAmount + ); + + const { rate } = await priceFeeds.queryRate(underlyingToken.address, collateralToken.address); + const { receipt } = await loanToken.marginTrade( + constants.ZERO_BYTES32, //loanId (0 for new loans) + leverageAmount, // leverageAmount + 0, + collateralTokenSent, + collateralToken.address, + accounts[0], + 0, + "0x", + { value: value } + ); + + const decode = decodeLogs(receipt.rawLogs, LoanOpenings, "Trade"); + const args = decode[0].args; + + expect(args["positionSize"]).to.eq( + new BN(args["borrowedAmount"]) + .mul(new BN(args["entryPrice"]) /*.addn(1)*/) + .div(oneEth) + .add(collateralTokenSent) + .toString() + ); //addn(1) - rounding error if used with p3.9 from bzx peckshield-audit-report-bZxV2-v1.0rc1.pdf; cannot be applied solely as it drives to some other tests failure + + expect(args["borrowedAmount"]).to.eq( + loanSize + .mul(collateralTokenSent) + .mul(leverageAmount) + .div(new BN(10).pow(new BN(36))) + .toString() + ); + expect(args["interestRate"]).to.eq("0"); + expect(args["entryPrice"]).to.eq( + rate + .mul(new BN(9985)) + .div(new BN(10000)) /*.sub(new BN(1))*/ + .toString() + ); //9985 == (1-0.15/100); sub(1) - rounding error // p3.9 from bzx peckshield-audit-report-bZxV2-v1.0rc1.pdf; cannot be applied solely as it drives to some other tests failure + expect(args["entryLeverage"]).to.eq(leverageAmount.toString()); }; const margin_trading_sending_collateral_tokens_sov_reward_payment = async ( - trader, - loanToken, - underlyingToken, - collateralToken, - collateralTokenSent, - leverageAmount, - value, - FeesEvents, - SOV, - sovryn + trader, + loanToken, + underlyingToken, + collateralToken, + collateralTokenSent, + leverageAmount, + value, + FeesEvents, + SOV, + sovryn ) => { - const sov_initial_balance = await SOV.balanceOf(trader); - const { receipt } = await loanToken.marginTrade( - constants.ZERO_BYTES32, - leverageAmount, - 0, - collateralTokenSent, - collateralToken.address, - trader, - 0, - "0x", - { - from: trader, - value: value, - } - ); - - await increaseTime(10 * 24 * 60 * 60); - - const loan_id = constants.ZERO_BYTES32; // is zero because is a new loan - await verify_sov_reward_payment( - receipt.rawLogs, - FeesEvents, - SOV, - trader, - loan_id, - sov_initial_balance, - 1, - underlyingToken.address, - collateralToken.address, - sovryn - ); + const sov_initial_balance = await SOV.balanceOf(trader); + const { receipt } = await loanToken.marginTrade( + constants.ZERO_BYTES32, + leverageAmount, + 0, + collateralTokenSent, + collateralToken.address, + trader, + 0, + "0x", + { + from: trader, + value: value, + } + ); + + await increaseTime(10 * 24 * 60 * 60); + + const loan_id = constants.ZERO_BYTES32; // is zero because is a new loan + await verify_sov_reward_payment( + receipt.rawLogs, + FeesEvents, + SOV, + trader, + loan_id, + sov_initial_balance, + 1, + underlyingToken.address, + collateralToken.address, + sovryn + ); }; const margin_trading_sending_collateral_tokens_sov_reward_payment_with_special_rebates = async ( - trader, - loanToken, - underlyingToken, - collateralToken, - collateralTokenSent, - leverageAmount, - value, - FeesEvents, - SOV, - sovryn + trader, + loanToken, + underlyingToken, + collateralToken, + collateralTokenSent, + leverageAmount, + value, + FeesEvents, + SOV, + sovryn ) => { - await sovryn.setSpecialRebates(underlyingToken.address, collateralToken.address, wei("30", "ether")); - const sov_initial_balance = await SOV.balanceOf(trader); - const { receipt } = await loanToken.marginTrade( - constants.ZERO_BYTES32, - leverageAmount, - 0, - collateralTokenSent, - collateralToken.address, - trader, - 0, - "0x", - { - from: trader, - value: value, - } - ); - - await increaseTime(10 * 24 * 60 * 60); - - const loan_id = constants.ZERO_BYTES32; // is zero because is a new loan - await verify_sov_reward_payment( - receipt.rawLogs, - FeesEvents, - SOV, - trader, - loan_id, - sov_initial_balance, - 1, - underlyingToken.address, - collateralToken.address, - sovryn - ); + await sovryn.setSpecialRebates( + underlyingToken.address, + collateralToken.address, + wei("30", "ether") + ); + const sov_initial_balance = await SOV.balanceOf(trader); + const { receipt } = await loanToken.marginTrade( + constants.ZERO_BYTES32, + leverageAmount, + 0, + collateralTokenSent, + collateralToken.address, + trader, + 0, + "0x", + { + from: trader, + value: value, + } + ); + + await increaseTime(10 * 24 * 60 * 60); + + const loan_id = constants.ZERO_BYTES32; // is zero because is a new loan + await verify_sov_reward_payment( + receipt.rawLogs, + FeesEvents, + SOV, + trader, + loan_id, + sov_initial_balance, + 1, + underlyingToken.address, + collateralToken.address, + sovryn + ); }; /* @@ -390,174 +450,226 @@ close a position completely. 5. verifies the result */ const close_complete_margin_trade = async ( - sovryn, - loanToken, - set_demand_curve, - lend_to_pool, - open_margin_trade_position, - priceFeeds, - return_token_is_collateral, - RBTC, - WRBTC, - SUSD, - accounts + sovryn, + loanToken, + set_demand_curve, + lend_to_pool, + open_margin_trade_position, + priceFeeds, + return_token_is_collateral, + RBTC, + WRBTC, + SUSD, + accounts ) => { - // prepare the test - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, accounts[0]); - const [loan_id, trader, loan_token_sent] = await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, accounts[1]); - - await increaseTime(10 * 24 * 60 * 60); - const initial_loan = await sovryn.getLoan(loan_id); - - // needs to be called by the trader - expectRevert(sovryn.closeWithSwap(loan_id, trader, loan_token_sent, return_token_is_collateral, "0x"), "unauthorized"); - - // complete closure means the whole collateral is swapped - const swap_amount = initial_loan["collateral"]; - - await internal_test_close_margin_trade( - new BN(swap_amount), - initial_loan, - loanToken, - loan_id, - priceFeeds, - sovryn, - trader, - return_token_is_collateral - ); + // prepare the test + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, accounts[0]); + const [loan_id, trader, loan_token_sent] = await open_margin_trade_position( + loanToken, + RBTC, + WRBTC, + SUSD, + accounts[1] + ); + + await increaseTime(10 * 24 * 60 * 60); + const initial_loan = await sovryn.getLoan(loan_id); + + // needs to be called by the trader + expectRevert( + sovryn.closeWithSwap(loan_id, trader, loan_token_sent, return_token_is_collateral, "0x"), + "unauthorized" + ); + + // complete closure means the whole collateral is swapped + const swap_amount = initial_loan["collateral"]; + + await internal_test_close_margin_trade( + new BN(swap_amount), + initial_loan, + loanToken, + loan_id, + priceFeeds, + sovryn, + trader, + return_token_is_collateral + ); }; const close_complete_margin_trade_wrbtc = async ( - sovryn, - loanToken, - loanTokenWRBTC, - set_demand_curve, - lend_to_pool_iBTC, - open_margin_trade_position_iBTC, - priceFeeds, - return_token_is_collateral, - RBTC, - WRBTC, - SUSD, - accounts + sovryn, + loanToken, + loanTokenWRBTC, + set_demand_curve, + lend_to_pool_iBTC, + open_margin_trade_position_iBTC, + priceFeeds, + return_token_is_collateral, + RBTC, + WRBTC, + SUSD, + accounts ) => { - // prepare the test - await set_demand_curve(loanToken); - await lend_to_pool_iBTC(loanTokenWRBTC, accounts[0]); - const [loan_id, trader, loan_token_sent] = await open_margin_trade_position_iBTC(loanTokenWRBTC, SUSD, accounts[1]); - - await increaseTime(10 * 24 * 60 * 60); - const initial_loan = await sovryn.getLoan(loan_id); - - // needs to be called by the trader - expectRevert(sovryn.closeWithSwap(loan_id, trader, loan_token_sent, return_token_is_collateral, "0x"), "unauthorized"); - - // complete closure means the whole collateral is swapped - const swap_amount = initial_loan["collateral"]; - - await internal_test_close_margin_trade( - new BN(swap_amount), - initial_loan, - loanTokenWRBTC, - loan_id, - priceFeeds, - sovryn, - trader, - return_token_is_collateral - ); + // prepare the test + await set_demand_curve(loanToken); + await lend_to_pool_iBTC(loanTokenWRBTC, accounts[0]); + const [loan_id, trader, loan_token_sent] = await open_margin_trade_position_iBTC( + loanTokenWRBTC, + SUSD, + accounts[1] + ); + + await increaseTime(10 * 24 * 60 * 60); + const initial_loan = await sovryn.getLoan(loan_id); + + // needs to be called by the trader + expectRevert( + sovryn.closeWithSwap(loan_id, trader, loan_token_sent, return_token_is_collateral, "0x"), + "unauthorized" + ); + + // complete closure means the whole collateral is swapped + const swap_amount = initial_loan["collateral"]; + + await internal_test_close_margin_trade( + new BN(swap_amount), + initial_loan, + loanTokenWRBTC, + loan_id, + priceFeeds, + sovryn, + trader, + return_token_is_collateral + ); }; const close_complete_margin_trade_sov_reward_payment = async ( - sovryn, - set_demand_curve, - lend_to_pool, - open_margin_trade_position, - return_token_is_collateral, - FeesEvents, - loanToken, - RBTC, - WRBTC, - SUSD, - SOV, - accounts + sovryn, + set_demand_curve, + lend_to_pool, + open_margin_trade_position, + return_token_is_collateral, + FeesEvents, + loanToken, + RBTC, + WRBTC, + SUSD, + SOV, + accounts ) => { - // prepare the test - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, accounts[0]); - const [loan_id, trader, loan_token_sent] = await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, accounts[1]); - - await increaseTime(10 * 24 * 60 * 60); - const initial_loan = await sovryn.getLoan(loan_id); - - // needs to be called by the trader - expectRevert(sovryn.closeWithSwap(loan_id, trader, loan_token_sent, return_token_is_collateral, "0x"), "unauthorized"); - - // complete closure means the whole collateral is swapped - const swap_amount = initial_loan["collateral"]; - - lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); - const sov_initial_balance = (await SOV.balanceOf(trader)).add(await lockedSOV.getLockedBalance(trader)); - const { receipt } = await sovryn.closeWithSwap(loan_id, trader, swap_amount, return_token_is_collateral, "0x", { from: trader }); - await verify_sov_reward_payment( - receipt.rawLogs, - FeesEvents, - SOV, - trader, - loan_id, - sov_initial_balance, - 2, - SUSD.address, - RBTC.address, - sovryn - ); + // prepare the test + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, accounts[0]); + const [loan_id, trader, loan_token_sent] = await open_margin_trade_position( + loanToken, + RBTC, + WRBTC, + SUSD, + accounts[1] + ); + + await increaseTime(10 * 24 * 60 * 60); + const initial_loan = await sovryn.getLoan(loan_id); + + // needs to be called by the trader + expectRevert( + sovryn.closeWithSwap(loan_id, trader, loan_token_sent, return_token_is_collateral, "0x"), + "unauthorized" + ); + + // complete closure means the whole collateral is swapped + const swap_amount = initial_loan["collateral"]; + + lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); + const sov_initial_balance = (await SOV.balanceOf(trader)).add( + await lockedSOV.getLockedBalance(trader) + ); + const { receipt } = await sovryn.closeWithSwap( + loan_id, + trader, + swap_amount, + return_token_is_collateral, + "0x", + { from: trader } + ); + await verify_sov_reward_payment( + receipt.rawLogs, + FeesEvents, + SOV, + trader, + loan_id, + sov_initial_balance, + 2, + SUSD.address, + RBTC.address, + sovryn + ); }; const close_complete_margin_trade_sov_reward_payment_with_special_rebates = async ( - sovryn, - set_demand_curve, - lend_to_pool, - open_margin_trade_position, - return_token_is_collateral, - FeesEvents, - loanToken, - RBTC, - WRBTC, - SUSD, - SOV, - accounts + sovryn, + set_demand_curve, + lend_to_pool, + open_margin_trade_position, + return_token_is_collateral, + FeesEvents, + loanToken, + RBTC, + WRBTC, + SUSD, + SOV, + accounts ) => { - // prepare the test - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, accounts[0]); - await sovryn.setSpecialRebates(SUSD.address, RBTC.address, wei("30", "ether")); - await sovryn.setSpecialRebates(RBTC.address, SUSD.address, wei("30", "ether")); - const [loan_id, trader, loan_token_sent] = await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, accounts[1]); - - await increaseTime(10 * 24 * 60 * 60); - const initial_loan = await sovryn.getLoan(loan_id); - - // needs to be called by the trader - expectRevert(sovryn.closeWithSwap(loan_id, trader, loan_token_sent, return_token_is_collateral, "0x"), "unauthorized"); - - // complete closure means the whole collateral is swapped - const swap_amount = initial_loan["collateral"]; - - lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); - const sov_initial_balance = (await SOV.balanceOf(trader)).add(await lockedSOV.getLockedBalance(trader)); - const { receipt } = await sovryn.closeWithSwap(loan_id, trader, swap_amount, return_token_is_collateral, "0x", { from: trader }); - await verify_sov_reward_payment( - receipt.rawLogs, - FeesEvents, - SOV, - trader, - loan_id, - sov_initial_balance, - 2, - SUSD.address, - RBTC.address, - sovryn - ); + // prepare the test + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, accounts[0]); + await sovryn.setSpecialRebates(SUSD.address, RBTC.address, wei("30", "ether")); + await sovryn.setSpecialRebates(RBTC.address, SUSD.address, wei("30", "ether")); + const [loan_id, trader, loan_token_sent] = await open_margin_trade_position( + loanToken, + RBTC, + WRBTC, + SUSD, + accounts[1] + ); + + await increaseTime(10 * 24 * 60 * 60); + const initial_loan = await sovryn.getLoan(loan_id); + + // needs to be called by the trader + expectRevert( + sovryn.closeWithSwap(loan_id, trader, loan_token_sent, return_token_is_collateral, "0x"), + "unauthorized" + ); + + // complete closure means the whole collateral is swapped + const swap_amount = initial_loan["collateral"]; + + lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); + const sov_initial_balance = (await SOV.balanceOf(trader)).add( + await lockedSOV.getLockedBalance(trader) + ); + const { receipt } = await sovryn.closeWithSwap( + loan_id, + trader, + swap_amount, + return_token_is_collateral, + "0x", + { from: trader } + ); + await verify_sov_reward_payment( + receipt.rawLogs, + FeesEvents, + SOV, + trader, + loan_id, + sov_initial_balance, + 2, + SUSD.address, + RBTC.address, + sovryn + ); }; /* @@ -569,306 +681,400 @@ close a position partially 5. verifies the result */ const close_partial_margin_trade = async ( - sovryn, - loanToken, - set_demand_curve, - lend_to_pool, - open_margin_trade_position, - priceFeeds, - return_token_is_collateral, - RBTC, - WRBTC, - SUSD, - accounts + sovryn, + loanToken, + set_demand_curve, + lend_to_pool, + open_margin_trade_position, + priceFeeds, + return_token_is_collateral, + RBTC, + WRBTC, + SUSD, + accounts ) => { - // prepare the test - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, accounts[0]); - const [loan_id, trader, loan_token_sent] = await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, accounts[1]); - - await increaseTime(10 * 24 * 60 * 60); - const initial_loan = await sovryn.getLoan(loan_id); - - // needs to be called by the trader - expectRevert(sovryn.closeWithSwap(loan_id, trader, loan_token_sent, return_token_is_collateral, "0x"), "unauthorized"); - - // partial closure means 80% of the collateral is swapped - const swap_amount = new BN(initial_loan["collateral"]).mul(new BN(80).mul(oneEth)).div(hunEth); - - await internal_test_close_margin_trade( - new BN(swap_amount), - initial_loan, - loanToken, - loan_id, - priceFeeds, - sovryn, - trader, - return_token_is_collateral - ); + // prepare the test + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, accounts[0]); + const [loan_id, trader, loan_token_sent] = await open_margin_trade_position( + loanToken, + RBTC, + WRBTC, + SUSD, + accounts[1] + ); + + await increaseTime(10 * 24 * 60 * 60); + const initial_loan = await sovryn.getLoan(loan_id); + + // needs to be called by the trader + expectRevert( + sovryn.closeWithSwap(loan_id, trader, loan_token_sent, return_token_is_collateral, "0x"), + "unauthorized" + ); + + // partial closure means 80% of the collateral is swapped + const swap_amount = new BN(initial_loan["collateral"]).mul(new BN(80).mul(oneEth)).div(hunEth); + + await internal_test_close_margin_trade( + new BN(swap_amount), + initial_loan, + loanToken, + loan_id, + priceFeeds, + sovryn, + trader, + return_token_is_collateral + ); }; const close_partial_margin_trade_wrbtc = async ( - sovryn, - loanToken, - loanTokenWRBTC, - set_demand_curve, - lend_to_pool_iBTC, - open_margin_trade_position_iBTC, - priceFeeds, - return_token_is_collateral, - RBTC, - WRBTC, - SUSD, - accounts + sovryn, + loanToken, + loanTokenWRBTC, + set_demand_curve, + lend_to_pool_iBTC, + open_margin_trade_position_iBTC, + priceFeeds, + return_token_is_collateral, + RBTC, + WRBTC, + SUSD, + accounts ) => { - // prepare the test - await set_demand_curve(loanToken); - await lend_to_pool_iBTC(loanTokenWRBTC, accounts[0]); - const [loan_id, trader, loan_token_sent] = await open_margin_trade_position_iBTC(loanTokenWRBTC, SUSD, accounts[1]); - - await increaseTime(10 * 24 * 60 * 60); - const initial_loan = await sovryn.getLoan(loan_id); - - // needs to be called by the trader - expectRevert(sovryn.closeWithSwap(loan_id, trader, loan_token_sent, return_token_is_collateral, "0x"), "unauthorized"); - - // partial closure means 80% of the collateral is swapped - const swap_amount = new BN(initial_loan["collateral"]).mul(new BN(80).mul(oneEth)).div(hunEth); - - await internal_test_close_margin_trade( - new BN(swap_amount), - initial_loan, - loanTokenWRBTC, - loan_id, - priceFeeds, - sovryn, - trader, - return_token_is_collateral - ); + // prepare the test + await set_demand_curve(loanToken); + await lend_to_pool_iBTC(loanTokenWRBTC, accounts[0]); + const [loan_id, trader, loan_token_sent] = await open_margin_trade_position_iBTC( + loanTokenWRBTC, + SUSD, + accounts[1] + ); + + await increaseTime(10 * 24 * 60 * 60); + const initial_loan = await sovryn.getLoan(loan_id); + + // needs to be called by the trader + expectRevert( + sovryn.closeWithSwap(loan_id, trader, loan_token_sent, return_token_is_collateral, "0x"), + "unauthorized" + ); + + // partial closure means 80% of the collateral is swapped + const swap_amount = new BN(initial_loan["collateral"]).mul(new BN(80).mul(oneEth)).div(hunEth); + + await internal_test_close_margin_trade( + new BN(swap_amount), + initial_loan, + loanTokenWRBTC, + loan_id, + priceFeeds, + sovryn, + trader, + return_token_is_collateral + ); }; const close_partial_margin_trade_sov_reward_payment = async ( - sovryn, - set_demand_curve, - lend_to_pool, - open_margin_trade_position, - return_token_is_collateral, - FeesEvents, - loanToken, - RBTC, - WRBTC, - SUSD, - SOV, - accounts + sovryn, + set_demand_curve, + lend_to_pool, + open_margin_trade_position, + return_token_is_collateral, + FeesEvents, + loanToken, + RBTC, + WRBTC, + SUSD, + SOV, + accounts ) => { - // prepare the test - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, accounts[0]); - const [loan_id, trader] = await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, accounts[1]); - - await increaseTime(10 * 24 * 60 * 60); - const initial_loan = await sovryn.getLoan(loan_id); - - // partial closure means 80% of the collateral is swapped - const swap_amount = new BN(initial_loan["collateral"]).mul(new BN(80).mul(oneEth)).div(hunEth); - - lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); - const sov_initial_balance = (await SOV.balanceOf(trader)).add(await lockedSOV.getLockedBalance(trader)); - const { receipt } = await sovryn.closeWithSwap(loan_id, trader, swap_amount, return_token_is_collateral, "0x", { from: trader }); - await verify_sov_reward_payment( - receipt.rawLogs, - FeesEvents, - SOV, - trader, - loan_id, - sov_initial_balance, - 2, - SUSD.address, - RBTC.address, - sovryn - ); + // prepare the test + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, accounts[0]); + const [loan_id, trader] = await open_margin_trade_position( + loanToken, + RBTC, + WRBTC, + SUSD, + accounts[1] + ); + + await increaseTime(10 * 24 * 60 * 60); + const initial_loan = await sovryn.getLoan(loan_id); + + // partial closure means 80% of the collateral is swapped + const swap_amount = new BN(initial_loan["collateral"]).mul(new BN(80).mul(oneEth)).div(hunEth); + + lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); + const sov_initial_balance = (await SOV.balanceOf(trader)).add( + await lockedSOV.getLockedBalance(trader) + ); + const { receipt } = await sovryn.closeWithSwap( + loan_id, + trader, + swap_amount, + return_token_is_collateral, + "0x", + { from: trader } + ); + await verify_sov_reward_payment( + receipt.rawLogs, + FeesEvents, + SOV, + trader, + loan_id, + sov_initial_balance, + 2, + SUSD.address, + RBTC.address, + sovryn + ); }; const close_partial_margin_trade_sov_reward_payment_with_special_rebates = async ( - sovryn, - set_demand_curve, - lend_to_pool, - open_margin_trade_position, - return_token_is_collateral, - FeesEvents, - loanToken, - RBTC, - WRBTC, - SUSD, - SOV, - accounts + sovryn, + set_demand_curve, + lend_to_pool, + open_margin_trade_position, + return_token_is_collateral, + FeesEvents, + loanToken, + RBTC, + WRBTC, + SUSD, + SOV, + accounts ) => { - // prepare the test - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, accounts[0]); - await sovryn.setSpecialRebates(SUSD.address, RBTC.address, wei("30", "ether")); - await sovryn.setSpecialRebates(RBTC.address, SUSD.address, wei("30", "ether")); - const [loan_id, trader] = await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, accounts[1]); - - await increaseTime(10 * 24 * 60 * 60); - const initial_loan = await sovryn.getLoan(loan_id); - - // partial closure means 80% of the collateral is swapped - const swap_amount = new BN(initial_loan["collateral"]).mul(new BN(80).mul(oneEth)).div(hunEth); - - lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); - const sov_initial_balance = (await SOV.balanceOf(trader)).add(await lockedSOV.getLockedBalance(trader)); - const { receipt } = await sovryn.closeWithSwap(loan_id, trader, swap_amount, return_token_is_collateral, "0x", { from: trader }); - await verify_sov_reward_payment( - receipt.rawLogs, - FeesEvents, - SOV, - trader, - loan_id, - sov_initial_balance, - 2, - SUSD.address, - RBTC.address, - sovryn - ); + // prepare the test + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, accounts[0]); + await sovryn.setSpecialRebates(SUSD.address, RBTC.address, wei("30", "ether")); + await sovryn.setSpecialRebates(RBTC.address, SUSD.address, wei("30", "ether")); + const [loan_id, trader] = await open_margin_trade_position( + loanToken, + RBTC, + WRBTC, + SUSD, + accounts[1] + ); + + await increaseTime(10 * 24 * 60 * 60); + const initial_loan = await sovryn.getLoan(loan_id); + + // partial closure means 80% of the collateral is swapped + const swap_amount = new BN(initial_loan["collateral"]).mul(new BN(80).mul(oneEth)).div(hunEth); + + lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); + const sov_initial_balance = (await SOV.balanceOf(trader)).add( + await lockedSOV.getLockedBalance(trader) + ); + const { receipt } = await sovryn.closeWithSwap( + loan_id, + trader, + swap_amount, + return_token_is_collateral, + "0x", + { from: trader } + ); + await verify_sov_reward_payment( + receipt.rawLogs, + FeesEvents, + SOV, + trader, + loan_id, + sov_initial_balance, + 2, + SUSD.address, + RBTC.address, + sovryn + ); }; const internal_test_close_margin_trade = async ( - swap_amount, - initial_loan, - loanToken, - loan_id, - priceFeeds, - sovryn, - trader, - return_token_is_collateral + swap_amount, + initial_loan, + loanToken, + loan_id, + priceFeeds, + sovryn, + trader, + return_token_is_collateral ) => { - const principal_ = new BN(initial_loan["principal"]); - const collateral_ = new BN(initial_loan["collateral"]); - - const { receipt } = await sovryn.closeWithSwap(loan_id, trader, swap_amount, return_token_is_collateral, "0x", { from: trader }); - const closed_loan = await sovryn.getLoan(loan_id); - const loan_token_ = initial_loan["loanToken"]; - const collateral_token_ = initial_loan["collateralToken"]; - const { rate: trade_rate, precision } = await priceFeeds.queryRate(collateral_token_, loan_token_); - - swap_amount = swap_amount.gt(collateral_) ? collateral_ : swap_amount; - - let loan_close_amount = swap_amount.eq(collateral_) - ? principal_ - : return_token_is_collateral - ? principal_.mul(swap_amount).div(collateral_) - : new BN(0); - - const interest_refund_to_borrower = new BN(initial_loan["interestDepositRemaining"]).mul(loan_close_amount).div(principal_); - - const loan_close_amount_less_interest = - !loan_close_amount.eq(new BN(0)) && loan_close_amount.gte(interest_refund_to_borrower) - ? loan_close_amount.sub(interest_refund_to_borrower) - : interest_refund_to_borrower; - - const trading_fee_percent = await sovryn.tradingFeePercent(); - const aux_trading_fee = return_token_is_collateral ? loan_close_amount_less_interest : swap_amount; - const trading_fee = aux_trading_fee.mul(trading_fee_percent).div(hunEth); - - const source_token_amount_used = return_token_is_collateral - ? loan_close_amount_less_interest.add(trading_fee).mul(precision).div(trade_rate) - : swap_amount; - - const dest_token_amount_received = return_token_is_collateral - ? loan_close_amount_less_interest - : swap_amount.sub(trading_fee).mul(trade_rate).div(precision); - - let collateral_to_loan_swap_rate = dest_token_amount_received.mul(precision).div(source_token_amount_used); - // 1e36 produces a wrong number because of floating point - collateral_to_loan_swap_rate = new BN(10).pow(new BN(36)).div(collateral_to_loan_swap_rate); - - const source_token_amount_used_2 = dest_token_amount_received.gte(principal_) ? collateral_ : source_token_amount_used; - const used_collateral = source_token_amount_used_2.gt(swap_amount) ? source_token_amount_used_2 : swap_amount; - - const covered_principal = - swap_amount.eq(collateral_) || return_token_is_collateral - ? loan_close_amount_less_interest - : dest_token_amount_received.gte(principal_) - ? principal_ - : dest_token_amount_received; - - loan_close_amount = loan_close_amount.eq(new BN(0)) ? covered_principal : loan_close_amount; - - const new_collateral = !used_collateral.eq(new BN(0)) ? collateral_.sub(used_collateral) : collateral_; - const new_principal = loan_close_amount.eq(principal_) ? new BN(0) : principal_.sub(loan_close_amount); - - let current_margin = new_collateral.mul(trade_rate).mul(oneEth).div(precision).div(oneEth); - current_margin = - !new_principal.eq(new BN(0)) && current_margin.gte(new_principal) - ? current_margin.sub(new_principal).mul(hunEth).div(new_principal) - : new BN(0); - current_margin = !current_margin.eq(new BN(0)) ? new BN(10).pow(new BN(38)).div(current_margin) : new BN(0); - - const decode = decodeLogs(receipt.rawLogs, SwapsEvents, "LoanSwap"); - const args = decode[0].args; - - expect(args["loanId"] == loan_id).to.be.true; - expect(args["sourceToken"]).to.equal(collateral_token_.toString()); - expect(args["destToken"] == loan_token_).to.be.true; - expect(args["borrower"] == trader).to.be.true; - - // 10000 is the source buffer used by the sovryn swap connector - // expect(new BN(args["sourceAmount"]).sub(source_token_amount_used).lte(new BN(10000))).to.be.true; - expect(new BN(args["destAmount"]).gte(dest_token_amount_received.mul(new BN(995)).div(new BN(1000)))).to.be.true; - - const decode2 = decodeLogs(receipt.rawLogs, LoanClosingsEvents, "CloseWithSwap"); - const args2 = decode2[0].args; - - expect(args2["loanId"] == loan_id).to.be.true; - expect(args2["loanCloseAmount"]).to.eq(loan_close_amount.toString()); - expect(args2["currentLeverage"]).to.eq(current_margin.toString()); - expect(args2["closer"] == trader).to.be.true; - expect(args2["user"] == trader).to.be.true; - expect(args2["lender"] == loanToken.address).to.be.true; - expect(args2["collateralToken"] == collateral_token_).to.be.true; - expect(args2["loanToken"] == loan_token_).to.be.true; - expect(args2["positionCloseSize"]).to.eq(used_collateral.toString()); - expect(new BN(args2["exitPrice"]).sub(collateral_to_loan_swap_rate).mul(new BN(100)).div(collateral_to_loan_swap_rate).eq(new BN(0))).to - .be.true; - - expect(closed_loan["principal"] == new_principal.toString()).to.be.true; - if (loan_close_amount.eq(principal_)) { - const last_block_timestamp = (await web3.eth.getBlock(await web3.eth.getBlockNumber()))["timestamp"]; - expect(closed_loan["endTimestamp"] <= last_block_timestamp).to.be.true; - } + const principal_ = new BN(initial_loan["principal"]); + const collateral_ = new BN(initial_loan["collateral"]); + + const { receipt } = await sovryn.closeWithSwap( + loan_id, + trader, + swap_amount, + return_token_is_collateral, + "0x", + { from: trader } + ); + const closed_loan = await sovryn.getLoan(loan_id); + const loan_token_ = initial_loan["loanToken"]; + const collateral_token_ = initial_loan["collateralToken"]; + const { rate: trade_rate, precision } = await priceFeeds.queryRate( + collateral_token_, + loan_token_ + ); + + swap_amount = swap_amount.gt(collateral_) ? collateral_ : swap_amount; + + let loan_close_amount = swap_amount.eq(collateral_) + ? principal_ + : return_token_is_collateral + ? principal_.mul(swap_amount).div(collateral_) + : new BN(0); + + const interest_refund_to_borrower = new BN(initial_loan["interestDepositRemaining"]) + .mul(loan_close_amount) + .div(principal_); + + const loan_close_amount_less_interest = + !loan_close_amount.eq(new BN(0)) && loan_close_amount.gte(interest_refund_to_borrower) + ? loan_close_amount.sub(interest_refund_to_borrower) + : interest_refund_to_borrower; + + const trading_fee_percent = await sovryn.tradingFeePercent(); + const aux_trading_fee = return_token_is_collateral + ? loan_close_amount_less_interest + : swap_amount; + const trading_fee = aux_trading_fee.mul(trading_fee_percent).div(hunEth); + + const source_token_amount_used = return_token_is_collateral + ? loan_close_amount_less_interest.add(trading_fee).mul(precision).div(trade_rate) + : swap_amount; + + const dest_token_amount_received = return_token_is_collateral + ? loan_close_amount_less_interest + : swap_amount.sub(trading_fee).mul(trade_rate).div(precision); + + let collateral_to_loan_swap_rate = dest_token_amount_received + .mul(precision) + .div(source_token_amount_used); + // 1e36 produces a wrong number because of floating point + collateral_to_loan_swap_rate = new BN(10).pow(new BN(36)).div(collateral_to_loan_swap_rate); + + const source_token_amount_used_2 = dest_token_amount_received.gte(principal_) + ? collateral_ + : source_token_amount_used; + const used_collateral = source_token_amount_used_2.gt(swap_amount) + ? source_token_amount_used_2 + : swap_amount; + + const covered_principal = + swap_amount.eq(collateral_) || return_token_is_collateral + ? loan_close_amount_less_interest + : dest_token_amount_received.gte(principal_) + ? principal_ + : dest_token_amount_received; + + loan_close_amount = loan_close_amount.eq(new BN(0)) ? covered_principal : loan_close_amount; + + const new_collateral = !used_collateral.eq(new BN(0)) + ? collateral_.sub(used_collateral) + : collateral_; + const new_principal = loan_close_amount.eq(principal_) + ? new BN(0) + : principal_.sub(loan_close_amount); + + let current_margin = new_collateral.mul(trade_rate).mul(oneEth).div(precision).div(oneEth); + current_margin = + !new_principal.eq(new BN(0)) && current_margin.gte(new_principal) + ? current_margin.sub(new_principal).mul(hunEth).div(new_principal) + : new BN(0); + current_margin = !current_margin.eq(new BN(0)) + ? new BN(10).pow(new BN(38)).div(current_margin) + : new BN(0); + + const decode = decodeLogs(receipt.rawLogs, SwapsEvents, "LoanSwap"); + const args = decode[0].args; + + expect(args["loanId"] == loan_id).to.be.true; + expect(args["sourceToken"]).to.equal(collateral_token_.toString()); + expect(args["destToken"] == loan_token_).to.be.true; + expect(args["borrower"] == trader).to.be.true; + + // 10000 is the source buffer used by the sovryn swap connector + // expect(new BN(args["sourceAmount"]).sub(source_token_amount_used).lte(new BN(10000))).to.be.true; + expect( + new BN(args["destAmount"]).gte( + dest_token_amount_received.mul(new BN(995)).div(new BN(1000)) + ) + ).to.be.true; + + const decode2 = decodeLogs(receipt.rawLogs, LoanClosingsEvents, "CloseWithSwap"); + const args2 = decode2[0].args; + + expect(args2["loanId"] == loan_id).to.be.true; + expect(args2["loanCloseAmount"]).to.eq(loan_close_amount.toString()); + expect(args2["currentLeverage"]).to.eq(current_margin.toString()); + expect(args2["closer"] == trader).to.be.true; + expect(args2["user"] == trader).to.be.true; + expect(args2["lender"] == loanToken.address).to.be.true; + expect(args2["collateralToken"] == collateral_token_).to.be.true; + expect(args2["loanToken"] == loan_token_).to.be.true; + expect(args2["positionCloseSize"]).to.eq(used_collateral.toString()); + expect( + new BN(args2["exitPrice"]) + .sub(collateral_to_loan_swap_rate) + .mul(new BN(100)) + .div(collateral_to_loan_swap_rate) + .eq(new BN(0)) + ).to.be.true; + + expect(closed_loan["principal"] == new_principal.toString()).to.be.true; + if (loan_close_amount.eq(principal_)) { + const last_block_timestamp = (await web3.eth.getBlock(await web3.eth.getBlockNumber()))[ + "timestamp" + ]; + expect(closed_loan["endTimestamp"] <= last_block_timestamp).to.be.true; + } }; -const get_estimated_margin_details = async (loanToken, collateralToken, loanSize, collateralTokenSent, leverageAmount) => { - // leverageAmount, loanTokenSent, collateralTokenSent, collateralTokenAddress - const result = await loanToken.getEstimatedMarginDetails(leverageAmount, 0, collateralTokenSent, collateralToken.address); - //"2003004506760140211"; collateralTokenSent - //"5000000000000000000"; leverageAmount - //"20000000000000000000000"; loanSize - //"100150225338007010550000"; result[0] - - expect(result[0]).to.be.a.bignumber.eq( - loanSize - .mul(collateralTokenSent) - .mul(leverageAmount) - .div(new BN(10).pow(new BN(36))) - ); - expect(result[2].eq(new BN(0))).to.be.true; +const get_estimated_margin_details = async ( + loanToken, + collateralToken, + loanSize, + collateralTokenSent, + leverageAmount +) => { + // leverageAmount, loanTokenSent, collateralTokenSent, collateralTokenAddress + const result = await loanToken.getEstimatedMarginDetails( + leverageAmount, + 0, + collateralTokenSent, + collateralToken.address + ); + //"2003004506760140211"; collateralTokenSent + //"5000000000000000000"; leverageAmount + //"20000000000000000000000"; loanSize + //"100150225338007010550000"; result[0] + + expect(result[0]).to.be.a.bignumber.eq( + loanSize + .mul(collateralTokenSent) + .mul(leverageAmount) + .div(new BN(10).pow(new BN(36))) + ); + expect(result[2].eq(new BN(0))).to.be.true; }; module.exports = { - margin_trading_sending_loan_tokens, - margin_trading_sov_reward_payment, - margin_trading_sov_reward_payment_with_special_rebates, - margin_trading_sending_collateral_tokens, - margin_trading_sending_collateral_tokens_sov_reward_payment, - margin_trading_sending_collateral_tokens_sov_reward_payment_with_special_rebates, - close_complete_margin_trade, - close_complete_margin_trade_sov_reward_payment, - close_complete_margin_trade_sov_reward_payment_with_special_rebates, - close_partial_margin_trade, - close_partial_margin_trade_wrbtc, - close_partial_margin_trade_sov_reward_payment, - close_partial_margin_trade_sov_reward_payment_with_special_rebates, - close_complete_margin_trade_wrbtc, + margin_trading_sending_loan_tokens, + margin_trading_sov_reward_payment, + margin_trading_sov_reward_payment_with_special_rebates, + margin_trading_sending_collateral_tokens, + margin_trading_sending_collateral_tokens_sov_reward_payment, + margin_trading_sending_collateral_tokens_sov_reward_payment_with_special_rebates, + close_complete_margin_trade, + close_complete_margin_trade_sov_reward_payment, + close_complete_margin_trade_sov_reward_payment_with_special_rebates, + close_partial_margin_trade, + close_partial_margin_trade_wrbtc, + close_partial_margin_trade_sov_reward_payment, + close_partial_margin_trade_sov_reward_payment_with_special_rebates, + close_complete_margin_trade_wrbtc, }; diff --git a/tests/margin_trading_boilerplate.test.js b/tests/margin_trading_boilerplate.test.js index 1b730762b..6b4611e85 100644 --- a/tests/margin_trading_boilerplate.test.js +++ b/tests/margin_trading_boilerplate.test.js @@ -25,73 +25,89 @@ const LoanToken = artifacts.require("LoanToken"); const TestToken = artifacts.require("TestToken"); const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getLoanTokenLogic, - getLoanToken, - getLoanTokenLogicWrbtc, - getLoanTokenWRBTC, - loan_pool_setup, - set_demand_curve, - getPriceFeeds, - getSovryn, - decodeLogs, - getSOV, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getLoanTokenLogic, + getLoanToken, + getLoanTokenLogicWrbtc, + getLoanTokenWRBTC, + loan_pool_setup, + set_demand_curve, + getPriceFeeds, + getSovryn, + decodeLogs, + getSOV, } = require("./Utils/initializer.js"); -const { BN, constants, balance, expectEvent, expectRevert } = require("@openzeppelin/test-helpers"); +const { + BN, + constants, + balance, + expectEvent, + expectRevert, +} = require("@openzeppelin/test-helpers"); contract("Margin Trading with Affiliates boilerplate", (accounts) => { - let loanTokenLogic; - let WRBTC; - let doc; - let sovryn; - let loanTokenV2; - let wei = web3.utils.toWei; - - async function deploymentAndInitFixture(_wallets, _provider) { - // Deploying sovrynProtocol w/ generic function from initializer.js - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - await sovryn.setSovrynProtocolAddress(sovryn.address); - - // Custom tokens - SOV = await getSOV(sovryn, priceFeeds, SUSD, accounts); - doc = await TestToken.new("dollar on chain", "DOC", 18, web3.utils.toWei("20000", "ether")); - - // Loan Pool - const initLoanTokenLogic = await getLoanTokenLogic(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] - loanTokenLogic = initLoanTokenLogic[0]; - loanTokenLogicBeacon = initLoanTokenLogic[1]; - - loanToken = await LoanToken.new(owner, loanTokenLogic.address, sovryn.address, WRBTC.address); - await loanToken.initialize(doc.address, "SUSD", "SUSD"); - - /** Initialize the loan token logic proxy */ - loanTokenV2 = await ILoanTokenLogicProxy.at(loanToken.address); - await loanTokenV2.setBeaconAddress(loanTokenLogicBeacon.address); - - /** Use interface of LoanTokenModules */ - loanTokenV2 = await ILoanTokenModules.at(loanTokenV2.address); - - const loanTokenAddress = await loanToken.loanTokenAddress(); - if (owner == (await sovryn.owner())) { - await sovryn.setLoanPool([loanTokenV2.address], [loanTokenAddress]); - } - - // initializing - await priceFeeds.setRates(doc.address, WRBTC.address, wei("0.01", "ether")); - await sovryn.setSupportedTokens([doc.address, WRBTC.address], [true, true]); - await sovryn.setFeesController(owner); - - { - /** + let loanTokenLogic; + let WRBTC; + let doc; + let sovryn; + let loanTokenV2; + let wei = web3.utils.toWei; + + async function deploymentAndInitFixture(_wallets, _provider) { + // Deploying sovrynProtocol w/ generic function from initializer.js + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + await sovryn.setSovrynProtocolAddress(sovryn.address); + + // Custom tokens + SOV = await getSOV(sovryn, priceFeeds, SUSD, accounts); + doc = await TestToken.new( + "dollar on chain", + "DOC", + 18, + web3.utils.toWei("20000", "ether") + ); + + // Loan Pool + const initLoanTokenLogic = await getLoanTokenLogic(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] + loanTokenLogic = initLoanTokenLogic[0]; + loanTokenLogicBeacon = initLoanTokenLogic[1]; + + loanToken = await LoanToken.new( + owner, + loanTokenLogic.address, + sovryn.address, + WRBTC.address + ); + await loanToken.initialize(doc.address, "SUSD", "SUSD"); + + /** Initialize the loan token logic proxy */ + loanTokenV2 = await ILoanTokenLogicProxy.at(loanToken.address); + await loanTokenV2.setBeaconAddress(loanTokenLogicBeacon.address); + + /** Use interface of LoanTokenModules */ + loanTokenV2 = await ILoanTokenModules.at(loanTokenV2.address); + + const loanTokenAddress = await loanToken.loanTokenAddress(); + if (owner == (await sovryn.owner())) { + await sovryn.setLoanPool([loanTokenV2.address], [loanTokenAddress]); + } + + // initializing + await priceFeeds.setRates(doc.address, WRBTC.address, wei("0.01", "ether")); + await sovryn.setSupportedTokens([doc.address, WRBTC.address], [true, true]); + await sovryn.setFeesController(owner); + + { + /** struct LoanParams { bytes32 id; // id of loan params object bool active; // if false, this object has been disabled by the owner and can't be used for future loans @@ -103,64 +119,75 @@ contract("Margin Trading with Affiliates boilerplate", (accounts) => { uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) } */ - } - params = [ - "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object - false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans - owner, // address owner; // owner of this object - doc.address, // address loanToken; // the token being loaned - WRBTC.address, // address collateralToken; // the required collateral token - wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin - wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value - 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) - ]; - - await loanTokenV2.setupLoanParams([params], true); - await loanTokenV2.setupLoanParams([params], false); - - // setting up interest rates - const baseRate = wei("1", "ether"); - const rateMultiplier = wei("20.25", "ether"); - const targetLevel = wei("80", "ether"); - const kinkLevel = wei("90", "ether"); - const maxScaleRate = wei("100", "ether"); - await loanTokenV2.setDemandCurve(baseRate, rateMultiplier, baseRate, rateMultiplier, targetLevel, kinkLevel, maxScaleRate); - - // GIVING SOME DOC tokens to loanToken so that we can borrow from loanToken - await doc.transfer(loanTokenV2.address, wei("500", "ether")); - await doc.approve(loanToken.address, web3.utils.toWei("20", "ether")); - } - - before(async () => { - [owner, trader, referrer, account1, account2, ...accounts] = accounts; - }); - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - it("Margin trading with 3X leverage with DOC token and topUp position by 12rwBTC", async () => { - // Setting up interest rates - // Giving some WRBTC to sovrynAddress (by minting some WRBTC), so that it can open position in wRBTC. - await WRBTC.mint(sovryn.address, wei("500", "ether")); - - assert.equal(await sovryn.protocolAddress(), sovryn.address); - - const leverageAmount = web3.utils.toWei("3", "ether"); - const loanTokenSent = web3.utils.toWei("20", "ether"); - - await loanTokenV2.marginTrade( - constants.ZERO_BYTES32, // loanId (0 for new loans) - leverageAmount, // leverageAmount - loanTokenSent, // loanTokenSent - 0, // no collateral token sent - WRBTC.address, // collateralTokenAddress - owner, // trader - //referrer, // affiliates referrer - 0, - "0x", // loanDataBytes (only required with ether) - { from: owner } - ); - expect(await sovryn.getUserNotFirstTradeFlag(owner), "sovryn.getUserNotFirstTradeFlag(trader) should be true").to.be.true; - }); + } + params = [ + "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object + false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans + owner, // address owner; // owner of this object + doc.address, // address loanToken; // the token being loaned + WRBTC.address, // address collateralToken; // the required collateral token + wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin + wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value + 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) + ]; + + await loanTokenV2.setupLoanParams([params], true); + await loanTokenV2.setupLoanParams([params], false); + + // setting up interest rates + const baseRate = wei("1", "ether"); + const rateMultiplier = wei("20.25", "ether"); + const targetLevel = wei("80", "ether"); + const kinkLevel = wei("90", "ether"); + const maxScaleRate = wei("100", "ether"); + await loanTokenV2.setDemandCurve( + baseRate, + rateMultiplier, + baseRate, + rateMultiplier, + targetLevel, + kinkLevel, + maxScaleRate + ); + + // GIVING SOME DOC tokens to loanToken so that we can borrow from loanToken + await doc.transfer(loanTokenV2.address, wei("500", "ether")); + await doc.approve(loanToken.address, web3.utils.toWei("20", "ether")); + } + + before(async () => { + [owner, trader, referrer, account1, account2, ...accounts] = accounts; + }); + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + it("Margin trading with 3X leverage with DOC token and topUp position by 12rwBTC", async () => { + // Setting up interest rates + // Giving some WRBTC to sovrynAddress (by minting some WRBTC), so that it can open position in wRBTC. + await WRBTC.mint(sovryn.address, wei("500", "ether")); + + assert.equal(await sovryn.protocolAddress(), sovryn.address); + + const leverageAmount = web3.utils.toWei("3", "ether"); + const loanTokenSent = web3.utils.toWei("20", "ether"); + + await loanTokenV2.marginTrade( + constants.ZERO_BYTES32, // loanId (0 for new loans) + leverageAmount, // leverageAmount + loanTokenSent, // loanTokenSent + 0, // no collateral token sent + WRBTC.address, // collateralTokenAddress + owner, // trader + //referrer, // affiliates referrer + 0, + "0x", // loanDataBytes (only required with ether) + { from: owner } + ); + expect( + await sovryn.getUserNotFirstTradeFlag(owner), + "sovryn.getUserNotFirstTradeFlag(trader) should be true" + ).to.be.true; + }); }); diff --git a/tests/multisig/MultiSigKeyHolders.js b/tests/multisig/MultiSigKeyHolders.js index c77f110e7..172ab5e05 100644 --- a/tests/multisig/MultiSigKeyHolders.js +++ b/tests/multisig/MultiSigKeyHolders.js @@ -18,476 +18,551 @@ const EMPTY_ADDRESS = ""; const MultiSigKeyHolders = artifacts.require("MultiSigKeyHolders"); contract("MultiSigKeyHolders:", (accounts) => { - let root, account1, account2, account3, account4; - let multiSig; - let bitcoinAccount1 = "bc1q9gl8ddnkr0xr5d9vefnkwyd3g8fpjsp8z8l7zm"; - let bitcoinAccount2 = "37S6qsjzw14MH9SFt7PmsBchobkRE6SxNP"; - let bitcoinAccount3 = "37S6qsjzw14MH9SFt7PmsBchobkRE6SxN3"; - - async function deploymentAndInitFixture(_wallets, _provider) { - multiSig = await MultiSigKeyHolders.new(); - } - - before(async () => { - [root, account1, account2, account3, account4, ...accounts] = accounts; - }); - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - describe("initialization", () => { - it("Should set default values", async () => { - expect(await multiSig.ethereumRequired.call()).to.be.bignumber.equal(new BN(2)); - expect(await multiSig.bitcoinRequired.call()).to.be.bignumber.equal(new BN(2)); - }); - }); - - describe("addEthereumAddress", () => { - it("Shouldn't be able to add zero address", async () => { - await expectRevert(multiSig.addEthereumAddress(ZERO_ADDRESS), "Invalid address"); - }); - - it("Only owner should be able to add address", async () => { - await expectRevert(multiSig.addEthereumAddress(account1, { from: account1 }), "unauthorized"); - }); - - it("Should be able to add address", async () => { - let tx = await multiSig.addEthereumAddress(account1); - - let isOwner = await multiSig.isEthereumAddressOwner.call(account1); - expect(isOwner).to.be.true; - - let list = await multiSig.getEthereumAddresses.call(); - expect(list.length).to.be.equal(1); - expect(list[0]).to.be.equal(account1); - - expectEvent(tx, "EthereumAddressAdded", { - account: account1, - }); - }); - - /// @dev For test coverage - it("Should be able to add redundant addresses, but ignored", async () => { - let tx = await multiSig.addEthereumAddresses([account1, account1, account2]); - - let isOwner = await multiSig.isEthereumAddressOwner.call(account1); - expect(isOwner).to.be.true; - - let list = await multiSig.getEthereumAddresses.call(); - expect(list.length).to.be.equal(2); - expect(list[0]).to.be.equal(account1); - expect(list[1]).to.be.equal(account2); - - expectEvent(tx, "EthereumAddressAdded", { - account: account1, - }); - }); - }); - - describe("addEthereumAddresses", () => { - it("Shouldn't be able to add zero addresses", async () => { - await expectRevert(multiSig.addEthereumAddresses([ZERO_ADDRESS]), "Invalid address"); - }); - - it("Only owner should be able to add addresses", async () => { - await expectRevert(multiSig.addEthereumAddresses([account1, account2], { from: account1 }), "unauthorized"); - }); - - it("Should be able to add addresses", async () => { - let tx = await multiSig.addEthereumAddresses([account1, account2]); - - let isOwner = await multiSig.isEthereumAddressOwner.call(account1); - expect(isOwner).to.be.true; - isOwner = await multiSig.isEthereumAddressOwner.call(account2); - expect(isOwner).to.be.true; - - let list = await multiSig.getEthereumAddresses.call(); - expect(list.length).to.be.equal(2); - expect(list[0]).to.be.equal(account1); - expect(list[1]).to.be.equal(account2); - - expectEvent(tx, "EthereumAddressAdded", { - account: account1, - }); - expectEvent(tx, "EthereumAddressAdded", { - account: account2, - }); - }); - }); - - describe("changeEthereumRequirement", () => { - it("Only owner should be able to change ethereum requirement", async () => { - await expectRevert(multiSig.changeEthereumRequirement(1, { from: account1 }), "unauthorized"); - }); - - it("Only owner should be able to change ethereum requirement", async () => { - await expectRevert(multiSig.changeEthereumRequirement(5), "Invalid required"); - }); - - it("Should be able to change ethereum requirement", async () => { - let required = 3; - await multiSig.addEthereumAddresses([account1, account2, account3]); - - let tx = await multiSig.changeEthereumRequirement(required); - - expect(await multiSig.ethereumRequired.call()).to.be.bignumber.equal(new BN(required)); - - expectEvent(tx, "EthereumRequirementChanged", { - required: new BN(required), - }); - }); - }); - - describe("addBitcoinAddress", () => { - it("Shouldn't be able to add zero address", async () => { - await expectRevert(multiSig.addBitcoinAddress(EMPTY_ADDRESS), "Invalid address"); - }); - - it("Only owner should be able to add address", async () => { - await expectRevert(multiSig.addBitcoinAddress(bitcoinAccount1, { from: account1 }), "unauthorized"); - }); - - it("Should be able to add address", async () => { - let tx = await multiSig.addBitcoinAddress(bitcoinAccount1); - - let isOwner = await multiSig.isBitcoinAddressOwner.call(bitcoinAccount1); - expect(isOwner).to.be.true; - - let list = await multiSig.getBitcoinAddresses.call(); - expect(list.length).to.be.equal(1); - expect(list[0]).to.be.equal(bitcoinAccount1); - - expectEvent(tx, "BitcoinAddressAdded", { - account: bitcoinAccount1, - }); - }); - }); - - describe("addBitcoinAddresses", () => { - it("Shouldn't be able to add zero addresses", async () => { - await expectRevert(multiSig.addBitcoinAddresses([EMPTY_ADDRESS]), "Invalid address"); - }); - - it("Only owner should be able to add addresses", async () => { - await expectRevert(multiSig.addBitcoinAddresses([bitcoinAccount1, bitcoinAccount2], { from: account1 }), "unauthorized"); - }); - - it("Should be able to add addresses", async () => { - let tx = await multiSig.addBitcoinAddresses([bitcoinAccount1, bitcoinAccount2]); - - let isOwner = await multiSig.isBitcoinAddressOwner.call(bitcoinAccount1); - expect(isOwner).to.be.true; - isOwner = await multiSig.isBitcoinAddressOwner.call(bitcoinAccount2); - expect(isOwner).to.be.true; - - let list = await multiSig.getBitcoinAddresses.call(); - expect(list.length).to.be.equal(2); - expect(list[0]).to.be.equal(bitcoinAccount1); - expect(list[1]).to.be.equal(bitcoinAccount2); - - expectEvent(tx, "BitcoinAddressAdded", { - account: bitcoinAccount1, - }); - expectEvent(tx, "BitcoinAddressAdded", { - account: bitcoinAccount2, - }); - }); - - /// @dev For test coverage - it("Should be able to add redundant addresses, but ignored", async () => { - let tx = await multiSig.addBitcoinAddresses([bitcoinAccount1, bitcoinAccount1, bitcoinAccount2]); - - let isOwner = await multiSig.isBitcoinAddressOwner.call(bitcoinAccount1); - expect(isOwner).to.be.true; - isOwner = await multiSig.isBitcoinAddressOwner.call(bitcoinAccount2); - expect(isOwner).to.be.true; - - let list = await multiSig.getBitcoinAddresses.call(); - expect(list.length).to.be.equal(2); - expect(list[0]).to.be.equal(bitcoinAccount1); - expect(list[1]).to.be.equal(bitcoinAccount2); - - expectEvent(tx, "BitcoinAddressAdded", { - account: bitcoinAccount1, - }); - expectEvent(tx, "BitcoinAddressAdded", { - account: bitcoinAccount2, - }); - }); - }); - - describe("addEthereumAndBitcoinAddresses", () => { - it("Shouldn't be able to add zero address", async () => { - await expectRevert(multiSig.addEthereumAndBitcoinAddresses([ZERO_ADDRESS], [bitcoinAccount1]), "Invalid address"); - }); - - it("Only owner should be able to add address", async () => { - await expectRevert( - multiSig.addEthereumAndBitcoinAddresses([account1, account2], [bitcoinAccount1, bitcoinAccount2], { from: account1 }), - "unauthorized" - ); - }); - - it("Should be able to add addresses", async () => { - let tx = await multiSig.addEthereumAndBitcoinAddresses([account1, account2], [bitcoinAccount1, bitcoinAccount2]); - - let isOwner = await multiSig.isEthereumAddressOwner.call(account1); - expect(isOwner).to.be.true; - isOwner = await multiSig.isEthereumAddressOwner.call(account2); - expect(isOwner).to.be.true; - - let list = await multiSig.getEthereumAddresses.call(); - expect(list.length).to.be.equal(2); - expect(list[0]).to.be.equal(account1); - expect(list[1]).to.be.equal(account2); - - expectEvent(tx, "EthereumAddressAdded", { - account: account1, - }); - expectEvent(tx, "EthereumAddressAdded", { - account: account2, - }); - - isOwner = await multiSig.isBitcoinAddressOwner.call(bitcoinAccount1); - expect(isOwner).to.be.true; - isOwner = await multiSig.isBitcoinAddressOwner.call(bitcoinAccount2); - expect(isOwner).to.be.true; - - list = await multiSig.getBitcoinAddresses.call(); - expect(list.length).to.be.equal(2); - expect(list[0]).to.be.equal(bitcoinAccount1); - expect(list[1]).to.be.equal(bitcoinAccount2); - - expectEvent(tx, "BitcoinAddressAdded", { - account: bitcoinAccount1, - }); - expectEvent(tx, "BitcoinAddressAdded", { - account: bitcoinAccount2, - }); - }); - }); - - describe("removeEthereumAddress", () => { - it("Shouldn't be able to remove zero address", async () => { - await expectRevert(multiSig.removeEthereumAddress(ZERO_ADDRESS), "Invalid address"); - }); - - it("Only owner should be able to remove address", async () => { - await expectRevert(multiSig.removeEthereumAddress(account1, { from: account1 }), "unauthorized"); - }); - - it("Should be able to remove address", async () => { - await multiSig.addEthereumAddress(account1); - let tx = await multiSig.removeEthereumAddress(account1); - - let isOwner = await multiSig.isEthereumAddressOwner.call(account1); - expect(isOwner).to.be.false; - - let list = await multiSig.getEthereumAddresses.call(); - expect(list.length).to.be.equal(0); - - expectEvent(tx, "EthereumAddressRemoved", { - account: account1, - }); - }); - }); - - describe("removeEthereumAddresses", () => { - it("Shouldn't be able to remove zero addresses", async () => { - await expectRevert(multiSig.removeEthereumAddresses([ZERO_ADDRESS]), "Invalid address"); - }); - - it("Only owner should be remove to add addresses", async () => { - await expectRevert(multiSig.removeEthereumAddresses([account1, account2], { from: account1 }), "unauthorized"); - }); - - it("Should be able to remove addresses", async () => { - await multiSig.addEthereumAddresses([account1, account2]); - let tx = await multiSig.removeEthereumAddresses([account1, account2]); - - let isOwner = await multiSig.isEthereumAddressOwner.call(account1); - expect(isOwner).to.be.false; - isOwner = await multiSig.isEthereumAddressOwner.call(account2); - expect(isOwner).to.be.false; - - let list = await multiSig.getEthereumAddresses.call(); - expect(list.length).to.be.equal(0); - - expectEvent(tx, "EthereumAddressRemoved", { - account: account1, - }); - expectEvent(tx, "EthereumAddressRemoved", { - account: account2, - }); - }); - - /// @dev For test coverage - it("Should be able to remove redundant addresses, just ignoring repeated ones", async () => { - await multiSig.addEthereumAddresses([account1, account2]); - let tx = await multiSig.removeEthereumAddresses([account1, account1, account2]); - - let isOwner = await multiSig.isEthereumAddressOwner.call(account1); - expect(isOwner).to.be.false; - isOwner = await multiSig.isEthereumAddressOwner.call(account2); - expect(isOwner).to.be.false; - - let list = await multiSig.getEthereumAddresses.call(); - expect(list.length).to.be.equal(0); - - expectEvent(tx, "EthereumAddressRemoved", { - account: account1, - }); - expectEvent(tx, "EthereumAddressRemoved", { - account: account2, - }); - }); - }); - - describe("removeBitcoinAddress", () => { - it("Shouldn't be able to remove zero address", async () => { - await expectRevert(multiSig.removeBitcoinAddress(EMPTY_ADDRESS), "Invalid address"); - }); - - it("Only owner should be remove to add address", async () => { - await expectRevert(multiSig.removeBitcoinAddress(bitcoinAccount1, { from: account1 }), "unauthorized"); - }); - - it("Should be able to remove address", async () => { - await multiSig.addBitcoinAddress(bitcoinAccount1); - let tx = await multiSig.removeBitcoinAddress(bitcoinAccount1); - - let isOwner = await multiSig.isBitcoinAddressOwner.call(bitcoinAccount1); - expect(isOwner).to.be.false; - - let list = await multiSig.getBitcoinAddresses.call(); - expect(list.length).to.be.equal(0); - - expectEvent(tx, "BitcoinAddressRemoved", { - account: bitcoinAccount1, - }); - }); - }); - - describe("removeBitcoinAddresses", () => { - it("Shouldn't be able to remove zero addresses", async () => { - await expectRevert(multiSig.removeBitcoinAddresses([EMPTY_ADDRESS]), "Invalid address"); - }); - - it("Only owner should be able to remove addresses", async () => { - await expectRevert(multiSig.removeBitcoinAddresses([bitcoinAccount1, bitcoinAccount2], { from: account1 }), "unauthorized"); - }); - - it("Should be able to remove addresses", async () => { - await multiSig.addBitcoinAddresses([bitcoinAccount1, bitcoinAccount2]); - let tx = await multiSig.removeBitcoinAddresses([bitcoinAccount1, bitcoinAccount2]); - - let isOwner = await multiSig.isBitcoinAddressOwner.call(bitcoinAccount1); - expect(isOwner).to.be.false; - isOwner = await multiSig.isBitcoinAddressOwner.call(bitcoinAccount2); - expect(isOwner).to.be.false; - - let list = await multiSig.getBitcoinAddresses.call(); - expect(list.length).to.be.equal(0); - - expectEvent(tx, "BitcoinAddressRemoved", { - account: bitcoinAccount1, - }); - expectEvent(tx, "BitcoinAddressRemoved", { - account: bitcoinAccount2, - }); - }); - - /// @dev For test coverage - it("Should be able to remove redundant addresses, just ignoring repeated ones", async () => { - await multiSig.addBitcoinAddresses([bitcoinAccount1, bitcoinAccount2]); - let tx = await multiSig.removeBitcoinAddresses([bitcoinAccount1, bitcoinAccount1, bitcoinAccount2]); - - let isOwner = await multiSig.isBitcoinAddressOwner.call(bitcoinAccount1); - expect(isOwner).to.be.false; - isOwner = await multiSig.isBitcoinAddressOwner.call(bitcoinAccount2); - expect(isOwner).to.be.false; - - let list = await multiSig.getBitcoinAddresses.call(); - expect(list.length).to.be.equal(0); - - expectEvent(tx, "BitcoinAddressRemoved", { - account: bitcoinAccount1, - }); - expectEvent(tx, "BitcoinAddressRemoved", { - account: bitcoinAccount2, - }); - }); - }); - - describe("removeEthereumAndBitcoinAddresses", () => { - it("Shouldn't be able to remove zero address", async () => { - await expectRevert(multiSig.removeEthereumAndBitcoinAddresses([ZERO_ADDRESS], [bitcoinAccount1]), "Invalid address"); - }); - - it("Only owner should be able to remove address", async () => { - await expectRevert( - multiSig.removeEthereumAndBitcoinAddresses([account1, account2], [bitcoinAccount1, bitcoinAccount2], { from: account1 }), - "unauthorized" - ); - }); - - it("Should be able to remove addresses", async () => { - await multiSig.addEthereumAndBitcoinAddresses([account1, account2], [bitcoinAccount1, bitcoinAccount2]); - let tx = await multiSig.removeEthereumAndBitcoinAddresses([account1, account2], [bitcoinAccount1, bitcoinAccount2]); - - let isOwner = await multiSig.isEthereumAddressOwner.call(account1); - expect(isOwner).to.be.false; - isOwner = await multiSig.isEthereumAddressOwner.call(account2); - expect(isOwner).to.be.false; - - let list = await multiSig.getEthereumAddresses.call(); - expect(list.length).to.be.equal(0); - - expectEvent(tx, "EthereumAddressRemoved", { - account: account1, - }); - expectEvent(tx, "EthereumAddressRemoved", { - account: account2, - }); - - isOwner = await multiSig.isBitcoinAddressOwner.call(bitcoinAccount1); - expect(isOwner).to.be.false; - isOwner = await multiSig.isBitcoinAddressOwner.call(bitcoinAccount2); - expect(isOwner).to.be.false; - - list = await multiSig.getBitcoinAddresses.call(); - expect(list.length).to.be.equal(0); - - expectEvent(tx, "BitcoinAddressRemoved", { - account: bitcoinAccount1, - }); - expectEvent(tx, "BitcoinAddressRemoved", { - account: bitcoinAccount2, - }); - }); - }); - - describe("changeBitcoinRequirement", () => { - it("Only owner should be able to change ethereum requirement", async () => { - await expectRevert(multiSig.changeBitcoinRequirement(1, { from: account1 }), "unauthorized"); - }); - - it("Only owner should be able to change ethereum requirement", async () => { - await expectRevert(multiSig.changeBitcoinRequirement(5), "Invalid required"); - }); - - it("Should be able to change ethereum requirement", async () => { - let required = 3; - await multiSig.addBitcoinAddresses([bitcoinAccount1, bitcoinAccount2, bitcoinAccount3]); - - let tx = await multiSig.changeBitcoinRequirement(required); - - expect(await multiSig.bitcoinRequired.call()).to.be.bignumber.equal(new BN(required)); - - expectEvent(tx, "BitcoinRequirementChanged", { - required: new BN(required), - }); - }); - }); + let root, account1, account2, account3, account4; + let multiSig; + let bitcoinAccount1 = "bc1q9gl8ddnkr0xr5d9vefnkwyd3g8fpjsp8z8l7zm"; + let bitcoinAccount2 = "37S6qsjzw14MH9SFt7PmsBchobkRE6SxNP"; + let bitcoinAccount3 = "37S6qsjzw14MH9SFt7PmsBchobkRE6SxN3"; + + async function deploymentAndInitFixture(_wallets, _provider) { + multiSig = await MultiSigKeyHolders.new(); + } + + before(async () => { + [root, account1, account2, account3, account4, ...accounts] = accounts; + }); + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + describe("initialization", () => { + it("Should set default values", async () => { + expect(await multiSig.ethereumRequired.call()).to.be.bignumber.equal(new BN(2)); + expect(await multiSig.bitcoinRequired.call()).to.be.bignumber.equal(new BN(2)); + }); + }); + + describe("addEthereumAddress", () => { + it("Shouldn't be able to add zero address", async () => { + await expectRevert(multiSig.addEthereumAddress(ZERO_ADDRESS), "Invalid address"); + }); + + it("Only owner should be able to add address", async () => { + await expectRevert( + multiSig.addEthereumAddress(account1, { from: account1 }), + "unauthorized" + ); + }); + + it("Should be able to add address", async () => { + let tx = await multiSig.addEthereumAddress(account1); + + let isOwner = await multiSig.isEthereumAddressOwner.call(account1); + expect(isOwner).to.be.true; + + let list = await multiSig.getEthereumAddresses.call(); + expect(list.length).to.be.equal(1); + expect(list[0]).to.be.equal(account1); + + expectEvent(tx, "EthereumAddressAdded", { + account: account1, + }); + }); + + /// @dev For test coverage + it("Should be able to add redundant addresses, but ignored", async () => { + let tx = await multiSig.addEthereumAddresses([account1, account1, account2]); + + let isOwner = await multiSig.isEthereumAddressOwner.call(account1); + expect(isOwner).to.be.true; + + let list = await multiSig.getEthereumAddresses.call(); + expect(list.length).to.be.equal(2); + expect(list[0]).to.be.equal(account1); + expect(list[1]).to.be.equal(account2); + + expectEvent(tx, "EthereumAddressAdded", { + account: account1, + }); + }); + }); + + describe("addEthereumAddresses", () => { + it("Shouldn't be able to add zero addresses", async () => { + await expectRevert(multiSig.addEthereumAddresses([ZERO_ADDRESS]), "Invalid address"); + }); + + it("Only owner should be able to add addresses", async () => { + await expectRevert( + multiSig.addEthereumAddresses([account1, account2], { from: account1 }), + "unauthorized" + ); + }); + + it("Should be able to add addresses", async () => { + let tx = await multiSig.addEthereumAddresses([account1, account2]); + + let isOwner = await multiSig.isEthereumAddressOwner.call(account1); + expect(isOwner).to.be.true; + isOwner = await multiSig.isEthereumAddressOwner.call(account2); + expect(isOwner).to.be.true; + + let list = await multiSig.getEthereumAddresses.call(); + expect(list.length).to.be.equal(2); + expect(list[0]).to.be.equal(account1); + expect(list[1]).to.be.equal(account2); + + expectEvent(tx, "EthereumAddressAdded", { + account: account1, + }); + expectEvent(tx, "EthereumAddressAdded", { + account: account2, + }); + }); + }); + + describe("changeEthereumRequirement", () => { + it("Only owner should be able to change ethereum requirement", async () => { + await expectRevert( + multiSig.changeEthereumRequirement(1, { from: account1 }), + "unauthorized" + ); + }); + + it("Only owner should be able to change ethereum requirement", async () => { + await expectRevert(multiSig.changeEthereumRequirement(5), "Invalid required"); + }); + + it("Should be able to change ethereum requirement", async () => { + let required = 3; + await multiSig.addEthereumAddresses([account1, account2, account3]); + + let tx = await multiSig.changeEthereumRequirement(required); + + expect(await multiSig.ethereumRequired.call()).to.be.bignumber.equal(new BN(required)); + + expectEvent(tx, "EthereumRequirementChanged", { + required: new BN(required), + }); + }); + }); + + describe("addBitcoinAddress", () => { + it("Shouldn't be able to add zero address", async () => { + await expectRevert(multiSig.addBitcoinAddress(EMPTY_ADDRESS), "Invalid address"); + }); + + it("Only owner should be able to add address", async () => { + await expectRevert( + multiSig.addBitcoinAddress(bitcoinAccount1, { from: account1 }), + "unauthorized" + ); + }); + + it("Should be able to add address", async () => { + let tx = await multiSig.addBitcoinAddress(bitcoinAccount1); + + let isOwner = await multiSig.isBitcoinAddressOwner.call(bitcoinAccount1); + expect(isOwner).to.be.true; + + let list = await multiSig.getBitcoinAddresses.call(); + expect(list.length).to.be.equal(1); + expect(list[0]).to.be.equal(bitcoinAccount1); + + expectEvent(tx, "BitcoinAddressAdded", { + account: bitcoinAccount1, + }); + }); + }); + + describe("addBitcoinAddresses", () => { + it("Shouldn't be able to add zero addresses", async () => { + await expectRevert(multiSig.addBitcoinAddresses([EMPTY_ADDRESS]), "Invalid address"); + }); + + it("Only owner should be able to add addresses", async () => { + await expectRevert( + multiSig.addBitcoinAddresses([bitcoinAccount1, bitcoinAccount2], { + from: account1, + }), + "unauthorized" + ); + }); + + it("Should be able to add addresses", async () => { + let tx = await multiSig.addBitcoinAddresses([bitcoinAccount1, bitcoinAccount2]); + + let isOwner = await multiSig.isBitcoinAddressOwner.call(bitcoinAccount1); + expect(isOwner).to.be.true; + isOwner = await multiSig.isBitcoinAddressOwner.call(bitcoinAccount2); + expect(isOwner).to.be.true; + + let list = await multiSig.getBitcoinAddresses.call(); + expect(list.length).to.be.equal(2); + expect(list[0]).to.be.equal(bitcoinAccount1); + expect(list[1]).to.be.equal(bitcoinAccount2); + + expectEvent(tx, "BitcoinAddressAdded", { + account: bitcoinAccount1, + }); + expectEvent(tx, "BitcoinAddressAdded", { + account: bitcoinAccount2, + }); + }); + + /// @dev For test coverage + it("Should be able to add redundant addresses, but ignored", async () => { + let tx = await multiSig.addBitcoinAddresses([ + bitcoinAccount1, + bitcoinAccount1, + bitcoinAccount2, + ]); + + let isOwner = await multiSig.isBitcoinAddressOwner.call(bitcoinAccount1); + expect(isOwner).to.be.true; + isOwner = await multiSig.isBitcoinAddressOwner.call(bitcoinAccount2); + expect(isOwner).to.be.true; + + let list = await multiSig.getBitcoinAddresses.call(); + expect(list.length).to.be.equal(2); + expect(list[0]).to.be.equal(bitcoinAccount1); + expect(list[1]).to.be.equal(bitcoinAccount2); + + expectEvent(tx, "BitcoinAddressAdded", { + account: bitcoinAccount1, + }); + expectEvent(tx, "BitcoinAddressAdded", { + account: bitcoinAccount2, + }); + }); + }); + + describe("addEthereumAndBitcoinAddresses", () => { + it("Shouldn't be able to add zero address", async () => { + await expectRevert( + multiSig.addEthereumAndBitcoinAddresses([ZERO_ADDRESS], [bitcoinAccount1]), + "Invalid address" + ); + }); + + it("Only owner should be able to add address", async () => { + await expectRevert( + multiSig.addEthereumAndBitcoinAddresses( + [account1, account2], + [bitcoinAccount1, bitcoinAccount2], + { from: account1 } + ), + "unauthorized" + ); + }); + + it("Should be able to add addresses", async () => { + let tx = await multiSig.addEthereumAndBitcoinAddresses( + [account1, account2], + [bitcoinAccount1, bitcoinAccount2] + ); + + let isOwner = await multiSig.isEthereumAddressOwner.call(account1); + expect(isOwner).to.be.true; + isOwner = await multiSig.isEthereumAddressOwner.call(account2); + expect(isOwner).to.be.true; + + let list = await multiSig.getEthereumAddresses.call(); + expect(list.length).to.be.equal(2); + expect(list[0]).to.be.equal(account1); + expect(list[1]).to.be.equal(account2); + + expectEvent(tx, "EthereumAddressAdded", { + account: account1, + }); + expectEvent(tx, "EthereumAddressAdded", { + account: account2, + }); + + isOwner = await multiSig.isBitcoinAddressOwner.call(bitcoinAccount1); + expect(isOwner).to.be.true; + isOwner = await multiSig.isBitcoinAddressOwner.call(bitcoinAccount2); + expect(isOwner).to.be.true; + + list = await multiSig.getBitcoinAddresses.call(); + expect(list.length).to.be.equal(2); + expect(list[0]).to.be.equal(bitcoinAccount1); + expect(list[1]).to.be.equal(bitcoinAccount2); + + expectEvent(tx, "BitcoinAddressAdded", { + account: bitcoinAccount1, + }); + expectEvent(tx, "BitcoinAddressAdded", { + account: bitcoinAccount2, + }); + }); + }); + + describe("removeEthereumAddress", () => { + it("Shouldn't be able to remove zero address", async () => { + await expectRevert(multiSig.removeEthereumAddress(ZERO_ADDRESS), "Invalid address"); + }); + + it("Only owner should be able to remove address", async () => { + await expectRevert( + multiSig.removeEthereumAddress(account1, { from: account1 }), + "unauthorized" + ); + }); + + it("Should be able to remove address", async () => { + await multiSig.addEthereumAddress(account1); + let tx = await multiSig.removeEthereumAddress(account1); + + let isOwner = await multiSig.isEthereumAddressOwner.call(account1); + expect(isOwner).to.be.false; + + let list = await multiSig.getEthereumAddresses.call(); + expect(list.length).to.be.equal(0); + + expectEvent(tx, "EthereumAddressRemoved", { + account: account1, + }); + }); + }); + + describe("removeEthereumAddresses", () => { + it("Shouldn't be able to remove zero addresses", async () => { + await expectRevert( + multiSig.removeEthereumAddresses([ZERO_ADDRESS]), + "Invalid address" + ); + }); + + it("Only owner should be remove to add addresses", async () => { + await expectRevert( + multiSig.removeEthereumAddresses([account1, account2], { from: account1 }), + "unauthorized" + ); + }); + + it("Should be able to remove addresses", async () => { + await multiSig.addEthereumAddresses([account1, account2]); + let tx = await multiSig.removeEthereumAddresses([account1, account2]); + + let isOwner = await multiSig.isEthereumAddressOwner.call(account1); + expect(isOwner).to.be.false; + isOwner = await multiSig.isEthereumAddressOwner.call(account2); + expect(isOwner).to.be.false; + + let list = await multiSig.getEthereumAddresses.call(); + expect(list.length).to.be.equal(0); + + expectEvent(tx, "EthereumAddressRemoved", { + account: account1, + }); + expectEvent(tx, "EthereumAddressRemoved", { + account: account2, + }); + }); + + /// @dev For test coverage + it("Should be able to remove redundant addresses, just ignoring repeated ones", async () => { + await multiSig.addEthereumAddresses([account1, account2]); + let tx = await multiSig.removeEthereumAddresses([account1, account1, account2]); + + let isOwner = await multiSig.isEthereumAddressOwner.call(account1); + expect(isOwner).to.be.false; + isOwner = await multiSig.isEthereumAddressOwner.call(account2); + expect(isOwner).to.be.false; + + let list = await multiSig.getEthereumAddresses.call(); + expect(list.length).to.be.equal(0); + + expectEvent(tx, "EthereumAddressRemoved", { + account: account1, + }); + expectEvent(tx, "EthereumAddressRemoved", { + account: account2, + }); + }); + }); + + describe("removeBitcoinAddress", () => { + it("Shouldn't be able to remove zero address", async () => { + await expectRevert(multiSig.removeBitcoinAddress(EMPTY_ADDRESS), "Invalid address"); + }); + + it("Only owner should be remove to add address", async () => { + await expectRevert( + multiSig.removeBitcoinAddress(bitcoinAccount1, { from: account1 }), + "unauthorized" + ); + }); + + it("Should be able to remove address", async () => { + await multiSig.addBitcoinAddress(bitcoinAccount1); + let tx = await multiSig.removeBitcoinAddress(bitcoinAccount1); + + let isOwner = await multiSig.isBitcoinAddressOwner.call(bitcoinAccount1); + expect(isOwner).to.be.false; + + let list = await multiSig.getBitcoinAddresses.call(); + expect(list.length).to.be.equal(0); + + expectEvent(tx, "BitcoinAddressRemoved", { + account: bitcoinAccount1, + }); + }); + }); + + describe("removeBitcoinAddresses", () => { + it("Shouldn't be able to remove zero addresses", async () => { + await expectRevert( + multiSig.removeBitcoinAddresses([EMPTY_ADDRESS]), + "Invalid address" + ); + }); + + it("Only owner should be able to remove addresses", async () => { + await expectRevert( + multiSig.removeBitcoinAddresses([bitcoinAccount1, bitcoinAccount2], { + from: account1, + }), + "unauthorized" + ); + }); + + it("Should be able to remove addresses", async () => { + await multiSig.addBitcoinAddresses([bitcoinAccount1, bitcoinAccount2]); + let tx = await multiSig.removeBitcoinAddresses([bitcoinAccount1, bitcoinAccount2]); + + let isOwner = await multiSig.isBitcoinAddressOwner.call(bitcoinAccount1); + expect(isOwner).to.be.false; + isOwner = await multiSig.isBitcoinAddressOwner.call(bitcoinAccount2); + expect(isOwner).to.be.false; + + let list = await multiSig.getBitcoinAddresses.call(); + expect(list.length).to.be.equal(0); + + expectEvent(tx, "BitcoinAddressRemoved", { + account: bitcoinAccount1, + }); + expectEvent(tx, "BitcoinAddressRemoved", { + account: bitcoinAccount2, + }); + }); + + /// @dev For test coverage + it("Should be able to remove redundant addresses, just ignoring repeated ones", async () => { + await multiSig.addBitcoinAddresses([bitcoinAccount1, bitcoinAccount2]); + let tx = await multiSig.removeBitcoinAddresses([ + bitcoinAccount1, + bitcoinAccount1, + bitcoinAccount2, + ]); + + let isOwner = await multiSig.isBitcoinAddressOwner.call(bitcoinAccount1); + expect(isOwner).to.be.false; + isOwner = await multiSig.isBitcoinAddressOwner.call(bitcoinAccount2); + expect(isOwner).to.be.false; + + let list = await multiSig.getBitcoinAddresses.call(); + expect(list.length).to.be.equal(0); + + expectEvent(tx, "BitcoinAddressRemoved", { + account: bitcoinAccount1, + }); + expectEvent(tx, "BitcoinAddressRemoved", { + account: bitcoinAccount2, + }); + }); + }); + + describe("removeEthereumAndBitcoinAddresses", () => { + it("Shouldn't be able to remove zero address", async () => { + await expectRevert( + multiSig.removeEthereumAndBitcoinAddresses([ZERO_ADDRESS], [bitcoinAccount1]), + "Invalid address" + ); + }); + + it("Only owner should be able to remove address", async () => { + await expectRevert( + multiSig.removeEthereumAndBitcoinAddresses( + [account1, account2], + [bitcoinAccount1, bitcoinAccount2], + { from: account1 } + ), + "unauthorized" + ); + }); + + it("Should be able to remove addresses", async () => { + await multiSig.addEthereumAndBitcoinAddresses( + [account1, account2], + [bitcoinAccount1, bitcoinAccount2] + ); + let tx = await multiSig.removeEthereumAndBitcoinAddresses( + [account1, account2], + [bitcoinAccount1, bitcoinAccount2] + ); + + let isOwner = await multiSig.isEthereumAddressOwner.call(account1); + expect(isOwner).to.be.false; + isOwner = await multiSig.isEthereumAddressOwner.call(account2); + expect(isOwner).to.be.false; + + let list = await multiSig.getEthereumAddresses.call(); + expect(list.length).to.be.equal(0); + + expectEvent(tx, "EthereumAddressRemoved", { + account: account1, + }); + expectEvent(tx, "EthereumAddressRemoved", { + account: account2, + }); + + isOwner = await multiSig.isBitcoinAddressOwner.call(bitcoinAccount1); + expect(isOwner).to.be.false; + isOwner = await multiSig.isBitcoinAddressOwner.call(bitcoinAccount2); + expect(isOwner).to.be.false; + + list = await multiSig.getBitcoinAddresses.call(); + expect(list.length).to.be.equal(0); + + expectEvent(tx, "BitcoinAddressRemoved", { + account: bitcoinAccount1, + }); + expectEvent(tx, "BitcoinAddressRemoved", { + account: bitcoinAccount2, + }); + }); + }); + + describe("changeBitcoinRequirement", () => { + it("Only owner should be able to change ethereum requirement", async () => { + await expectRevert( + multiSig.changeBitcoinRequirement(1, { from: account1 }), + "unauthorized" + ); + }); + + it("Only owner should be able to change ethereum requirement", async () => { + await expectRevert(multiSig.changeBitcoinRequirement(5), "Invalid required"); + }); + + it("Should be able to change ethereum requirement", async () => { + let required = 3; + await multiSig.addBitcoinAddresses([ + bitcoinAccount1, + bitcoinAccount2, + bitcoinAccount3, + ]); + + let tx = await multiSig.changeBitcoinRequirement(required); + + expect(await multiSig.bitcoinRequired.call()).to.be.bignumber.equal(new BN(required)); + + expectEvent(tx, "BitcoinRequirementChanged", { + required: new BN(required), + }); + }); + }); }); diff --git a/tests/multisig/MultiSigWallet.js b/tests/multisig/MultiSigWallet.js index b628f9d98..f4c19fc27 100644 --- a/tests/multisig/MultiSigWallet.js +++ b/tests/multisig/MultiSigWallet.js @@ -18,315 +18,321 @@ const wei = web3.utils.toWei; const MultiSigWallet = artifacts.require("MultiSigWallet"); contract("MultiSigWallet:", (accounts) => { - let root, account1, account2, account3, account4, account5; - let multiSig; - let defaultData; - - async function deploymentAndInitFixture(_wallets, _provider) { - multiSig = await MultiSigWallet.new([account1, account2, account3], 2); - } - - before(async () => { - [root, account1, account2, account3, account4, account5, ...accounts] = accounts; - }); - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - describe("fallback", () => { - it("Should allow to deposit ether", async () => { - const tx = await multiSig.send(wei("0.0000000000000001", "ether")); - expectEvent(tx, "Deposit", { - sender: root, - value: wei("0.0000000000000001", "ether"), - }); - }); - }); - - describe("constructor", () => { - it("Shouldn't allow multisig for invalid requirement", async () => { - await expectRevert.unspecified(MultiSigWallet.new([account1], 2)); - }); - - it("Should allow creation of new multisig", async () => { - let newMultiSig = await MultiSigWallet.new([account1, account2, account3], 2); - const ownerCount = await newMultiSig.getOwners(); - expect(ownerCount.length).to.be.equal(3); - expect(ownerCount[0]).to.be.equal(account1); - expect(ownerCount[1]).to.be.equal(account2); - expect(ownerCount[2]).to.be.equal(account3); - expect(await newMultiSig.required()).to.be.bignumber.equal(new BN(2)); - }); - }); - - describe("addOwner", () => { - it("should revert when submitting a transaction without wallet", async () => { - await expectRevert.unspecified(multiSig.addOwner(account5, { from: account5 })); - await expectRevert.unspecified(multiSig.addOwner(account5, { from: account1 })); - }); - - it("should revert when submitting a transaction with null destination", async () => { - let multiSigInterface = new web3.eth.Contract(multiSig.abi, ZERO_ADDRESS); - let data = multiSigInterface.methods.addOwner(ZERO_ADDRESS).encodeABI(); - await multiSig.submitTransaction(multiSig.address, 0, data, { from: account1 }); - let tx = await multiSig.confirmTransaction(0, { from: account2 }); - expectEvent(tx, "ExecutionFailure"); - }); - - it("should revert when submitting a transaction with owner already exists", async () => { - let multiSigInterface = new web3.eth.Contract(multiSig.abi, ZERO_ADDRESS); - let data = multiSigInterface.methods.addOwner(account1).encodeABI(); - await multiSig.submitTransaction(multiSig.address, 0, data, { from: account1 }); - let tx = await multiSig.confirmTransaction(0, { from: account2 }); - expectEvent(tx, "ExecutionFailure"); - }); - - it("Should allow to create a new owner for Multsig", async () => { - let multiSigInterface = new web3.eth.Contract(multiSig.abi, ZERO_ADDRESS); - let data = multiSigInterface.methods.addOwner(account4).encodeABI(); - // Submit Transaction - await multiSig.submitTransaction(multiSig.address, 0, data, { from: account1 }); - // Confirm Transaction - await multiSig.confirmTransaction(0, { from: account2 }); - const ownerCount = await multiSig.getOwners(); - expect(ownerCount.length).to.be.equal(4); - expect(ownerCount[3]).to.be.equal(account4); - }); - }); - - describe("multiSig coverage", () => { - /// @dev Test coverage for transactionExists modifier - it("should revert when calling confirmTransaction for an inexistent id", async () => { - let multiSigInterface = new web3.eth.Contract(multiSig.abi, ZERO_ADDRESS); - let data = multiSigInterface.methods.addOwner(account4).encodeABI(); - // Submit Transaction - await multiSig.submitTransaction(multiSig.address, 0, data, { from: account1 }); - - // Try to confirm Transaction w/ wrong id - await expectRevert.unspecified(multiSig.confirmTransaction(1, { from: account2 })); - }); - - /// @dev Test coverage for notConfirmed modifier - it("should revert when calling confirmTransaction for a tx already confirmed (same user)", async () => { - let multiSigInterface = new web3.eth.Contract(multiSig.abi, ZERO_ADDRESS); - let data = multiSigInterface.methods.addOwner(account4).encodeABI(); - // Submit Transaction - await multiSig.submitTransaction(multiSig.address, 0, data, { from: account1 }); - - // Confirm Transaction - await multiSig.confirmTransaction(0, { from: account2 }); - const ownerCount = await multiSig.getOwners(); - expect(ownerCount.length).to.be.equal(4); - expect(ownerCount[3]).to.be.equal(account4); - - // Try to confirm again the same transaction by the same user - await expectRevert.unspecified(multiSig.confirmTransaction(0, { from: account2 })); - }); - - /// @dev Test coverage for fallback w/o value transfer - it("should ignore a call to fallback function w/ value 0", async () => { - // It doesn't revert, just it does nothing - await multiSig.sendTransaction({}); - }); - }); - - describe("removeOwner", () => { - it("should revert when submitting a transaction without wallet", async () => { - await expectRevert.unspecified(multiSig.removeOwner(account5, { from: account5 })); - await expectRevert.unspecified(multiSig.removeOwner(account5, { from: account1 })); - }); - - it("should fail when removing non-existing owner", async () => { - let multiSigInterface = new web3.eth.Contract(multiSig.abi, ZERO_ADDRESS); - let data = multiSigInterface.methods.removeOwner(account5).encodeABI(); - await multiSig.submitTransaction(multiSig.address, 0, data, { from: account1 }); - let tx = await multiSig.confirmTransaction(0, { from: account2 }); - expectEvent(tx, "ExecutionFailure"); - }); - - it("Should remove a owner and change requirement for Multsig", async () => { - let newMultiSig = await MultiSigWallet.new([account1, account2], 2); - let multiSigInterface = new web3.eth.Contract(newMultiSig.abi, ZERO_ADDRESS); - let data = multiSigInterface.methods.removeOwner(account2).encodeABI(); - // Submit Transaction - await newMultiSig.submitTransaction(newMultiSig.address, 0, data, { from: account1 }); - // Confirm Transaction - let tx = await newMultiSig.confirmTransaction(0, { from: account2 }); - const ownerCount = await newMultiSig.getOwners(); - expect(ownerCount.length).to.be.equal(1); - expectEvent(tx, "OwnerRemoval"); - expectEvent(tx, "RequirementChange"); - }); - - it("Should allow to remove a owner for Multsig", async () => { - let multiSigInterface = new web3.eth.Contract(multiSig.abi, ZERO_ADDRESS); - let data = multiSigInterface.methods.removeOwner(account2).encodeABI(); - // Submit Transaction - await multiSig.submitTransaction(multiSig.address, 0, data, { from: account1 }); - // Confirm Transaction - let tx = await multiSig.confirmTransaction(0, { from: account2 }); - const ownerCount = await multiSig.getOwners(); - expect(ownerCount.length).to.be.equal(2); - expectEvent(tx, "OwnerRemoval"); - }); - }); - - describe("replaceOwner", () => { - it("should revert when submitting a transaction without wallet", async () => { - await expectRevert.unspecified(multiSig.replaceOwner(account2, account5, { from: account5 })); - await expectRevert.unspecified(multiSig.replaceOwner(account2, account5, { from: account1 })); - }); - - it("should fail when replacing non-existing owner", async () => { - let multiSigInterface = new web3.eth.Contract(multiSig.abi, ZERO_ADDRESS); - let data = multiSigInterface.methods.replaceOwner(account5, account4).encodeABI(); - await multiSig.submitTransaction(multiSig.address, 0, data, { from: account1 }); - let tx = await multiSig.confirmTransaction(0, { from: account2 }); - expectEvent(tx, "ExecutionFailure"); - }); - - it("Should allow to replacement of a owner for Multsig", async () => { - let multiSigInterface = new web3.eth.Contract(multiSig.abi, ZERO_ADDRESS); - let data = multiSigInterface.methods.replaceOwner(account3, account5).encodeABI(); - // Submit Transaction - await multiSig.submitTransaction(multiSig.address, 0, data, { from: account1 }); - // Confirm Transaction - let tx = await multiSig.confirmTransaction(0, { from: account2 }); - const ownerCount = await multiSig.getOwners(); - expect(ownerCount.length).to.be.equal(3); - expectEvent(tx, "OwnerRemoval"); - expectEvent(tx, "OwnerAddition"); - }); - }); - - describe("changeRequirement", () => { - it("should revert when changing a requirement not from wallet itself", async () => { - await expectRevert.unspecified(multiSig.changeRequirement(1, { from: account5 })); - await expectRevert.unspecified(multiSig.changeRequirement(1, { from: account1 })); - }); - - it("should fail transaction when changing a requirement to zero", async () => { - let multiSigInterface = new web3.eth.Contract(multiSig.abi, ZERO_ADDRESS); - let data = multiSigInterface.methods.changeRequirement(0).encodeABI(); - await multiSig.submitTransaction(multiSig.address, 0, data, { from: account1 }); - let tx = await multiSig.confirmTransaction(0, { from: account2 }); - expectEvent(tx, "ExecutionFailure"); - }); - - it("should fail transaction when changing a requirement to more than owners", async () => { - let multiSigInterface = new web3.eth.Contract(multiSig.abi, ZERO_ADDRESS); - let data = multiSigInterface.methods.changeRequirement(4).encodeABI(); - await multiSig.submitTransaction(multiSig.address, 0, data, { from: account1 }); - let tx = await multiSig.confirmTransaction(0, { from: account2 }); - expectEvent(tx, "ExecutionFailure"); - }); - - it("should change transaction", async () => { - let multiSigInterface = new web3.eth.Contract(multiSig.abi, ZERO_ADDRESS); - let data = multiSigInterface.methods.changeRequirement(3).encodeABI(); - await multiSig.submitTransaction(multiSig.address, 0, data, { from: account1 }); - let tx = await multiSig.confirmTransaction(0, { from: account2 }); - const ownerCount = await multiSig.getOwners(); - expect(ownerCount.length).to.be.equal(3); - expectEvent(tx, "Confirmation"); - expectEvent(tx, "RequirementChange"); - expectEvent(tx, "Execution"); - }); - }); - - describe("revokeConfirmation", () => { - it("should revert when revoking confirmation from unauthorized caller", async () => { - defaultDataSet(); - await multiSig.submitTransaction(multiSig.address, 0, defaultData, { from: account1 }); - await expectRevert.unspecified(multiSig.revokeConfirmation(0, { from: account5 })); - }); - - it("should revert when revoking unconfirmed transaction", async () => { - await expectRevert.unspecified(multiSig.revokeConfirmation(0, { from: account1 })); - }); - - it("should revert when revoking confirmation for already executed transaction", async () => { - defaultDataSet(); - await multiSig.submitTransaction(multiSig.address, 0, defaultData, { from: account1 }); - await multiSig.confirmTransaction(0, { from: account2 }); - await expectRevert.unspecified(multiSig.revokeConfirmation(0, { from: account3 })); - }); - - it("should revoke confirmation using correct data", async () => { - defaultDataSet(); - await multiSig.submitTransaction(multiSig.address, 0, defaultData, { from: account1 }); - let tx = await multiSig.revokeConfirmation(0, { from: account1 }); - expectEvent(tx, "Revocation"); - }); - }); - - describe("executeTransaction", () => { - it("should fail transaction execution for non-owners", async () => { - await expectRevert.unspecified(multiSig.executeTransaction(0, { from: account5 })); - }); - - it("should revert when executing unconfirmed transaction", async () => { - await expectRevert.unspecified(multiSig.executeTransaction(0, { from: account1 })); - }); - - it("should revert when executing confirmation for already executed transaction", async () => { - defaultDataSet(); - await multiSig.submitTransaction(multiSig.address, 0, defaultData, { from: account1 }); - await multiSig.confirmTransaction(0, { from: account2 }); - await expectRevert.unspecified(multiSig.executeTransaction(0, { from: account3 })); - }); - - it("should execute transaction using correct data", async () => { - defaultDataSet(); - await multiSig.submitTransaction(multiSig.address, 0, defaultData, { from: account1 }); - await multiSig.revokeConfirmation(0, { from: account1 }); - await multiSig.confirmTransaction(0, { from: account2 }); - let tx = await multiSig.confirmTransaction(0, { from: account3 }); - expectEvent(tx, "Execution"); - }); - }); - - describe("getters", () => { - it("should get confirmation count", async () => { - defaultDataSet(); - await multiSig.submitTransaction(multiSig.address, 0, defaultData, { from: account1 }); - expect(await multiSig.getConfirmationCount(0)).to.be.bignumber.equal(new BN(1)); - }); - - it("should get transaction count", async () => { - defaultDataSet(); - await multiSig.submitTransaction(multiSig.address, 0, defaultData, { from: account1 }); - expect(await multiSig.getTransactionCount(true, true)).to.be.bignumber.equal(new BN(1)); - }); - - it("should get the owner address for confirmed transactions", async () => { - defaultDataSet(); - await multiSig.submitTransaction(multiSig.address, 0, defaultData, { from: account1 }); - await multiSig.confirmTransaction(0, { from: account2 }); - let result = await multiSig.getConfirmations(0); - expect(result.length).to.be.equal(2); - expect(result[0]).to.be.equal(account1); - expect(result[1]).to.be.equal(account2); - }); - - it("should get the list of transaction IDs in defined range", async () => { - defaultDataSet(); - await multiSig.submitTransaction(multiSig.address, 0, defaultData, { from: account1 }); - await multiSig.confirmTransaction(0, { from: account2 }); - let multiSigInterface = new web3.eth.Contract(multiSig.abi, ZERO_ADDRESS); - let data = multiSigInterface.methods.replaceOwner(account3, account4).encodeABI(); - await multiSig.submitTransaction(multiSig.address, 0, data, { from: account2 }); - await multiSig.confirmTransaction(1, { from: account3 }); - let result = await multiSig.getTransactionIds(0, 2, true, true); - expect(result.length).to.be.equal(2); - expect(result[0]).to.be.bignumber.equal(new BN(0)); - expect(result[1]).to.be.bignumber.equal(new BN(1)); - }); - }); - - function defaultDataSet() { - let multiSigInterface = new web3.eth.Contract(multiSig.abi, ZERO_ADDRESS); - defaultData = multiSigInterface.methods.addOwner(account5).encodeABI(); - } + let root, account1, account2, account3, account4, account5; + let multiSig; + let defaultData; + + async function deploymentAndInitFixture(_wallets, _provider) { + multiSig = await MultiSigWallet.new([account1, account2, account3], 2); + } + + before(async () => { + [root, account1, account2, account3, account4, account5, ...accounts] = accounts; + }); + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + describe("fallback", () => { + it("Should allow to deposit ether", async () => { + const tx = await multiSig.send(wei("0.0000000000000001", "ether")); + expectEvent(tx, "Deposit", { + sender: root, + value: wei("0.0000000000000001", "ether"), + }); + }); + }); + + describe("constructor", () => { + it("Shouldn't allow multisig for invalid requirement", async () => { + await expectRevert.unspecified(MultiSigWallet.new([account1], 2)); + }); + + it("Should allow creation of new multisig", async () => { + let newMultiSig = await MultiSigWallet.new([account1, account2, account3], 2); + const ownerCount = await newMultiSig.getOwners(); + expect(ownerCount.length).to.be.equal(3); + expect(ownerCount[0]).to.be.equal(account1); + expect(ownerCount[1]).to.be.equal(account2); + expect(ownerCount[2]).to.be.equal(account3); + expect(await newMultiSig.required()).to.be.bignumber.equal(new BN(2)); + }); + }); + + describe("addOwner", () => { + it("should revert when submitting a transaction without wallet", async () => { + await expectRevert.unspecified(multiSig.addOwner(account5, { from: account5 })); + await expectRevert.unspecified(multiSig.addOwner(account5, { from: account1 })); + }); + + it("should revert when submitting a transaction with null destination", async () => { + let multiSigInterface = new web3.eth.Contract(multiSig.abi, ZERO_ADDRESS); + let data = multiSigInterface.methods.addOwner(ZERO_ADDRESS).encodeABI(); + await multiSig.submitTransaction(multiSig.address, 0, data, { from: account1 }); + let tx = await multiSig.confirmTransaction(0, { from: account2 }); + expectEvent(tx, "ExecutionFailure"); + }); + + it("should revert when submitting a transaction with owner already exists", async () => { + let multiSigInterface = new web3.eth.Contract(multiSig.abi, ZERO_ADDRESS); + let data = multiSigInterface.methods.addOwner(account1).encodeABI(); + await multiSig.submitTransaction(multiSig.address, 0, data, { from: account1 }); + let tx = await multiSig.confirmTransaction(0, { from: account2 }); + expectEvent(tx, "ExecutionFailure"); + }); + + it("Should allow to create a new owner for Multsig", async () => { + let multiSigInterface = new web3.eth.Contract(multiSig.abi, ZERO_ADDRESS); + let data = multiSigInterface.methods.addOwner(account4).encodeABI(); + // Submit Transaction + await multiSig.submitTransaction(multiSig.address, 0, data, { from: account1 }); + // Confirm Transaction + await multiSig.confirmTransaction(0, { from: account2 }); + const ownerCount = await multiSig.getOwners(); + expect(ownerCount.length).to.be.equal(4); + expect(ownerCount[3]).to.be.equal(account4); + }); + }); + + describe("multiSig coverage", () => { + /// @dev Test coverage for transactionExists modifier + it("should revert when calling confirmTransaction for an inexistent id", async () => { + let multiSigInterface = new web3.eth.Contract(multiSig.abi, ZERO_ADDRESS); + let data = multiSigInterface.methods.addOwner(account4).encodeABI(); + // Submit Transaction + await multiSig.submitTransaction(multiSig.address, 0, data, { from: account1 }); + + // Try to confirm Transaction w/ wrong id + await expectRevert.unspecified(multiSig.confirmTransaction(1, { from: account2 })); + }); + + /// @dev Test coverage for notConfirmed modifier + it("should revert when calling confirmTransaction for a tx already confirmed (same user)", async () => { + let multiSigInterface = new web3.eth.Contract(multiSig.abi, ZERO_ADDRESS); + let data = multiSigInterface.methods.addOwner(account4).encodeABI(); + // Submit Transaction + await multiSig.submitTransaction(multiSig.address, 0, data, { from: account1 }); + + // Confirm Transaction + await multiSig.confirmTransaction(0, { from: account2 }); + const ownerCount = await multiSig.getOwners(); + expect(ownerCount.length).to.be.equal(4); + expect(ownerCount[3]).to.be.equal(account4); + + // Try to confirm again the same transaction by the same user + await expectRevert.unspecified(multiSig.confirmTransaction(0, { from: account2 })); + }); + + /// @dev Test coverage for fallback w/o value transfer + it("should ignore a call to fallback function w/ value 0", async () => { + // It doesn't revert, just it does nothing + await multiSig.sendTransaction({}); + }); + }); + + describe("removeOwner", () => { + it("should revert when submitting a transaction without wallet", async () => { + await expectRevert.unspecified(multiSig.removeOwner(account5, { from: account5 })); + await expectRevert.unspecified(multiSig.removeOwner(account5, { from: account1 })); + }); + + it("should fail when removing non-existing owner", async () => { + let multiSigInterface = new web3.eth.Contract(multiSig.abi, ZERO_ADDRESS); + let data = multiSigInterface.methods.removeOwner(account5).encodeABI(); + await multiSig.submitTransaction(multiSig.address, 0, data, { from: account1 }); + let tx = await multiSig.confirmTransaction(0, { from: account2 }); + expectEvent(tx, "ExecutionFailure"); + }); + + it("Should remove a owner and change requirement for Multsig", async () => { + let newMultiSig = await MultiSigWallet.new([account1, account2], 2); + let multiSigInterface = new web3.eth.Contract(newMultiSig.abi, ZERO_ADDRESS); + let data = multiSigInterface.methods.removeOwner(account2).encodeABI(); + // Submit Transaction + await newMultiSig.submitTransaction(newMultiSig.address, 0, data, { from: account1 }); + // Confirm Transaction + let tx = await newMultiSig.confirmTransaction(0, { from: account2 }); + const ownerCount = await newMultiSig.getOwners(); + expect(ownerCount.length).to.be.equal(1); + expectEvent(tx, "OwnerRemoval"); + expectEvent(tx, "RequirementChange"); + }); + + it("Should allow to remove a owner for Multsig", async () => { + let multiSigInterface = new web3.eth.Contract(multiSig.abi, ZERO_ADDRESS); + let data = multiSigInterface.methods.removeOwner(account2).encodeABI(); + // Submit Transaction + await multiSig.submitTransaction(multiSig.address, 0, data, { from: account1 }); + // Confirm Transaction + let tx = await multiSig.confirmTransaction(0, { from: account2 }); + const ownerCount = await multiSig.getOwners(); + expect(ownerCount.length).to.be.equal(2); + expectEvent(tx, "OwnerRemoval"); + }); + }); + + describe("replaceOwner", () => { + it("should revert when submitting a transaction without wallet", async () => { + await expectRevert.unspecified( + multiSig.replaceOwner(account2, account5, { from: account5 }) + ); + await expectRevert.unspecified( + multiSig.replaceOwner(account2, account5, { from: account1 }) + ); + }); + + it("should fail when replacing non-existing owner", async () => { + let multiSigInterface = new web3.eth.Contract(multiSig.abi, ZERO_ADDRESS); + let data = multiSigInterface.methods.replaceOwner(account5, account4).encodeABI(); + await multiSig.submitTransaction(multiSig.address, 0, data, { from: account1 }); + let tx = await multiSig.confirmTransaction(0, { from: account2 }); + expectEvent(tx, "ExecutionFailure"); + }); + + it("Should allow to replacement of a owner for Multsig", async () => { + let multiSigInterface = new web3.eth.Contract(multiSig.abi, ZERO_ADDRESS); + let data = multiSigInterface.methods.replaceOwner(account3, account5).encodeABI(); + // Submit Transaction + await multiSig.submitTransaction(multiSig.address, 0, data, { from: account1 }); + // Confirm Transaction + let tx = await multiSig.confirmTransaction(0, { from: account2 }); + const ownerCount = await multiSig.getOwners(); + expect(ownerCount.length).to.be.equal(3); + expectEvent(tx, "OwnerRemoval"); + expectEvent(tx, "OwnerAddition"); + }); + }); + + describe("changeRequirement", () => { + it("should revert when changing a requirement not from wallet itself", async () => { + await expectRevert.unspecified(multiSig.changeRequirement(1, { from: account5 })); + await expectRevert.unspecified(multiSig.changeRequirement(1, { from: account1 })); + }); + + it("should fail transaction when changing a requirement to zero", async () => { + let multiSigInterface = new web3.eth.Contract(multiSig.abi, ZERO_ADDRESS); + let data = multiSigInterface.methods.changeRequirement(0).encodeABI(); + await multiSig.submitTransaction(multiSig.address, 0, data, { from: account1 }); + let tx = await multiSig.confirmTransaction(0, { from: account2 }); + expectEvent(tx, "ExecutionFailure"); + }); + + it("should fail transaction when changing a requirement to more than owners", async () => { + let multiSigInterface = new web3.eth.Contract(multiSig.abi, ZERO_ADDRESS); + let data = multiSigInterface.methods.changeRequirement(4).encodeABI(); + await multiSig.submitTransaction(multiSig.address, 0, data, { from: account1 }); + let tx = await multiSig.confirmTransaction(0, { from: account2 }); + expectEvent(tx, "ExecutionFailure"); + }); + + it("should change transaction", async () => { + let multiSigInterface = new web3.eth.Contract(multiSig.abi, ZERO_ADDRESS); + let data = multiSigInterface.methods.changeRequirement(3).encodeABI(); + await multiSig.submitTransaction(multiSig.address, 0, data, { from: account1 }); + let tx = await multiSig.confirmTransaction(0, { from: account2 }); + const ownerCount = await multiSig.getOwners(); + expect(ownerCount.length).to.be.equal(3); + expectEvent(tx, "Confirmation"); + expectEvent(tx, "RequirementChange"); + expectEvent(tx, "Execution"); + }); + }); + + describe("revokeConfirmation", () => { + it("should revert when revoking confirmation from unauthorized caller", async () => { + defaultDataSet(); + await multiSig.submitTransaction(multiSig.address, 0, defaultData, { from: account1 }); + await expectRevert.unspecified(multiSig.revokeConfirmation(0, { from: account5 })); + }); + + it("should revert when revoking unconfirmed transaction", async () => { + await expectRevert.unspecified(multiSig.revokeConfirmation(0, { from: account1 })); + }); + + it("should revert when revoking confirmation for already executed transaction", async () => { + defaultDataSet(); + await multiSig.submitTransaction(multiSig.address, 0, defaultData, { from: account1 }); + await multiSig.confirmTransaction(0, { from: account2 }); + await expectRevert.unspecified(multiSig.revokeConfirmation(0, { from: account3 })); + }); + + it("should revoke confirmation using correct data", async () => { + defaultDataSet(); + await multiSig.submitTransaction(multiSig.address, 0, defaultData, { from: account1 }); + let tx = await multiSig.revokeConfirmation(0, { from: account1 }); + expectEvent(tx, "Revocation"); + }); + }); + + describe("executeTransaction", () => { + it("should fail transaction execution for non-owners", async () => { + await expectRevert.unspecified(multiSig.executeTransaction(0, { from: account5 })); + }); + + it("should revert when executing unconfirmed transaction", async () => { + await expectRevert.unspecified(multiSig.executeTransaction(0, { from: account1 })); + }); + + it("should revert when executing confirmation for already executed transaction", async () => { + defaultDataSet(); + await multiSig.submitTransaction(multiSig.address, 0, defaultData, { from: account1 }); + await multiSig.confirmTransaction(0, { from: account2 }); + await expectRevert.unspecified(multiSig.executeTransaction(0, { from: account3 })); + }); + + it("should execute transaction using correct data", async () => { + defaultDataSet(); + await multiSig.submitTransaction(multiSig.address, 0, defaultData, { from: account1 }); + await multiSig.revokeConfirmation(0, { from: account1 }); + await multiSig.confirmTransaction(0, { from: account2 }); + let tx = await multiSig.confirmTransaction(0, { from: account3 }); + expectEvent(tx, "Execution"); + }); + }); + + describe("getters", () => { + it("should get confirmation count", async () => { + defaultDataSet(); + await multiSig.submitTransaction(multiSig.address, 0, defaultData, { from: account1 }); + expect(await multiSig.getConfirmationCount(0)).to.be.bignumber.equal(new BN(1)); + }); + + it("should get transaction count", async () => { + defaultDataSet(); + await multiSig.submitTransaction(multiSig.address, 0, defaultData, { from: account1 }); + expect(await multiSig.getTransactionCount(true, true)).to.be.bignumber.equal( + new BN(1) + ); + }); + + it("should get the owner address for confirmed transactions", async () => { + defaultDataSet(); + await multiSig.submitTransaction(multiSig.address, 0, defaultData, { from: account1 }); + await multiSig.confirmTransaction(0, { from: account2 }); + let result = await multiSig.getConfirmations(0); + expect(result.length).to.be.equal(2); + expect(result[0]).to.be.equal(account1); + expect(result[1]).to.be.equal(account2); + }); + + it("should get the list of transaction IDs in defined range", async () => { + defaultDataSet(); + await multiSig.submitTransaction(multiSig.address, 0, defaultData, { from: account1 }); + await multiSig.confirmTransaction(0, { from: account2 }); + let multiSigInterface = new web3.eth.Contract(multiSig.abi, ZERO_ADDRESS); + let data = multiSigInterface.methods.replaceOwner(account3, account4).encodeABI(); + await multiSig.submitTransaction(multiSig.address, 0, data, { from: account2 }); + await multiSig.confirmTransaction(1, { from: account3 }); + let result = await multiSig.getTransactionIds(0, 2, true, true); + expect(result.length).to.be.equal(2); + expect(result[0]).to.be.bignumber.equal(new BN(0)); + expect(result[1]).to.be.bignumber.equal(new BN(1)); + }); + }); + + function defaultDataSet() { + let multiSigInterface = new web3.eth.Contract(multiSig.abi, ZERO_ADDRESS); + defaultData = multiSigInterface.methods.addOwner(account5).encodeABI(); + } }); diff --git a/tests/other/LoanOpeningsBorrowOrTradeFromPool.test.js b/tests/other/LoanOpeningsBorrowOrTradeFromPool.test.js index 34f2339c6..7814ddbe2 100644 --- a/tests/other/LoanOpeningsBorrowOrTradeFromPool.test.js +++ b/tests/other/LoanOpeningsBorrowOrTradeFromPool.test.js @@ -18,7 +18,16 @@ const { constants, BN, expectRevert } = require("@openzeppelin/test-helpers"); const LoanSettingsEvents = artifacts.require("LoanSettingsEvents"); const LoanOpeningsEvents = artifacts.require("LoanOpeningsEvents"); -const { getSUSD, getRBTC, getWRBTC, getBZRX, getPriceFeeds, getSovryn, getSOV, decodeLogs } = require("../Utils/initializer.js"); +const { + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getPriceFeeds, + getSovryn, + getSOV, + decodeLogs, +} = require("../Utils/initializer.js"); const wei = web3.utils.toWei; @@ -26,534 +35,550 @@ const oneEth = new BN(wei("1", "ether")); const hunEth = new BN(wei("100", "ether")); contract("LoanOpeningsBorrowOrTradeFromPool", (accounts) => { - let sovryn, SUSD, WRBTC, RBTC, BZRX, priceFeeds; - - async function deploymentAndInitFixture(_wallets, _provider) { - // Deploying sovrynProtocol w/ generic function from initializer.js - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - await getSOV(sovryn, priceFeeds, SUSD, accounts); - - // setup simulated loan pool - await sovryn.setLoanPool([accounts[1]], [accounts[2]]); - - const sovrynBeforeSUSDBalance = await SUSD.balanceOf(sovryn.address); - console.log("sovrynBeforeSUSDBalance", sovrynBeforeSUSDBalance.toString()); - - const sovrynBeforeRBTCBalance = await RBTC.balanceOf(sovryn.address); - console.log("sovrynBeforeRBTCBalance", sovrynBeforeRBTCBalance.toString()); - - /// @dev Generic mint useful for every test - await SUSD.mint(sovryn.address, hunEth, { from: accounts[0] }); - } - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - const LinkDaiMarginParamsId = async () => { - const loanParams = { - id: constants.ZERO_BYTES32, - active: false, - owner: constants.ZERO_ADDRESS, - loanToken: SUSD.address, - collateralToken: RBTC.address, - minInitialMargin: new BN(20).mul(oneEth), - maintenanceMargin: new BN(15).mul(oneEth), - fixedLoanTerm: "2419200", // 28 days - }; - const { receipt } = await sovryn.setupLoanParams([Object.values(loanParams)]); - const decode = decodeLogs(receipt.rawLogs, LoanSettingsEvents, "LoanParamsIdSetup"); - return decode[0].args["id"]; - }; - - const LinkDaiBorrowParamsId = async () => { - const loanParams = { - id: constants.ZERO_BYTES32, - active: false, - owner: constants.ZERO_ADDRESS, - loanToken: SUSD.address, - collateralToken: RBTC.address, - minInitialMargin: new BN(20).mul(oneEth), - maintenanceMargin: new BN(15).mul(oneEth), - fixedLoanTerm: "0", // torque loan - }; - const { receipt } = await sovryn.setupLoanParams([Object.values(loanParams)]); - const decode = decodeLogs(receipt.rawLogs, LoanSettingsEvents, "LoanParamsIdSetup"); - return decode[0].args["id"]; - }; - - describe("Tests loan opening isolated. does not work together with the other tests. needs to be run separately.", () => { - /* + let sovryn, SUSD, WRBTC, RBTC, BZRX, priceFeeds; + + async function deploymentAndInitFixture(_wallets, _provider) { + // Deploying sovrynProtocol w/ generic function from initializer.js + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + await getSOV(sovryn, priceFeeds, SUSD, accounts); + + // setup simulated loan pool + await sovryn.setLoanPool([accounts[1]], [accounts[2]]); + + const sovrynBeforeSUSDBalance = await SUSD.balanceOf(sovryn.address); + console.log("sovrynBeforeSUSDBalance", sovrynBeforeSUSDBalance.toString()); + + const sovrynBeforeRBTCBalance = await RBTC.balanceOf(sovryn.address); + console.log("sovrynBeforeRBTCBalance", sovrynBeforeRBTCBalance.toString()); + + /// @dev Generic mint useful for every test + await SUSD.mint(sovryn.address, hunEth, { from: accounts[0] }); + } + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + const LinkDaiMarginParamsId = async () => { + const loanParams = { + id: constants.ZERO_BYTES32, + active: false, + owner: constants.ZERO_ADDRESS, + loanToken: SUSD.address, + collateralToken: RBTC.address, + minInitialMargin: new BN(20).mul(oneEth), + maintenanceMargin: new BN(15).mul(oneEth), + fixedLoanTerm: "2419200", // 28 days + }; + const { receipt } = await sovryn.setupLoanParams([Object.values(loanParams)]); + const decode = decodeLogs(receipt.rawLogs, LoanSettingsEvents, "LoanParamsIdSetup"); + return decode[0].args["id"]; + }; + + const LinkDaiBorrowParamsId = async () => { + const loanParams = { + id: constants.ZERO_BYTES32, + active: false, + owner: constants.ZERO_ADDRESS, + loanToken: SUSD.address, + collateralToken: RBTC.address, + minInitialMargin: new BN(20).mul(oneEth), + maintenanceMargin: new BN(15).mul(oneEth), + fixedLoanTerm: "0", // torque loan + }; + const { receipt } = await sovryn.setupLoanParams([Object.values(loanParams)]); + const decode = decodeLogs(receipt.rawLogs, LoanSettingsEvents, "LoanParamsIdSetup"); + return decode[0].args["id"]; + }; + + describe("Tests loan opening isolated. does not work together with the other tests. needs to be run separately.", () => { + /* At this moment the maxLoanTerm is always 28 because it is hardcoded in setupLoanParams. So there are only fix-term loans. */ - it("Test marginTradeFromPool sim", async () => { - const loanTokenSent = hunEth; - const sovrynSwap = await sovryn.sovrynSwapContractRegistryAddress(); - // console.log('sovryn seap contract registry address is ',sovrynSwap) - // addressOf = sovrynSwap.addressOf(sovrynSwap.address) - console.log("returned address is ", sovrynSwap); - const swapsI = await sovryn.swapsImpl(); - console.log("swaps impl is ", swapsI); - const collateralTokenSent = await sovryn.getRequiredCollateral( - SUSD.address, - RBTC.address, - loanTokenSent, - new BN(50).mul(oneEth), - false - ); - console.log("required collateral:", collateralTokenSent.div(oneEth).toString()); - - await RBTC.mint(sovryn.address, collateralTokenSent, { from: accounts[0] }); - - console.log("loanTokenSent", loanTokenSent.toString()); - console.log("collateralTokenSent", collateralTokenSent.toString()); - - const tx = await sovryn.borrowOrTradeFromPool( - await LinkDaiMarginParamsId(), // loanParamsId - "0x0", // loanId - false, // isTorqueLoan, - hunEth, // initialMargin - [ - accounts[2], // lender - accounts[1], // borrower - accounts[1], // receiver - constants.ZERO_ADDRESS, // manager - ], - [ - new BN(5).mul(oneEth), // newRate (5%) - loanTokenSent, // newPrincipal - 0, // torqueInterest - loanTokenSent, // loanTokenSent - collateralTokenSent, // collateralTokenSent - ], - "0x", // loanDataBytes - { from: accounts[1] } - ); - - const sovrynAfterSUSDBalance = await SUSD.balanceOf(sovryn.address); - console.log("sovrynAfterSUSDBalance", sovrynAfterSUSDBalance.toString()); - - const sovrynAfterRBTCBalance = await RBTC.balanceOf(sovryn.address); - console.log("sovrynAfterRBTCBalance", sovrynAfterRBTCBalance.toString()); - - const decode = decodeLogs(tx.receipt.rawLogs, LoanOpeningsEvents, "Trade"); - const tradeEvent = decode[0].args; - - const interestForPosition = loanTokenSent - .mul(new BN(5).mul(oneEth)) - .div(hunEth) - .div(new BN(365)) - .mul(new BN(2419200)) - .div(new BN(86400)); - console.log("interestForPosition", interestForPosition.toString()); - - // expectedPositionSize = collateralTokenSent + ((loanTokenSent - interestForPosition) * tradeEvent["entryPrice"] // 1e18) - const expectedPositionSize = loanTokenSent - .sub(interestForPosition) - .mul(new BN(tradeEvent["entryPrice"])) - .div(oneEth) - .add(collateralTokenSent); - - // ignore differences in least significant digits due to rounding error - expect(Math.abs(expectedPositionSize.sub(new BN(tradeEvent["positionSize"])).toNumber()) < 100).to.be.true; - }); - - it("Test borrowFromPool sim", async () => { - const loanTokenSent = oneEth; - const newPrincipal = new BN(101).mul(oneEth); - - const collateralTokenSent = await sovryn.getRequiredCollateral( - SUSD.address, - RBTC.address, - newPrincipal, - new BN(50).mul(oneEth), - true - ); - - await RBTC.mint(sovryn.address, collateralTokenSent, { from: accounts[0] }); - - console.log("loanTokenSent", loanTokenSent.toString()); - console.log("collateralTokenSent", collateralTokenSent.toString()); - - const tx = await sovryn.borrowOrTradeFromPool( - await LinkDaiBorrowParamsId(), // loanParamsId - "0x0", // loanId - true, // isTorqueLoan, - new BN(50).mul(oneEth), // initialMargin - [ - accounts[2], // lender - accounts[1], // borrower - accounts[1], // receiver - constants.ZERO_ADDRESS, // manager - ], - [ - new BN(5).mul(oneEth), // newRate (5%) - newPrincipal, // newPrincipal - oneEth, // torqueInterest - loanTokenSent, // loanTokenSent - collateralTokenSent, // collateralTokenSent - ], - "0x", // loanDataBytes - { from: accounts[1] } - ); - // TODO: add expected and actual result comparison or else the borrow test is without validation - const sovrynAfterSUSDBalance = await SUSD.balanceOf(sovryn.address); - console.log("sovrynAfterSUSDBalance", sovrynAfterSUSDBalance.toString()); - - const sovrynAfterRBTCBalance = await RBTC.balanceOf(sovryn.address); - console.log("sovrynAfterRBTCBalance", sovrynAfterRBTCBalance.toString()); - - const decode = decodeLogs(tx.receipt.rawLogs, LoanOpeningsEvents, "Borrow"); - const borrowEvent = decode[0].args; - console.log(borrowEvent); - }); - - it("Test withdrawBorrowingFees", async () => { - const loanTokenSent = oneEth; - const newPrincipal = new BN(101).mul(oneEth); - - const collateralTokenSent = await sovryn.getRequiredCollateral( - SUSD.address, - RBTC.address, - newPrincipal, - new BN(50).mul(oneEth), - true - ); - - await RBTC.mint(sovryn.address, collateralTokenSent, { from: accounts[0] }); - - const tx = await sovryn.borrowOrTradeFromPool( - await LinkDaiBorrowParamsId(), // loanParamsId - "0x0", // loanId - true, // isTorqueLoan, - new BN(50).mul(oneEth), // initialMargin - [ - accounts[2], // lender - accounts[1], // borrower - accounts[1], // receiver - constants.ZERO_ADDRESS, // manager - ], - [ - new BN(5).mul(oneEth), // newRate (5%) - newPrincipal, // newPrincipal - oneEth, // torqueInterest - loanTokenSent, // loanTokenSent - collateralTokenSent, // collateralTokenSent - ], - "0x", // loanDataBytes - { from: accounts[1] } - ); - - await sovryn.setFeesController(accounts[0]); - - const fees = await sovryn.borrowingFeeTokensHeld(RBTC.address); - // console.log("fees: ", fees.toString()); - await sovryn.withdrawBorrowingFees(RBTC.address, accounts[1], fees); - const paid = await sovryn.borrowingFeeTokensPaid(RBTC.address); - // console.log("paid: ", paid.toString()); - - expect(paid.eq(fees)).to.be.true; - expect(await sovryn.borrowingFeeTokensHeld(RBTC.address)).to.be.a.bignumber.eq(new BN(0)); - expect(await RBTC.balanceOf(accounts[1])).to.be.a.bignumber.eq(fees); - }); - - it("should revert when withdrawing borrowing fees by no feesController", async () => { - // Prepare the test - const loanTokenSent = oneEth; - const newPrincipal = new BN(101).mul(oneEth); - - const collateralTokenSent = await sovryn.getRequiredCollateral( - SUSD.address, - RBTC.address, - newPrincipal, - new BN(50).mul(oneEth), - true - ); - - await RBTC.mint(sovryn.address, collateralTokenSent, { from: accounts[0] }); - - const tx = await sovryn.borrowOrTradeFromPool( - await LinkDaiBorrowParamsId(), // loanParamsId - "0x0", // loanId - true, // isTorqueLoan, - new BN(50).mul(oneEth), // initialMargin - [ - accounts[2], // lender - accounts[1], // borrower - accounts[1], // receiver - constants.ZERO_ADDRESS, // manager - ], - [ - new BN(5).mul(oneEth), // newRate (5%) - newPrincipal, // newPrincipal - oneEth, // torqueInterest - loanTokenSent, // loanTokenSent - collateralTokenSent, // collateralTokenSent - ], - "0x", // loanDataBytes - { from: accounts[1] } - ); - - await sovryn.setFeesController(accounts[0]); - - // Try to withdraw fees - const fees = await sovryn.borrowingFeeTokensHeld(RBTC.address); - await expectRevert(sovryn.withdrawBorrowingFees(RBTC.address, accounts[1], fees, { from: accounts[1] }), "unauthorized"); - }); - - it("should ignore withdrawAmounts bigger than balance when withdrawing borrowing fees", async () => { - // Prepare the test - const loanTokenSent = oneEth; - const newPrincipal = new BN(101).mul(oneEth); - - const collateralTokenSent = await sovryn.getRequiredCollateral( - SUSD.address, - RBTC.address, - newPrincipal, - new BN(50).mul(oneEth), - true - ); - - await RBTC.mint(sovryn.address, collateralTokenSent, { from: accounts[0] }); - - const tx = await sovryn.borrowOrTradeFromPool( - await LinkDaiBorrowParamsId(), // loanParamsId - "0x0", // loanId - true, // isTorqueLoan, - new BN(50).mul(oneEth), // initialMargin - [ - accounts[2], // lender - accounts[1], // borrower - accounts[1], // receiver - constants.ZERO_ADDRESS, // manager - ], - [ - new BN(5).mul(oneEth), // newRate (5%) - newPrincipal, // newPrincipal - oneEth, // torqueInterest - loanTokenSent, // loanTokenSent - collateralTokenSent, // collateralTokenSent - ], - "0x", // loanDataBytes - { from: accounts[1] } - ); - - await sovryn.setFeesController(accounts[0]); - - // Withdraw fees and verify - const fees = await sovryn.borrowingFeeTokensHeld(RBTC.address); - await sovryn.withdrawBorrowingFees(RBTC.address, accounts[1], fees.mul(new BN(2))); - const paid = await sovryn.borrowingFeeTokensPaid(RBTC.address); - - expect(paid.eq(fees)).to.be.true; - expect(await sovryn.borrowingFeeTokensHeld(RBTC.address)).to.be.a.bignumber.eq(new BN(0)); - expect(await RBTC.balanceOf(accounts[1])).to.be.a.bignumber.eq(fees); - }); - - it("should return false when withdrawing amount 0 of borrowing fees", async () => { - // Prepare the test - const loanTokenSent = oneEth; - const newPrincipal = new BN(101).mul(oneEth); - - const collateralTokenSent = await sovryn.getRequiredCollateral( - SUSD.address, - RBTC.address, - newPrincipal, - new BN(50).mul(oneEth), - true - ); - - await RBTC.mint(sovryn.address, collateralTokenSent, { from: accounts[0] }); - - const tx = await sovryn.borrowOrTradeFromPool( - await LinkDaiBorrowParamsId(), // loanParamsId - "0x0", // loanId - true, // isTorqueLoan, - new BN(50).mul(oneEth), // initialMargin - [ - accounts[2], // lender - accounts[1], // borrower - accounts[1], // receiver - constants.ZERO_ADDRESS, // manager - ], - [ - new BN(5).mul(oneEth), // newRate (5%) - newPrincipal, // newPrincipal - oneEth, // torqueInterest - loanTokenSent, // loanTokenSent - collateralTokenSent, // collateralTokenSent - ], - "0x", // loanDataBytes - { from: accounts[1] } - ); - - await sovryn.setFeesController(accounts[0]); - - // Withdraw fees and verify - const fees = await sovryn.borrowingFeeTokensHeld(RBTC.address); - let result = await sovryn.withdrawBorrowingFees.call(RBTC.address, accounts[1], new BN(0)); - expect(result).to.be.false; - }); - - it("should revert when sending Ethers w/o loanDataBytes", async () => { - // Prepare the test - const loanTokenSent = oneEth; - const newPrincipal = new BN(101).mul(oneEth); - const collateralTokenSent = await sovryn.getRequiredCollateral( - SUSD.address, - RBTC.address, - newPrincipal, - new BN(50).mul(oneEth), - true - ); - - await expectRevert( - sovryn.borrowOrTradeFromPool( - await LinkDaiBorrowParamsId(), // loanParamsId - "0x0", // loanId - true, // isTorqueLoan, - new BN(50).mul(oneEth), // initialMargin - [ - accounts[2], // lender - accounts[1], // borrower - accounts[1], // receiver - constants.ZERO_ADDRESS, // manager - ], - [ - new BN(5).mul(oneEth), // newRate (5%) - newPrincipal, // newPrincipal - oneEth, // torqueInterest - loanTokenSent, // loanTokenSent - collateralTokenSent, // collateralTokenSent - ], - "0x", // loanDataBytes - { from: accounts[1], value: 1 } - ), - "loanDataBytes required with ether" - ); - }); - - /// @dev Loan pool accounts are accounts[1] and accounts[2], according to fixture init - it("should revert when called by a non-loan pool account", async () => { - // Prepare the test - const loanTokenSent = oneEth; - const newPrincipal = new BN(101).mul(oneEth); - const collateralTokenSent = await sovryn.getRequiredCollateral( - SUSD.address, - RBTC.address, - newPrincipal, - new BN(50).mul(oneEth), - true - ); - - await expectRevert( - sovryn.borrowOrTradeFromPool( - await LinkDaiBorrowParamsId(), // loanParamsId - "0x0", // loanId - true, // isTorqueLoan, - new BN(50).mul(oneEth), // initialMargin - [ - accounts[2], // lender - accounts[1], // borrower - accounts[1], // receiver - constants.ZERO_ADDRESS, // manager - ], - [ - new BN(5).mul(oneEth), // newRate (5%) - newPrincipal, // newPrincipal - oneEth, // torqueInterest - loanTokenSent, // loanTokenSent - collateralTokenSent, // collateralTokenSent - ], - "0x", // loanDataBytes - { from: accounts[3] } - ), - "not authorized" - ); - }); - - it("should revert when called w/ wrong loanParamsId parameter", async () => { - // Prepare the test - const loanTokenSent = oneEth; - const newPrincipal = new BN(101).mul(oneEth); - const collateralTokenSent = await sovryn.getRequiredCollateral( - SUSD.address, - RBTC.address, - newPrincipal, - new BN(50).mul(oneEth), - true - ); - - await expectRevert( - sovryn.borrowOrTradeFromPool( - "0x0", // loanParamsId - "0x0", // loanId - true, // isTorqueLoan, - new BN(50).mul(oneEth), // initialMargin - [ - accounts[2], // lender - accounts[1], // borrower - accounts[1], // receiver - constants.ZERO_ADDRESS, // manager - ], - [ - new BN(5).mul(oneEth), // newRate (5%) - newPrincipal, // newPrincipal - oneEth, // torqueInterest - loanTokenSent, // loanTokenSent - collateralTokenSent, // collateralTokenSent - ], - "0x", // loanDataBytes - { from: accounts[1] } - ), - "loanParams not exists" - ); - }); - - /// @dev To force a 0 required collateral, newPrincipal is set to 0 - it("should revert when required collateral is 0", async () => { - // Prepare the test - const loanTokenSent = oneEth; - const newPrincipal = new BN(101).mul(oneEth); - const collateralTokenSent = await sovryn.getRequiredCollateral( - SUSD.address, - RBTC.address, - newPrincipal, - new BN(50).mul(oneEth), - true - ); - - await expectRevert( - sovryn.borrowOrTradeFromPool( - await LinkDaiBorrowParamsId(), // loanParamsId - "0x0", // loanId - true, // isTorqueLoan, - new BN(50).mul(oneEth), // initialMargin - [ - accounts[2], // lender - accounts[1], // borrower - accounts[1], // receiver - constants.ZERO_ADDRESS, // manager - ], - [ - new BN(5).mul(oneEth), // newRate (5%) - new BN(0), // newPrincipal - oneEth, // torqueInterest - loanTokenSent, // loanTokenSent - collateralTokenSent, // collateralTokenSent - ], - "0x", // loanDataBytes - { from: accounts[1] } - ), - "collateral is 0" - ); - }); - }); + it("Test marginTradeFromPool sim", async () => { + const loanTokenSent = hunEth; + const sovrynSwap = await sovryn.sovrynSwapContractRegistryAddress(); + // console.log('sovryn seap contract registry address is ',sovrynSwap) + // addressOf = sovrynSwap.addressOf(sovrynSwap.address) + console.log("returned address is ", sovrynSwap); + const swapsI = await sovryn.swapsImpl(); + console.log("swaps impl is ", swapsI); + const collateralTokenSent = await sovryn.getRequiredCollateral( + SUSD.address, + RBTC.address, + loanTokenSent, + new BN(50).mul(oneEth), + false + ); + console.log("required collateral:", collateralTokenSent.div(oneEth).toString()); + + await RBTC.mint(sovryn.address, collateralTokenSent, { from: accounts[0] }); + + console.log("loanTokenSent", loanTokenSent.toString()); + console.log("collateralTokenSent", collateralTokenSent.toString()); + + const tx = await sovryn.borrowOrTradeFromPool( + await LinkDaiMarginParamsId(), // loanParamsId + "0x0", // loanId + false, // isTorqueLoan, + hunEth, // initialMargin + [ + accounts[2], // lender + accounts[1], // borrower + accounts[1], // receiver + constants.ZERO_ADDRESS, // manager + ], + [ + new BN(5).mul(oneEth), // newRate (5%) + loanTokenSent, // newPrincipal + 0, // torqueInterest + loanTokenSent, // loanTokenSent + collateralTokenSent, // collateralTokenSent + ], + "0x", // loanDataBytes + { from: accounts[1] } + ); + + const sovrynAfterSUSDBalance = await SUSD.balanceOf(sovryn.address); + console.log("sovrynAfterSUSDBalance", sovrynAfterSUSDBalance.toString()); + + const sovrynAfterRBTCBalance = await RBTC.balanceOf(sovryn.address); + console.log("sovrynAfterRBTCBalance", sovrynAfterRBTCBalance.toString()); + + const decode = decodeLogs(tx.receipt.rawLogs, LoanOpeningsEvents, "Trade"); + const tradeEvent = decode[0].args; + + const interestForPosition = loanTokenSent + .mul(new BN(5).mul(oneEth)) + .div(hunEth) + .div(new BN(365)) + .mul(new BN(2419200)) + .div(new BN(86400)); + console.log("interestForPosition", interestForPosition.toString()); + + // expectedPositionSize = collateralTokenSent + ((loanTokenSent - interestForPosition) * tradeEvent["entryPrice"] // 1e18) + const expectedPositionSize = loanTokenSent + .sub(interestForPosition) + .mul(new BN(tradeEvent["entryPrice"])) + .div(oneEth) + .add(collateralTokenSent); + + // ignore differences in least significant digits due to rounding error + expect( + Math.abs(expectedPositionSize.sub(new BN(tradeEvent["positionSize"])).toNumber()) < + 100 + ).to.be.true; + }); + + it("Test borrowFromPool sim", async () => { + const loanTokenSent = oneEth; + const newPrincipal = new BN(101).mul(oneEth); + + const collateralTokenSent = await sovryn.getRequiredCollateral( + SUSD.address, + RBTC.address, + newPrincipal, + new BN(50).mul(oneEth), + true + ); + + await RBTC.mint(sovryn.address, collateralTokenSent, { from: accounts[0] }); + + console.log("loanTokenSent", loanTokenSent.toString()); + console.log("collateralTokenSent", collateralTokenSent.toString()); + + const tx = await sovryn.borrowOrTradeFromPool( + await LinkDaiBorrowParamsId(), // loanParamsId + "0x0", // loanId + true, // isTorqueLoan, + new BN(50).mul(oneEth), // initialMargin + [ + accounts[2], // lender + accounts[1], // borrower + accounts[1], // receiver + constants.ZERO_ADDRESS, // manager + ], + [ + new BN(5).mul(oneEth), // newRate (5%) + newPrincipal, // newPrincipal + oneEth, // torqueInterest + loanTokenSent, // loanTokenSent + collateralTokenSent, // collateralTokenSent + ], + "0x", // loanDataBytes + { from: accounts[1] } + ); + // TODO: add expected and actual result comparison or else the borrow test is without validation + const sovrynAfterSUSDBalance = await SUSD.balanceOf(sovryn.address); + console.log("sovrynAfterSUSDBalance", sovrynAfterSUSDBalance.toString()); + + const sovrynAfterRBTCBalance = await RBTC.balanceOf(sovryn.address); + console.log("sovrynAfterRBTCBalance", sovrynAfterRBTCBalance.toString()); + + const decode = decodeLogs(tx.receipt.rawLogs, LoanOpeningsEvents, "Borrow"); + const borrowEvent = decode[0].args; + console.log(borrowEvent); + }); + + it("Test withdrawBorrowingFees", async () => { + const loanTokenSent = oneEth; + const newPrincipal = new BN(101).mul(oneEth); + + const collateralTokenSent = await sovryn.getRequiredCollateral( + SUSD.address, + RBTC.address, + newPrincipal, + new BN(50).mul(oneEth), + true + ); + + await RBTC.mint(sovryn.address, collateralTokenSent, { from: accounts[0] }); + + const tx = await sovryn.borrowOrTradeFromPool( + await LinkDaiBorrowParamsId(), // loanParamsId + "0x0", // loanId + true, // isTorqueLoan, + new BN(50).mul(oneEth), // initialMargin + [ + accounts[2], // lender + accounts[1], // borrower + accounts[1], // receiver + constants.ZERO_ADDRESS, // manager + ], + [ + new BN(5).mul(oneEth), // newRate (5%) + newPrincipal, // newPrincipal + oneEth, // torqueInterest + loanTokenSent, // loanTokenSent + collateralTokenSent, // collateralTokenSent + ], + "0x", // loanDataBytes + { from: accounts[1] } + ); + + await sovryn.setFeesController(accounts[0]); + + const fees = await sovryn.borrowingFeeTokensHeld(RBTC.address); + // console.log("fees: ", fees.toString()); + await sovryn.withdrawBorrowingFees(RBTC.address, accounts[1], fees); + const paid = await sovryn.borrowingFeeTokensPaid(RBTC.address); + // console.log("paid: ", paid.toString()); + + expect(paid.eq(fees)).to.be.true; + expect(await sovryn.borrowingFeeTokensHeld(RBTC.address)).to.be.a.bignumber.eq( + new BN(0) + ); + expect(await RBTC.balanceOf(accounts[1])).to.be.a.bignumber.eq(fees); + }); + + it("should revert when withdrawing borrowing fees by no feesController", async () => { + // Prepare the test + const loanTokenSent = oneEth; + const newPrincipal = new BN(101).mul(oneEth); + + const collateralTokenSent = await sovryn.getRequiredCollateral( + SUSD.address, + RBTC.address, + newPrincipal, + new BN(50).mul(oneEth), + true + ); + + await RBTC.mint(sovryn.address, collateralTokenSent, { from: accounts[0] }); + + const tx = await sovryn.borrowOrTradeFromPool( + await LinkDaiBorrowParamsId(), // loanParamsId + "0x0", // loanId + true, // isTorqueLoan, + new BN(50).mul(oneEth), // initialMargin + [ + accounts[2], // lender + accounts[1], // borrower + accounts[1], // receiver + constants.ZERO_ADDRESS, // manager + ], + [ + new BN(5).mul(oneEth), // newRate (5%) + newPrincipal, // newPrincipal + oneEth, // torqueInterest + loanTokenSent, // loanTokenSent + collateralTokenSent, // collateralTokenSent + ], + "0x", // loanDataBytes + { from: accounts[1] } + ); + + await sovryn.setFeesController(accounts[0]); + + // Try to withdraw fees + const fees = await sovryn.borrowingFeeTokensHeld(RBTC.address); + await expectRevert( + sovryn.withdrawBorrowingFees(RBTC.address, accounts[1], fees, { + from: accounts[1], + }), + "unauthorized" + ); + }); + + it("should ignore withdrawAmounts bigger than balance when withdrawing borrowing fees", async () => { + // Prepare the test + const loanTokenSent = oneEth; + const newPrincipal = new BN(101).mul(oneEth); + + const collateralTokenSent = await sovryn.getRequiredCollateral( + SUSD.address, + RBTC.address, + newPrincipal, + new BN(50).mul(oneEth), + true + ); + + await RBTC.mint(sovryn.address, collateralTokenSent, { from: accounts[0] }); + + const tx = await sovryn.borrowOrTradeFromPool( + await LinkDaiBorrowParamsId(), // loanParamsId + "0x0", // loanId + true, // isTorqueLoan, + new BN(50).mul(oneEth), // initialMargin + [ + accounts[2], // lender + accounts[1], // borrower + accounts[1], // receiver + constants.ZERO_ADDRESS, // manager + ], + [ + new BN(5).mul(oneEth), // newRate (5%) + newPrincipal, // newPrincipal + oneEth, // torqueInterest + loanTokenSent, // loanTokenSent + collateralTokenSent, // collateralTokenSent + ], + "0x", // loanDataBytes + { from: accounts[1] } + ); + + await sovryn.setFeesController(accounts[0]); + + // Withdraw fees and verify + const fees = await sovryn.borrowingFeeTokensHeld(RBTC.address); + await sovryn.withdrawBorrowingFees(RBTC.address, accounts[1], fees.mul(new BN(2))); + const paid = await sovryn.borrowingFeeTokensPaid(RBTC.address); + + expect(paid.eq(fees)).to.be.true; + expect(await sovryn.borrowingFeeTokensHeld(RBTC.address)).to.be.a.bignumber.eq( + new BN(0) + ); + expect(await RBTC.balanceOf(accounts[1])).to.be.a.bignumber.eq(fees); + }); + + it("should return false when withdrawing amount 0 of borrowing fees", async () => { + // Prepare the test + const loanTokenSent = oneEth; + const newPrincipal = new BN(101).mul(oneEth); + + const collateralTokenSent = await sovryn.getRequiredCollateral( + SUSD.address, + RBTC.address, + newPrincipal, + new BN(50).mul(oneEth), + true + ); + + await RBTC.mint(sovryn.address, collateralTokenSent, { from: accounts[0] }); + + const tx = await sovryn.borrowOrTradeFromPool( + await LinkDaiBorrowParamsId(), // loanParamsId + "0x0", // loanId + true, // isTorqueLoan, + new BN(50).mul(oneEth), // initialMargin + [ + accounts[2], // lender + accounts[1], // borrower + accounts[1], // receiver + constants.ZERO_ADDRESS, // manager + ], + [ + new BN(5).mul(oneEth), // newRate (5%) + newPrincipal, // newPrincipal + oneEth, // torqueInterest + loanTokenSent, // loanTokenSent + collateralTokenSent, // collateralTokenSent + ], + "0x", // loanDataBytes + { from: accounts[1] } + ); + + await sovryn.setFeesController(accounts[0]); + + // Withdraw fees and verify + const fees = await sovryn.borrowingFeeTokensHeld(RBTC.address); + let result = await sovryn.withdrawBorrowingFees.call( + RBTC.address, + accounts[1], + new BN(0) + ); + expect(result).to.be.false; + }); + + it("should revert when sending Ethers w/o loanDataBytes", async () => { + // Prepare the test + const loanTokenSent = oneEth; + const newPrincipal = new BN(101).mul(oneEth); + const collateralTokenSent = await sovryn.getRequiredCollateral( + SUSD.address, + RBTC.address, + newPrincipal, + new BN(50).mul(oneEth), + true + ); + + await expectRevert( + sovryn.borrowOrTradeFromPool( + await LinkDaiBorrowParamsId(), // loanParamsId + "0x0", // loanId + true, // isTorqueLoan, + new BN(50).mul(oneEth), // initialMargin + [ + accounts[2], // lender + accounts[1], // borrower + accounts[1], // receiver + constants.ZERO_ADDRESS, // manager + ], + [ + new BN(5).mul(oneEth), // newRate (5%) + newPrincipal, // newPrincipal + oneEth, // torqueInterest + loanTokenSent, // loanTokenSent + collateralTokenSent, // collateralTokenSent + ], + "0x", // loanDataBytes + { from: accounts[1], value: 1 } + ), + "loanDataBytes required with ether" + ); + }); + + /// @dev Loan pool accounts are accounts[1] and accounts[2], according to fixture init + it("should revert when called by a non-loan pool account", async () => { + // Prepare the test + const loanTokenSent = oneEth; + const newPrincipal = new BN(101).mul(oneEth); + const collateralTokenSent = await sovryn.getRequiredCollateral( + SUSD.address, + RBTC.address, + newPrincipal, + new BN(50).mul(oneEth), + true + ); + + await expectRevert( + sovryn.borrowOrTradeFromPool( + await LinkDaiBorrowParamsId(), // loanParamsId + "0x0", // loanId + true, // isTorqueLoan, + new BN(50).mul(oneEth), // initialMargin + [ + accounts[2], // lender + accounts[1], // borrower + accounts[1], // receiver + constants.ZERO_ADDRESS, // manager + ], + [ + new BN(5).mul(oneEth), // newRate (5%) + newPrincipal, // newPrincipal + oneEth, // torqueInterest + loanTokenSent, // loanTokenSent + collateralTokenSent, // collateralTokenSent + ], + "0x", // loanDataBytes + { from: accounts[3] } + ), + "not authorized" + ); + }); + + it("should revert when called w/ wrong loanParamsId parameter", async () => { + // Prepare the test + const loanTokenSent = oneEth; + const newPrincipal = new BN(101).mul(oneEth); + const collateralTokenSent = await sovryn.getRequiredCollateral( + SUSD.address, + RBTC.address, + newPrincipal, + new BN(50).mul(oneEth), + true + ); + + await expectRevert( + sovryn.borrowOrTradeFromPool( + "0x0", // loanParamsId + "0x0", // loanId + true, // isTorqueLoan, + new BN(50).mul(oneEth), // initialMargin + [ + accounts[2], // lender + accounts[1], // borrower + accounts[1], // receiver + constants.ZERO_ADDRESS, // manager + ], + [ + new BN(5).mul(oneEth), // newRate (5%) + newPrincipal, // newPrincipal + oneEth, // torqueInterest + loanTokenSent, // loanTokenSent + collateralTokenSent, // collateralTokenSent + ], + "0x", // loanDataBytes + { from: accounts[1] } + ), + "loanParams not exists" + ); + }); + + /// @dev To force a 0 required collateral, newPrincipal is set to 0 + it("should revert when required collateral is 0", async () => { + // Prepare the test + const loanTokenSent = oneEth; + const newPrincipal = new BN(101).mul(oneEth); + const collateralTokenSent = await sovryn.getRequiredCollateral( + SUSD.address, + RBTC.address, + newPrincipal, + new BN(50).mul(oneEth), + true + ); + + await expectRevert( + sovryn.borrowOrTradeFromPool( + await LinkDaiBorrowParamsId(), // loanParamsId + "0x0", // loanId + true, // isTorqueLoan, + new BN(50).mul(oneEth), // initialMargin + [ + accounts[2], // lender + accounts[1], // borrower + accounts[1], // receiver + constants.ZERO_ADDRESS, // manager + ], + [ + new BN(5).mul(oneEth), // newRate (5%) + new BN(0), // newPrincipal + oneEth, // torqueInterest + loanTokenSent, // loanTokenSent + collateralTokenSent, // collateralTokenSent + ], + "0x", // loanDataBytes + { from: accounts[1] } + ), + "collateral is 0" + ); + }); + }); }); diff --git a/tests/other/ModifyNameAndSymbol.test.js b/tests/other/ModifyNameAndSymbol.test.js index 0100dd98d..00d637316 100644 --- a/tests/other/ModifyNameAndSymbol.test.js +++ b/tests/other/ModifyNameAndSymbol.test.js @@ -7,32 +7,32 @@ const { expect } = require("chai"); const LoanTokenLogicStandard = artifacts.require("LoanTokenLogicStandard"); const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getPriceFeeds, - getSovryn, - getLoanTokenLogic, - getLoanToken, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getPriceFeeds, + getSovryn, + getLoanTokenLogic, + getLoanToken, } = require("../Utils/initializer.js"); contract("ModifyNameAndSymbol", (accounts) => { - let sovryn, SUSD, WRBTC, RBTC, BZRX, priceFeeds, loanToken; + let sovryn, SUSD, WRBTC, RBTC, BZRX, priceFeeds, loanToken; - beforeEach(async () => { - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + beforeEach(async () => { + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - loanToken = await getLoanToken(accounts[0], sovryn, WRBTC, SUSD); - }); + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + loanToken = await getLoanToken(accounts[0], sovryn, WRBTC, SUSD); + }); - describe("Modifying name and symbol", () => { - /* + describe("Modifying name and symbol", () => { + /* it("Test modifying name and symbol", async () => { const name = "TestName", symbol = "TSB"; @@ -43,5 +43,5 @@ contract("ModifyNameAndSymbol", (accounts) => { expect((await localLoanToken.name()) == name).to.be.true; expect((await localLoanToken.symbol()) == symbol).to.be.true; });*/ - }); + }); }); diff --git a/tests/other/OracleIntegration.test.js b/tests/other/OracleIntegration.test.js index db8b56a64..7b40b1479 100644 --- a/tests/other/OracleIntegration.test.js +++ b/tests/other/OracleIntegration.test.js @@ -23,92 +23,114 @@ const PriceFeedsMoCMockup = artifacts.require("PriceFeedsMoCMockup"); const PriceFeedRSKOracleMockup = artifacts.require("PriceFeedRSKOracleMockup"); const SwapsImplSovrynSwap = artifacts.require("SwapsImplSovrynSwap"); -const { getSUSD, getRBTC, getWRBTC, getBZRX, getSovryn, getPriceFeeds } = require("../Utils/initializer.js"); +const { + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getSovryn, + getPriceFeeds, +} = require("../Utils/initializer.js"); contract("OracleIntegration", (accounts) => { - let sovryn, SUSD, WRBTC, RBTC, BZRX, priceFeeds, swapsImpl; - - async function deploymentAndInitFixture(_wallets, _provider) { - // Deploying sovrynProtocol w/ generic function from initializer.js - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - swapsImpl = await SwapsImplSovrynSwap.new(); - await sovryn.setSwapsImplContract(swapsImpl.address); - } - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - const set_oracle = async (price_feed_rsk_mockup, oracle_address = sovryn.address) => { - const price_feeds_moc = await PriceFeedsMoC.new(oracle_address, price_feed_rsk_mockup); - const price_feeds = await PriceFeeds.new(WRBTC.address, BZRX.address, SUSD.address); - - await price_feeds.setPriceFeed([BZRX.address, WRBTC.address], [price_feeds_moc.address, price_feeds_moc.address]); - - await sovryn.setPriceFeedContract( - price_feeds.address // priceFeeds - ); - - return [price_feeds, price_feeds_moc]; - }; - - const price_feed_moc_mockup = async () => { - const price_feeds_moc_mockup = await PriceFeedsMoCMockup.new(); - await price_feeds_moc_mockup.setHas(true); - await price_feeds_moc_mockup.setValue(new BN(10).pow(new BN(22))); - return price_feeds_moc_mockup; - }; - const price_feed_rsk_mockup = async () => { - const price_feed_rsk_mockup = await PriceFeedRSKOracleMockup.new(); - await price_feed_rsk_mockup.setHas(true); - await price_feed_rsk_mockup.setValue(new BN(10).pow(new BN(20))); - return price_feed_rsk_mockup; - }; - - describe("OracleIntegration Tests", () => { - it("Test moc oracle integration", async () => { - const [price_feeds, price_feeds_moc] = await set_oracle( - ( - await price_feed_rsk_mockup() - ).address, - ( - await price_feed_moc_mockup() - ).address - ); - - let res = await price_feeds.queryPrecision(BZRX.address, WRBTC.address); - expect(res.eq(new BN(10).pow(new BN(18)))).to.be.true; - - res = await price_feeds_moc.latestAnswer(); - expect(res.eq(new BN(10).pow(new BN(22)))).to.be.true; - }); - - it("Test set moc oracle address", async () => { - const [, price_feeds_moc] = await set_oracle((await price_feed_rsk_mockup()).address); - expectEvent(await price_feeds_moc.setMoCOracleAddress(BZRX.address), "SetMoCOracleAddress", { - mocOracleAddress: BZRX.address, - changerAddress: accounts[0], - }); - expect((await price_feeds_moc.mocOracleAddress()) == BZRX.address).to.be.true; - }); - - it("Test set moc oracle address unauthorized user should fail", async () => { - const [, price_feeds_moc] = await set_oracle((await price_feed_rsk_mockup()).address); - await expectRevert(price_feeds_moc.setMoCOracleAddress(BZRX.address, { from: accounts[1] }), "unauthorized"); - }); - - it("Test get price from rsk when hasValue false", async () => { - const price_feed_mockup = await price_feed_moc_mockup(); - await price_feed_mockup.setHas(false); - const [, price_feeds_moc] = await set_oracle((await price_feed_rsk_mockup()).address, price_feed_mockup.address); - const res = await price_feeds_moc.latestAnswer(); - expect(res.eq(new BN(10).pow(new BN(20)))).to.be.true; - }); - }); + let sovryn, SUSD, WRBTC, RBTC, BZRX, priceFeeds, swapsImpl; + + async function deploymentAndInitFixture(_wallets, _provider) { + // Deploying sovrynProtocol w/ generic function from initializer.js + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + swapsImpl = await SwapsImplSovrynSwap.new(); + await sovryn.setSwapsImplContract(swapsImpl.address); + } + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + const set_oracle = async (price_feed_rsk_mockup, oracle_address = sovryn.address) => { + const price_feeds_moc = await PriceFeedsMoC.new(oracle_address, price_feed_rsk_mockup); + const price_feeds = await PriceFeeds.new(WRBTC.address, BZRX.address, SUSD.address); + + await price_feeds.setPriceFeed( + [BZRX.address, WRBTC.address], + [price_feeds_moc.address, price_feeds_moc.address] + ); + + await sovryn.setPriceFeedContract( + price_feeds.address // priceFeeds + ); + + return [price_feeds, price_feeds_moc]; + }; + + const price_feed_moc_mockup = async () => { + const price_feeds_moc_mockup = await PriceFeedsMoCMockup.new(); + await price_feeds_moc_mockup.setHas(true); + await price_feeds_moc_mockup.setValue(new BN(10).pow(new BN(22))); + return price_feeds_moc_mockup; + }; + const price_feed_rsk_mockup = async () => { + const price_feed_rsk_mockup = await PriceFeedRSKOracleMockup.new(); + await price_feed_rsk_mockup.setHas(true); + await price_feed_rsk_mockup.setValue(new BN(10).pow(new BN(20))); + return price_feed_rsk_mockup; + }; + + describe("OracleIntegration Tests", () => { + it("Test moc oracle integration", async () => { + const [price_feeds, price_feeds_moc] = await set_oracle( + ( + await price_feed_rsk_mockup() + ).address, + ( + await price_feed_moc_mockup() + ).address + ); + + let res = await price_feeds.queryPrecision(BZRX.address, WRBTC.address); + expect(res.eq(new BN(10).pow(new BN(18)))).to.be.true; + + res = await price_feeds_moc.latestAnswer(); + expect(res.eq(new BN(10).pow(new BN(22)))).to.be.true; + }); + + it("Test set moc oracle address", async () => { + const [, price_feeds_moc] = await set_oracle((await price_feed_rsk_mockup()).address); + expectEvent( + await price_feeds_moc.setMoCOracleAddress(BZRX.address), + "SetMoCOracleAddress", + { + mocOracleAddress: BZRX.address, + changerAddress: accounts[0], + } + ); + expect((await price_feeds_moc.mocOracleAddress()) == BZRX.address).to.be.true; + }); + + it("Test set moc oracle address unauthorized user should fail", async () => { + const [, price_feeds_moc] = await set_oracle((await price_feed_rsk_mockup()).address); + await expectRevert( + price_feeds_moc.setMoCOracleAddress(BZRX.address, { from: accounts[1] }), + "unauthorized" + ); + }); + + it("Test get price from rsk when hasValue false", async () => { + const price_feed_mockup = await price_feed_moc_mockup(); + await price_feed_mockup.setHas(false); + const [, price_feeds_moc] = await set_oracle( + ( + await price_feed_rsk_mockup() + ).address, + price_feed_mockup.address + ); + const res = await price_feeds_moc.latestAnswer(); + expect(res.eq(new BN(10).pow(new BN(20)))).to.be.true; + }); + }); }); diff --git a/tests/other/Protocol.test.js b/tests/other/Protocol.test.js index 6493d33d5..205736fe9 100644 --- a/tests/other/Protocol.test.js +++ b/tests/other/Protocol.test.js @@ -9,7 +9,14 @@ const { constants, expectEvent, expectRevert } = require("@openzeppelin/test-helpers"); const { ZERO_ADDRESS } = require("@openzeppelin/test-helpers/src/constants"); const { expect } = require("chai"); -const { getSUSD, getRBTC, getWRBTC, getBZRX, getSovryn, getPriceFeeds } = require("../Utils/initializer.js"); +const { + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getSovryn, + getPriceFeeds, +} = require("../Utils/initializer.js"); const Affiliates = artifacts.require("Affiliates"); const LoanSettings = artifacts.require("LoanSettings"); @@ -22,148 +29,154 @@ const LoanClosingsRollover = artifacts.require("LoanClosingsRollover"); const LoanClosingsWith = artifacts.require("LoanClosingsWith"); contract("Protocol", (accounts) => { - let sovryn, SUSD, WRBTC, RBTC, BZRX, priceFeeds; - const ONE_ADDRESS = "0x0000000000000000000000000000000000000001"; - - before(async () => { - // Deploying sovrynProtocol w/ generic function from initializer.js - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - }); - - describe("Protocol Tests", () => { - it("Test targetSetup", async () => { - const sig1 = "testFunction1(address,uint256,bytes)"; - const sig2 = "testFunction2(address[],uint256[],bytes[])"; - - const sigs = [sig1, sig2]; - let targets = [ONE_ADDRESS, ONE_ADDRESS]; - await sovryn.setTargets(sigs, targets); - - expect((await sovryn.getTarget(sig1)) == ONE_ADDRESS).to.be.true; - expect((await sovryn.getTarget(sig2)) == ONE_ADDRESS).to.be.true; - - targets = [constants.ZERO_ADDRESS, constants.ZERO_ADDRESS]; - await sovryn.setTargets(sigs, targets); - - expect((await sovryn.getTarget(sig1)) == constants.ZERO_ADDRESS).to.be.true; - expect((await sovryn.getTarget(sig2)) == constants.ZERO_ADDRESS).to.be.true; - }); - - it("should revert for count mismatch while setting targets", async () => { - const sig1 = "testFunction1(address,uint256,bytes)"; - const sig2 = "testFunction2(address[],uint256[],bytes[])"; - const sigs = [sig1, sig2]; - let targets = [ONE_ADDRESS, ONE_ADDRESS, ONE_ADDRESS]; - - await expectRevert(sovryn.setTargets(sigs, targets), "count mismatch"); - }); - - it("Test replaceContract", async () => { - const sig = "setupLoanParams((bytes32,bool,address,address,address,uint256,uint256,uint256)[])"; - const loanSettings = await LoanSettings.new(); - - await sovryn.setTargets([sig], [constants.ZERO_ADDRESS]); - expect((await sovryn.getTarget(sig)) == constants.ZERO_ADDRESS).to.be.true; - - await sovryn.replaceContract(loanSettings.address); - expect((await sovryn.getTarget(sig)) == loanSettings.address).to.be.true; - }); - - it("Test receiveEther", async () => { - expect((await web3.eth.getBalance(sovryn.address)) == 0).to.be.true; - // gasleft() should be < 2300 in the Protocol.sol proxy contract, 21000 is the min gas for sending value - await web3.eth.sendTransaction({ from: accounts[0].toString(), to: sovryn.address, value: 10000, gas: 22000 }); - expect((await web3.eth.getBalance(sovryn.address)) == 10000).to.be.true; - }); - }); - - describe("Events - replaceContract", () => { - it("should fail replaceContract w/ bad address", async () => { - await expectRevert(sovryn.replaceContract(sovryn.address), "setup failed"); - }); - - it("Test replaceContract - Affiliates", async () => { - const selector = "getUserNotFirstTradeFlag(address)"; - let oldAffiliatesAddr = await sovryn.getTarget(selector); - let newAffiliatesAddr = await Affiliates.new(); - let tx = await sovryn.replaceContract(newAffiliatesAddr.address); - expectEvent(tx, "ProtocolModuleContractReplaced", { - prevModuleContractAddress: oldAffiliatesAddr, - newModuleContractAddress: newAffiliatesAddr.address, - module: ethers.utils.formatBytes32String("Affiliates"), - }); - }); - - it("Test replaceContract - LoanClosingsLiquidation", async () => { - const selector = "liquidate(bytes32,address,uint256)"; - let oldLoanClosingsLiquidationAddr = await sovryn.getTarget(selector); - let newLoanClosingsLiquidationAddr = await LoanClosingsLiquidation.new(); - let tx = await sovryn.replaceContract(newLoanClosingsLiquidationAddr.address); - expectEvent(tx, "ProtocolModuleContractReplaced", { - prevModuleContractAddress: oldLoanClosingsLiquidationAddr, - newModuleContractAddress: newLoanClosingsLiquidationAddr.address, - module: ethers.utils.formatBytes32String("LoanClosingsLiquidation"), - }); - }); - - it("Test replaceContract - LoanClosingsRollover", async () => { - const selector = "rollover(bytes32,bytes)"; - let oldLoanClosingsRolloverAddr = await sovryn.getTarget(selector); - let newLoanClosingsRolloverAddr = await LoanClosingsRollover.new(); - let tx = await sovryn.replaceContract(newLoanClosingsRolloverAddr.address); - expectEvent(tx, "ProtocolModuleContractReplaced", { - prevModuleContractAddress: oldLoanClosingsRolloverAddr, - newModuleContractAddress: newLoanClosingsRolloverAddr.address, - module: ethers.utils.formatBytes32String("LoanClosingsRollover"), - }); - }); - - it("Test replaceContract - LoanClosingsWith", async () => { - let newLoanClosingsWithAddr = await LoanClosingsWith.new(); - let tx = await sovryn.replaceContract(newLoanClosingsWithAddr.address); - expectEvent(tx, "ProtocolModuleContractReplaced"); - }); - - it("Test replaceContract - LoanMaintenance", async () => { - let newLoanMaintenanceAddr = await LoanMaintenance.new(); - let tx = await sovryn.replaceContract(newLoanMaintenanceAddr.address); - expectEvent(tx, "ProtocolModuleContractReplaced"); - }); - - it("Test replaceContract - LoanOpenings", async () => { - let newLoanOpeningsAddr = await LoanOpenings.new(); - let tx = await sovryn.replaceContract(newLoanOpeningsAddr.address); - expectEvent(tx, "ProtocolModuleContractReplaced"); - }); - - it("Test replaceContract - LoanSettings", async () => { - let newLoanSettingsAddr = await LoanSettings.new(); - let tx = await sovryn.replaceContract(newLoanSettingsAddr.address); - expectEvent(tx, "ProtocolModuleContractReplaced"); - }); - - it("Test replaceContract - ProtocolSettings", async () => { - const selector = "setSovrynProtocolAddress(address)"; - let oldProtocolSettingsAddr = await sovryn.getTarget(selector); - let newProtocolSettingsAddr = await ProtocolSettings.new(); - let tx = await sovryn.replaceContract(newProtocolSettingsAddr.address); - expectEvent(tx, "ProtocolModuleContractReplaced", { - prevModuleContractAddress: oldProtocolSettingsAddr, - newModuleContractAddress: newProtocolSettingsAddr.address, - module: ethers.utils.formatBytes32String("ProtocolSettings"), - }); - }); - - it("Test replaceContract - SwapsExternal", async () => { - let newSwapsExternalAddr = await SwapsExternal.new(); - let tx = await sovryn.replaceContract(newSwapsExternalAddr.address); - expectEvent(tx, "ProtocolModuleContractReplaced"); - }); - }); + let sovryn, SUSD, WRBTC, RBTC, BZRX, priceFeeds; + const ONE_ADDRESS = "0x0000000000000000000000000000000000000001"; + + before(async () => { + // Deploying sovrynProtocol w/ generic function from initializer.js + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + }); + + describe("Protocol Tests", () => { + it("Test targetSetup", async () => { + const sig1 = "testFunction1(address,uint256,bytes)"; + const sig2 = "testFunction2(address[],uint256[],bytes[])"; + + const sigs = [sig1, sig2]; + let targets = [ONE_ADDRESS, ONE_ADDRESS]; + await sovryn.setTargets(sigs, targets); + + expect((await sovryn.getTarget(sig1)) == ONE_ADDRESS).to.be.true; + expect((await sovryn.getTarget(sig2)) == ONE_ADDRESS).to.be.true; + + targets = [constants.ZERO_ADDRESS, constants.ZERO_ADDRESS]; + await sovryn.setTargets(sigs, targets); + + expect((await sovryn.getTarget(sig1)) == constants.ZERO_ADDRESS).to.be.true; + expect((await sovryn.getTarget(sig2)) == constants.ZERO_ADDRESS).to.be.true; + }); + + it("should revert for count mismatch while setting targets", async () => { + const sig1 = "testFunction1(address,uint256,bytes)"; + const sig2 = "testFunction2(address[],uint256[],bytes[])"; + const sigs = [sig1, sig2]; + let targets = [ONE_ADDRESS, ONE_ADDRESS, ONE_ADDRESS]; + + await expectRevert(sovryn.setTargets(sigs, targets), "count mismatch"); + }); + + it("Test replaceContract", async () => { + const sig = + "setupLoanParams((bytes32,bool,address,address,address,uint256,uint256,uint256)[])"; + const loanSettings = await LoanSettings.new(); + + await sovryn.setTargets([sig], [constants.ZERO_ADDRESS]); + expect((await sovryn.getTarget(sig)) == constants.ZERO_ADDRESS).to.be.true; + + await sovryn.replaceContract(loanSettings.address); + expect((await sovryn.getTarget(sig)) == loanSettings.address).to.be.true; + }); + + it("Test receiveEther", async () => { + expect((await web3.eth.getBalance(sovryn.address)) == 0).to.be.true; + // gasleft() should be < 2300 in the Protocol.sol proxy contract, 21000 is the min gas for sending value + await web3.eth.sendTransaction({ + from: accounts[0].toString(), + to: sovryn.address, + value: 10000, + gas: 22000, + }); + expect((await web3.eth.getBalance(sovryn.address)) == 10000).to.be.true; + }); + }); + + describe("Events - replaceContract", () => { + it("should fail replaceContract w/ bad address", async () => { + await expectRevert(sovryn.replaceContract(sovryn.address), "setup failed"); + }); + + it("Test replaceContract - Affiliates", async () => { + const selector = "getUserNotFirstTradeFlag(address)"; + let oldAffiliatesAddr = await sovryn.getTarget(selector); + let newAffiliatesAddr = await Affiliates.new(); + let tx = await sovryn.replaceContract(newAffiliatesAddr.address); + expectEvent(tx, "ProtocolModuleContractReplaced", { + prevModuleContractAddress: oldAffiliatesAddr, + newModuleContractAddress: newAffiliatesAddr.address, + module: ethers.utils.formatBytes32String("Affiliates"), + }); + }); + + it("Test replaceContract - LoanClosingsLiquidation", async () => { + const selector = "liquidate(bytes32,address,uint256)"; + let oldLoanClosingsLiquidationAddr = await sovryn.getTarget(selector); + let newLoanClosingsLiquidationAddr = await LoanClosingsLiquidation.new(); + let tx = await sovryn.replaceContract(newLoanClosingsLiquidationAddr.address); + expectEvent(tx, "ProtocolModuleContractReplaced", { + prevModuleContractAddress: oldLoanClosingsLiquidationAddr, + newModuleContractAddress: newLoanClosingsLiquidationAddr.address, + module: ethers.utils.formatBytes32String("LoanClosingsLiquidation"), + }); + }); + + it("Test replaceContract - LoanClosingsRollover", async () => { + const selector = "rollover(bytes32,bytes)"; + let oldLoanClosingsRolloverAddr = await sovryn.getTarget(selector); + let newLoanClosingsRolloverAddr = await LoanClosingsRollover.new(); + let tx = await sovryn.replaceContract(newLoanClosingsRolloverAddr.address); + expectEvent(tx, "ProtocolModuleContractReplaced", { + prevModuleContractAddress: oldLoanClosingsRolloverAddr, + newModuleContractAddress: newLoanClosingsRolloverAddr.address, + module: ethers.utils.formatBytes32String("LoanClosingsRollover"), + }); + }); + + it("Test replaceContract - LoanClosingsWith", async () => { + let newLoanClosingsWithAddr = await LoanClosingsWith.new(); + let tx = await sovryn.replaceContract(newLoanClosingsWithAddr.address); + expectEvent(tx, "ProtocolModuleContractReplaced"); + }); + + it("Test replaceContract - LoanMaintenance", async () => { + let newLoanMaintenanceAddr = await LoanMaintenance.new(); + let tx = await sovryn.replaceContract(newLoanMaintenanceAddr.address); + expectEvent(tx, "ProtocolModuleContractReplaced"); + }); + + it("Test replaceContract - LoanOpenings", async () => { + let newLoanOpeningsAddr = await LoanOpenings.new(); + let tx = await sovryn.replaceContract(newLoanOpeningsAddr.address); + expectEvent(tx, "ProtocolModuleContractReplaced"); + }); + + it("Test replaceContract - LoanSettings", async () => { + let newLoanSettingsAddr = await LoanSettings.new(); + let tx = await sovryn.replaceContract(newLoanSettingsAddr.address); + expectEvent(tx, "ProtocolModuleContractReplaced"); + }); + + it("Test replaceContract - ProtocolSettings", async () => { + const selector = "setSovrynProtocolAddress(address)"; + let oldProtocolSettingsAddr = await sovryn.getTarget(selector); + let newProtocolSettingsAddr = await ProtocolSettings.new(); + let tx = await sovryn.replaceContract(newProtocolSettingsAddr.address); + expectEvent(tx, "ProtocolModuleContractReplaced", { + prevModuleContractAddress: oldProtocolSettingsAddr, + newModuleContractAddress: newProtocolSettingsAddr.address, + module: ethers.utils.formatBytes32String("ProtocolSettings"), + }); + }); + + it("Test replaceContract - SwapsExternal", async () => { + let newSwapsExternalAddr = await SwapsExternal.new(); + let tx = await sovryn.replaceContract(newSwapsExternalAddr.address); + expectEvent(tx, "ProtocolModuleContractReplaced"); + }); + }); }); diff --git a/tests/other/ProtocolSettings.test.js b/tests/other/ProtocolSettings.test.js index 80def59ae..436c29a1f 100644 --- a/tests/other/ProtocolSettings.test.js +++ b/tests/other/ProtocolSettings.test.js @@ -29,15 +29,15 @@ const LoanMaintenance = artifacts.require("LoanMaintenance"); const SwapsExternal = artifacts.require("SwapsExternal"); const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getPriceFeeds, - decodeLogs, - getSovryn, - getLoanToken, - getLoanTokenLogicWrbtc, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getPriceFeeds, + decodeLogs, + getSovryn, + getLoanToken, + getLoanTokenLogicWrbtc, } = require("../Utils/initializer.js"); const wei = web3.utils.toWei; @@ -48,155 +48,175 @@ const hunEth = new BN(wei("100", "ether")); // Deploys the multisig wallet contract setting 3 owners and 2 required confirmations const getMultisig = async (accounts) => { - const requiredConf = 2; - const owners = [accounts[0], accounts[1], accounts[2]]; - const multisig = await MultiSigWallet.new(owners, requiredConf); - return multisig; + const requiredConf = 2; + const owners = [accounts[0], accounts[1], accounts[2]]; + const multisig = await MultiSigWallet.new(owners, requiredConf); + return multisig; }; contract("ProtocolSettings", (accounts) => { - let sovryn, SUSD, WRBTC, RBTC, BZRX, priceFeeds, multisig, sov; - const ONE_ADDRESS = "0x0000000000000000000000000000000000000001"; - let lender, loanToken, loanTokenAddress; - - async function deploymentAndInitFixture(_wallets, _provider) { - // Deploying sovrynProtocol w/ generic function from initializer.js - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - - multisig = await getMultisig(accounts); - await sovryn.transferOwnership(multisig.address); - - /// @dev A SOV mint useful for every test - sov = await TestToken.new("Sovryn", "SOV", 18, new BN(10).pow(new BN(50))); - await sov.transfer(multisig.address, new BN(10).pow(new BN(50)), { from: accounts[0] }); - - /// @dev a loanToken required to test setting loan pools on the protocol - loanToken = await getLoanToken(lender, sovryn, WRBTC, SUSD); - loanTokenAddress = await loanToken.loanTokenAddress(); - } - - before(async () => { - [lender] = accounts; - }); - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - describe("ProtocolSettings Tests", () => { - it("Test setCoreParams", async () => { - const dest = sovryn.address; - const val = 0; - - let data = sovryn.contract.methods.setPriceFeedContract(ONE_ADDRESS).encodeABI(); - let tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); - let txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - await multisig.confirmTransaction(txId, { from: accounts[1] }); - - data = sovryn.contract.methods.setSwapsImplContract(ONE_ADDRESS).encodeABI(); - tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); - txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - await multisig.confirmTransaction(txId, { from: accounts[1] }); - - expect((await sovryn.priceFeeds()) == ONE_ADDRESS).to.be.true; - expect((await sovryn.swapsImpl()) == ONE_ADDRESS).to.be.true; - }); - - it("Test setLoanPool", async () => { - expect((await sovryn.loanPoolToUnderlying(accounts[6])) == ZERO_ADDRESS).to.be.true; - expect((await sovryn.underlyingToLoanPool(accounts[7])) == ZERO_ADDRESS).to.be.true; - - expect(await sovryn.isLoanPool(accounts[6])).to.be.false; - expect(await sovryn.isLoanPool(accounts[8])).to.be.false; - const dest = sovryn.address; - let val = 0; - let data = sovryn.contract.methods.setLoanPool([accounts[6], accounts[8]], [accounts[7], accounts[9]]).encodeABI(); - let tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); - let txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - await multisig.confirmTransaction(txId, { from: accounts[1] }); - - expect((await sovryn.loanPoolToUnderlying(accounts[6])) == accounts[7]).to.be.true; - expect((await sovryn.underlyingToLoanPool(accounts[7])) == accounts[6]).to.be.true; - - expect((await sovryn.loanPoolToUnderlying(accounts[8])) == accounts[9]).to.be.true; - expect((await sovryn.underlyingToLoanPool(accounts[9])) == accounts[8]).to.be.true; - - expect(await sovryn.isLoanPool(accounts[6])).to.be.true; - expect(await sovryn.isLoanPool(accounts[8])).to.be.true; - - data = sovryn.contract.methods.setLoanPool([accounts[6]], [ZERO_ADDRESS]).encodeABI(); - tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); - txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - await multisig.confirmTransaction(txId, { from: accounts[1] }); - - expect((await sovryn.loanPoolToUnderlying(accounts[6])) == ZERO_ADDRESS).to.be.true; - expect((await sovryn.underlyingToLoanPool(accounts[7])) == ZERO_ADDRESS).to.be.true; - - expect(await sovryn.isLoanPool(accounts[6])).to.be.false; - }); - - it("Test set wrbtc token", async () => { - expect((await sovryn.owner()) == multisig.address).to.be.true; - - const dest = sovryn.address; - const val = 0; - const data = sovryn.contract.methods.setWrbtcToken(WRBTC.address).encodeABI(); - const tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); - const txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - await multisig.confirmTransaction(txId, { from: accounts[1] }); - - expect((await sovryn.wrbtcToken()) == WRBTC.address).to.be.true; - - await expectRevert(sovryn.setWrbtcToken(WRBTC.address, { from: accounts[0] }), "unauthorized"); - }); - - it("Should revert when setting wrbtc token w/ not a contract address", async () => { - expect((await sovryn.owner()) == multisig.address).to.be.true; - - const dest = sovryn.address; - const val = 0; - const data = sovryn.contract.methods.setWrbtcToken(ZERO_ADDRESS).encodeABI(); - const tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); - const txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); - - expectEvent(receipt, "ExecutionFailure"); - }); - - it("Test set protocol token address", async () => { - expect((await sovryn.protocolTokenAddress()) == ZERO_ADDRESS).to.be.true; - - const dest = sovryn.address; - const val = 0; - const data = sovryn.contract.methods.setProtocolTokenAddress(sov.address).encodeABI(); - const tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); - const txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - await multisig.confirmTransaction(txId, { from: accounts[1] }); - - expect((await sovryn.protocolTokenAddress()) == sov.address).to.be.true; - - await expectRevert(sovryn.setProtocolTokenAddress(sov.address, { from: accounts[1] }), "unauthorized"); - }); - - it("Should revert when setting protocol token w/ not a contract address", async () => { - expect((await sovryn.protocolTokenAddress()) == ZERO_ADDRESS).to.be.true; - - const dest = sovryn.address; - const val = 0; - const data = sovryn.contract.methods.setProtocolTokenAddress(ZERO_ADDRESS).encodeABI(); - const tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); - const txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); - - expectEvent(receipt, "ExecutionFailure"); - }); - - /* + let sovryn, SUSD, WRBTC, RBTC, BZRX, priceFeeds, multisig, sov; + const ONE_ADDRESS = "0x0000000000000000000000000000000000000001"; + let lender, loanToken, loanTokenAddress; + + async function deploymentAndInitFixture(_wallets, _provider) { + // Deploying sovrynProtocol w/ generic function from initializer.js + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + + multisig = await getMultisig(accounts); + await sovryn.transferOwnership(multisig.address); + + /// @dev A SOV mint useful for every test + sov = await TestToken.new("Sovryn", "SOV", 18, new BN(10).pow(new BN(50))); + await sov.transfer(multisig.address, new BN(10).pow(new BN(50)), { from: accounts[0] }); + + /// @dev a loanToken required to test setting loan pools on the protocol + loanToken = await getLoanToken(lender, sovryn, WRBTC, SUSD); + loanTokenAddress = await loanToken.loanTokenAddress(); + } + + before(async () => { + [lender] = accounts; + }); + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + describe("ProtocolSettings Tests", () => { + it("Test setCoreParams", async () => { + const dest = sovryn.address; + const val = 0; + + let data = sovryn.contract.methods.setPriceFeedContract(ONE_ADDRESS).encodeABI(); + let tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); + let txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + await multisig.confirmTransaction(txId, { from: accounts[1] }); + + data = sovryn.contract.methods.setSwapsImplContract(ONE_ADDRESS).encodeABI(); + tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); + txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; + await multisig.confirmTransaction(txId, { from: accounts[1] }); + + expect((await sovryn.priceFeeds()) == ONE_ADDRESS).to.be.true; + expect((await sovryn.swapsImpl()) == ONE_ADDRESS).to.be.true; + }); + + it("Test setLoanPool", async () => { + expect((await sovryn.loanPoolToUnderlying(accounts[6])) == ZERO_ADDRESS).to.be.true; + expect((await sovryn.underlyingToLoanPool(accounts[7])) == ZERO_ADDRESS).to.be.true; + + expect(await sovryn.isLoanPool(accounts[6])).to.be.false; + expect(await sovryn.isLoanPool(accounts[8])).to.be.false; + const dest = sovryn.address; + let val = 0; + let data = sovryn.contract.methods + .setLoanPool([accounts[6], accounts[8]], [accounts[7], accounts[9]]) + .encodeABI(); + let tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); + let txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + await multisig.confirmTransaction(txId, { from: accounts[1] }); + + expect((await sovryn.loanPoolToUnderlying(accounts[6])) == accounts[7]).to.be.true; + expect((await sovryn.underlyingToLoanPool(accounts[7])) == accounts[6]).to.be.true; + + expect((await sovryn.loanPoolToUnderlying(accounts[8])) == accounts[9]).to.be.true; + expect((await sovryn.underlyingToLoanPool(accounts[9])) == accounts[8]).to.be.true; + + expect(await sovryn.isLoanPool(accounts[6])).to.be.true; + expect(await sovryn.isLoanPool(accounts[8])).to.be.true; + + data = sovryn.contract.methods.setLoanPool([accounts[6]], [ZERO_ADDRESS]).encodeABI(); + tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); + txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; + await multisig.confirmTransaction(txId, { from: accounts[1] }); + + expect((await sovryn.loanPoolToUnderlying(accounts[6])) == ZERO_ADDRESS).to.be.true; + expect((await sovryn.underlyingToLoanPool(accounts[7])) == ZERO_ADDRESS).to.be.true; + + expect(await sovryn.isLoanPool(accounts[6])).to.be.false; + }); + + it("Test set wrbtc token", async () => { + expect((await sovryn.owner()) == multisig.address).to.be.true; + + const dest = sovryn.address; + const val = 0; + const data = sovryn.contract.methods.setWrbtcToken(WRBTC.address).encodeABI(); + const tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); + const txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + await multisig.confirmTransaction(txId, { from: accounts[1] }); + + expect((await sovryn.wrbtcToken()) == WRBTC.address).to.be.true; + + await expectRevert( + sovryn.setWrbtcToken(WRBTC.address, { from: accounts[0] }), + "unauthorized" + ); + }); + + it("Should revert when setting wrbtc token w/ not a contract address", async () => { + expect((await sovryn.owner()) == multisig.address).to.be.true; + + const dest = sovryn.address; + const val = 0; + const data = sovryn.contract.methods.setWrbtcToken(ZERO_ADDRESS).encodeABI(); + const tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); + const txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); + + expectEvent(receipt, "ExecutionFailure"); + }); + + it("Test set protocol token address", async () => { + expect((await sovryn.protocolTokenAddress()) == ZERO_ADDRESS).to.be.true; + + const dest = sovryn.address; + const val = 0; + const data = sovryn.contract.methods.setProtocolTokenAddress(sov.address).encodeABI(); + const tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); + const txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + await multisig.confirmTransaction(txId, { from: accounts[1] }); + + expect((await sovryn.protocolTokenAddress()) == sov.address).to.be.true; + + await expectRevert( + sovryn.setProtocolTokenAddress(sov.address, { from: accounts[1] }), + "unauthorized" + ); + }); + + it("Should revert when setting protocol token w/ not a contract address", async () => { + expect((await sovryn.protocolTokenAddress()) == ZERO_ADDRESS).to.be.true; + + const dest = sovryn.address; + const val = 0; + const data = sovryn.contract.methods.setProtocolTokenAddress(ZERO_ADDRESS).encodeABI(); + const tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); + const txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); + + expectEvent(receipt, "ExecutionFailure"); + }); + + /* Should set and deposit the protocol token 1. deploy erc20 2. set address @@ -204,580 +224,757 @@ contract("ProtocolSettings", (accounts) => { 4. deposit tokens 5. verify balance */ - it("Test deposit protocol token", async () => { - const dest = sovryn.address; - const val = 0; - - let data = await sov.contract.methods.approve(sovryn.address, hunEth).encodeABI(); - - let tx = await multisig.submitTransaction(sov.address, val, data, { from: accounts[0] }); - let txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - await multisig.confirmTransaction(txId, { from: accounts[1] }); - - data = await sovryn.contract.methods.setProtocolTokenAddress(sov.address).encodeABI(); - - tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); - txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - await multisig.confirmTransaction(txId, { from: accounts[1] }); - - data = sovryn.contract.methods.depositProtocolToken(hunEth).encodeABI(); - - tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); - txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - await multisig.confirmTransaction(txId, { from: accounts[1] }); - - expect((await sovryn.protocolTokenHeld()).eq(hunEth)).to.be.true; - }); - - it("Test fail deposit protocol token", async () => { - const dest = sovryn.address; - const val = 0; - - let data = await sov.contract.methods.approve(sovryn.address, hunEth).encodeABI(); - - let tx = await multisig.submitTransaction(sov.address, val, data, { from: accounts[0] }); - let txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - await multisig.confirmTransaction(txId, { from: accounts[1] }); - - data = await sovryn.contract.methods.setProtocolTokenAddress(sov.address).encodeABI(); - - tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); - txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - await multisig.confirmTransaction(txId, { from: accounts[1] }); - - await expectRevert(sovryn.depositProtocolToken(sov.address, { from: accounts[0] }), "unauthorized"); - }); - - // Should withdraw no tokens - it("Coverage Test: withdraw amount 0 from protocol", async () => { - const dest = sovryn.address; - const val = 0; - - let data = await sov.contract.methods.approve(sovryn.address, hunEth).encodeABI(); - - let tx = await multisig.submitTransaction(sov.address, val, data, { from: accounts[0] }); - let txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - await multisig.confirmTransaction(txId, { from: accounts[1] }); - - data = await sovryn.contract.methods.setProtocolTokenAddress(sov.address).encodeABI(); - - tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); - txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - await multisig.confirmTransaction(txId, { from: accounts[1] }); - - data = sovryn.contract.methods.depositProtocolToken(hunEth).encodeABI(); - - tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); - txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - await multisig.confirmTransaction(txId, { from: accounts[1] }); - - const balanceBefore = await sov.balanceOf(accounts[1]); + it("Test deposit protocol token", async () => { + const dest = sovryn.address; + const val = 0; - data = sovryn.contract.methods.withdrawProtocolToken(accounts[1], 0).encodeABI(); + let data = await sov.contract.methods.approve(sovryn.address, hunEth).encodeABI(); - tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); - txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - await multisig.confirmTransaction(txId, { from: accounts[1] }); + let tx = await multisig.submitTransaction(sov.address, val, data, { + from: accounts[0], + }); + let txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + await multisig.confirmTransaction(txId, { from: accounts[1] }); - const balanceAfter = await sov.balanceOf(accounts[1]); - expect((await sovryn.protocolTokenHeld()).eq(hunEth)).to.be.true; - expect(balanceAfter.eq(balanceBefore)).to.be.true; - }); + data = await sovryn.contract.methods.setProtocolTokenAddress(sov.address).encodeABI(); - // Should successfully withdraw all deposited protocol tokens - it("Test withdraw protocol token", async () => { - const dest = sovryn.address; - const val = 0; + tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); + txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; + await multisig.confirmTransaction(txId, { from: accounts[1] }); - let data = await sov.contract.methods.approve(sovryn.address, hunEth).encodeABI(); + data = sovryn.contract.methods.depositProtocolToken(hunEth).encodeABI(); - let tx = await multisig.submitTransaction(sov.address, val, data, { from: accounts[0] }); - let txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - await multisig.confirmTransaction(txId, { from: accounts[1] }); + tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); + txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; + await multisig.confirmTransaction(txId, { from: accounts[1] }); - data = await sovryn.contract.methods.setProtocolTokenAddress(sov.address).encodeABI(); + expect((await sovryn.protocolTokenHeld()).eq(hunEth)).to.be.true; + }); - tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); - txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - await multisig.confirmTransaction(txId, { from: accounts[1] }); + it("Test fail deposit protocol token", async () => { + const dest = sovryn.address; + const val = 0; - data = sovryn.contract.methods.depositProtocolToken(hunEth).encodeABI(); + let data = await sov.contract.methods.approve(sovryn.address, hunEth).encodeABI(); - tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); - txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - await multisig.confirmTransaction(txId, { from: accounts[1] }); + let tx = await multisig.submitTransaction(sov.address, val, data, { + from: accounts[0], + }); + let txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + await multisig.confirmTransaction(txId, { from: accounts[1] }); - const balanceBefore = await sov.balanceOf(accounts[1]); + data = await sovryn.contract.methods.setProtocolTokenAddress(sov.address).encodeABI(); - data = sovryn.contract.methods.withdrawProtocolToken(accounts[1], hunEth).encodeABI(); + tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); + txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; + await multisig.confirmTransaction(txId, { from: accounts[1] }); - tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); - txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - await multisig.confirmTransaction(txId, { from: accounts[1] }); + await expectRevert( + sovryn.depositProtocolToken(sov.address, { from: accounts[0] }), + "unauthorized" + ); + }); - const balanceAfter = await sov.balanceOf(accounts[1]); - expect((await sovryn.protocolTokenHeld()).eq(new BN(0))).to.be.true; - expect(balanceAfter.eq(balanceBefore.add(hunEth))).to.be.true; - }); + // Should withdraw no tokens + it("Coverage Test: withdraw amount 0 from protocol", async () => { + const dest = sovryn.address; + const val = 0; - // Should fail to withdraw 1e30 protocol tokens but withdraw 1e20 - it("Test fail withdraw protocol token", async () => { - const dest = sovryn.address; - const val = 0; + let data = await sov.contract.methods.approve(sovryn.address, hunEth).encodeABI(); - let data = await sov.contract.methods.approve(sovryn.address, hunEth).encodeABI(); + let tx = await multisig.submitTransaction(sov.address, val, data, { + from: accounts[0], + }); + let txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + await multisig.confirmTransaction(txId, { from: accounts[1] }); - let tx = await multisig.submitTransaction(sov.address, val, data, { from: accounts[0] }); - let txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - await multisig.confirmTransaction(txId, { from: accounts[1] }); + data = await sovryn.contract.methods.setProtocolTokenAddress(sov.address).encodeABI(); - data = await sovryn.contract.methods.setProtocolTokenAddress(sov.address).encodeABI(); + tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); + txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; + await multisig.confirmTransaction(txId, { from: accounts[1] }); - tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); - txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - await multisig.confirmTransaction(txId, { from: accounts[1] }); + data = sovryn.contract.methods.depositProtocolToken(hunEth).encodeABI(); - data = sovryn.contract.methods.depositProtocolToken(hunEth).encodeABI(); + tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); + txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; + await multisig.confirmTransaction(txId, { from: accounts[1] }); - tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); - txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - await multisig.confirmTransaction(txId, { from: accounts[1] }); + const balanceBefore = await sov.balanceOf(accounts[1]); - const balanceBefore = await sov.balanceOf(accounts[1]); + data = sovryn.contract.methods.withdrawProtocolToken(accounts[1], 0).encodeABI(); - data = sovryn.contract.methods.withdrawProtocolToken(accounts[1], new BN(10).pow(new BN(30))).encodeABI(); + tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); + txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; + await multisig.confirmTransaction(txId, { from: accounts[1] }); - tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); - txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - await multisig.confirmTransaction(txId, { from: accounts[1] }); + const balanceAfter = await sov.balanceOf(accounts[1]); + expect((await sovryn.protocolTokenHeld()).eq(hunEth)).to.be.true; + expect(balanceAfter.eq(balanceBefore)).to.be.true; + }); - const balanceAfter = await sov.balanceOf(accounts[1]); - expect((await sovryn.protocolTokenHeld()).eq(new BN(0))).to.be.true; - expect(balanceAfter.eq(balanceBefore.add(hunEth))).to.be.true; - }); + // Should successfully withdraw all deposited protocol tokens + it("Test withdraw protocol token", async () => { + const dest = sovryn.address; + const val = 0; - // Should successfully change rollover base reward - it("Test set rollover base reward", async () => { - const new_reward = new BN(10).pow(new BN(15)); - const old_reward = await sovryn.rolloverBaseReward(); + let data = await sov.contract.methods.approve(sovryn.address, hunEth).encodeABI(); - const dest = sovryn.address; - const val = 0; - const data = await sovryn.contract.methods.setRolloverBaseReward(new_reward).encodeABI(); + let tx = await multisig.submitTransaction(sov.address, val, data, { + from: accounts[0], + }); + let txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + await multisig.confirmTransaction(txId, { from: accounts[1] }); - const tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); - let txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); + data = await sovryn.contract.methods.setProtocolTokenAddress(sov.address).encodeABI(); - const decode = decodeLogs(receipt.rawLogs, ProtocolSettings, "SetRolloverBaseReward"); - const event = decode[0].args; - expect(event["sender"] == multisig.address).to.be.true; - expect(event["oldValue"] == old_reward.toString()).to.be.true; - expect(event["newValue"] == new_reward.toString()).to.be.true; - expect((await sovryn.rolloverBaseReward()).eq(new_reward)).to.be.true; - }); - - // Should fail to change rollover base reward by unauthorized user - it("Test set rollover base reward by unauthorized user", async () => { - await expectRevert(sovryn.setRolloverBaseReward(new BN(10).pow(new BN(15)), { from: accounts[0] }), "unauthorized"); - }); - - it("Should revert when setting rollover base reward w/ 0 amount", async () => { - const dest = sovryn.address; - const val = 0; - const data = await sovryn.contract.methods.setRolloverBaseReward(new BN(0)).encodeABI(); - - const tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); - let txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); - - expectEvent(receipt, "ExecutionFailure"); - }); - - // Should successfully change rebate percent - it("Test set rebate percent", async () => { - const new_percent = new BN(2).mul(oneEth); - const old_percent = await sovryn.getFeeRebatePercent(); - - const dest = sovryn.address; - const val = 0; - const data = await sovryn.contract.methods.setRebatePercent(new_percent).encodeABI(); - - const tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); - let txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); - - const decode = decodeLogs(receipt.rawLogs, ProtocolSettings, "SetRebatePercent"); - const event = decode[0].args; - expect(event["sender"] == multisig.address).to.be.true; - expect(event["oldRebatePercent"] == old_percent.toString()).to.be.true; - expect(event["newRebatePercent"] == new_percent.toString()).to.be.true; - expect((await sovryn.getFeeRebatePercent()).eq(new_percent)).to.be.true; - }); - - // Should fail to change rebate percent by unauthorized user - it("Test set rebate percent by unauthorized user", async () => { - await expectRevert(sovryn.setRebatePercent(new BN(2).mul(oneEth), { from: accounts[0] }), "unauthorized"); - }); - - it("Should revert when setting a too high fee rebate", async () => { - const dest = sovryn.address; - const val = 0; - const data = await sovryn.contract.methods.setRebatePercent(new BN(10).pow(new BN(20)).add(new BN(1))).encodeABI(); - - const tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); - let txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); - - expectEvent(receipt, "ExecutionFailure"); - }); - - // Should successfully change rebate percent - it("Test set trading rebate rewards basis point", async () => { - const new_basis_point = new BN(9999); - const old_basis_point = await sovryn.getTradingRebateRewardsBasisPoint(); - - const dest = sovryn.address; - const val = 0; - const data = await sovryn.contract.methods.setTradingRebateRewardsBasisPoint(new_basis_point).encodeABI(); - - const tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); - let txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); - - const decode = decodeLogs(receipt.rawLogs, ProtocolSettings, "SetTradingRebateRewardsBasisPoint"); - const event = decode[0].args; - expect(event["sender"] == multisig.address).to.be.true; - expect(event["oldBasisPoint"] == old_basis_point.toString()).to.be.true; - expect(event["newBasisPoint"] == new_basis_point.toString()).to.be.true; - expect((await sovryn.getTradingRebateRewardsBasisPoint()).eq(new_basis_point)).to.be.true; - }); - - // Should fail to change rebate percent by unauthorized user - it("Test set trading rebate rewards basis point by unauthorized user", async () => { - await expectRevert(sovryn.setTradingRebateRewardsBasisPoint(new BN(10000), { from: accounts[0] }), "unauthorized"); - }); - - // Should successfully change the swapExternalFeePercent - it("Test set swapExternalFeePercent", async () => { - const new_percent = new BN(2).mul(oneEth); - const old_percent = await sovryn.getSwapExternalFeePercent(); - - const dest = sovryn.address; - const val = 0; - const data = await sovryn.contract.methods.setSwapExternalFeePercent(new_percent).encodeABI(); - - const tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); - let txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); - - const decode = decodeLogs(receipt.rawLogs, ProtocolSettings, "SetSwapExternalFeePercent"); - const event = decode[0].args; - expect(event["sender"] == multisig.address).to.be.true; - expect(event["oldValue"] == old_percent.toString()).to.be.true; - expect(event["newValue"] == new_percent.toString()).to.be.true; - expect((await sovryn.getSwapExternalFeePercent()).eq(new_percent)).to.be.true; - }); - - // Should fail to change swap external fee percent by unauthorized user - it("Test set swapExternalFeePercent with unauthorized sender", async () => { - await expectRevert(sovryn.setSwapExternalFeePercent(new BN(2).mul(oneEth), { from: accounts[0] }), "unauthorized"); - }); - - it("should work: setBorrowingFeePercent", async () => { - /// @dev setBorrowingFeePercent must be called from multisig - let newValue = new BN(10).pow(new BN(20)); - const data = await sovryn.contract.methods.setBorrowingFeePercent(newValue).encodeABI(); - const tx = await multisig.submitTransaction(sovryn.address, 0, data, { from: accounts[0] }); - let txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); - expectEvent(receipt, "Execution"); - - // Check emitted event arguments - const decode = decodeLogs(receipt.rawLogs, ProtocolSettings, "SetBorrowingFeePercent"); - const event = decode[0].args; - expect(event["sender"] == multisig.address).to.be.true; - /// @dev Default value at State.sol: - /// 0.09% fee /// Origination fee paid for each loan. - /// uint256 public borrowingFeePercent = 9 * 10**16; - /// 90000000000000000 - expect(event["oldValue"] == new BN(9).mul(new BN(10).pow(new BN(16)))).to.be.true; - expect(event["newValue"] == newValue).to.be.true; - }); - - it("shouldn't work: setBorrowingFeePercent w/ value too high", async () => { - /// @dev setBorrowingFeePercent must be called from multisig - const data = await sovryn.contract.methods.setBorrowingFeePercent(new BN(10).pow(new BN(20)).add(new BN(1))).encodeABI(); - const tx = await multisig.submitTransaction(sovryn.address, 0, data, { from: accounts[0] }); - let txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); - expectEvent(receipt, "ExecutionFailure"); - }); - - it("should work: setLiquidationIncentivePercent", async () => { - /// @dev setLiquidationIncentivePercent must be called from multisig - let newValue = new BN(10).pow(new BN(20)); - const data = await sovryn.contract.methods.setLiquidationIncentivePercent(newValue).encodeABI(); - const tx = await multisig.submitTransaction(sovryn.address, 0, data, { from: accounts[0] }); - let txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); - expectEvent(receipt, "Execution"); - - // Check emitted event arguments - const decode = decodeLogs(receipt.rawLogs, ProtocolSettings, "SetLiquidationIncentivePercent"); - const event = decode[0].args; - expect(event["sender"] == multisig.address).to.be.true; - /// @dev Default value at State.sol: - /// 5% collateral discount /// Discount on collateral for liquidators. - /// uint256 public liquidationIncentivePercent = 5 * 10**18; - /// 5000000000000000000 - expect(event["oldValue"] == new BN(5).mul(new BN(10).pow(new BN(18)))).to.be.true; - expect(event["newValue"] == newValue).to.be.true; - }); - - it("shouldn't work: setLiquidationIncentivePercent w/ value too high", async () => { - /// @dev setLiquidationIncentivePercent must be called from multisig - const data = await sovryn.contract.methods - .setLiquidationIncentivePercent(new BN(10).pow(new BN(20)).add(new BN(1))) - .encodeABI(); - const tx = await multisig.submitTransaction(sovryn.address, 0, data, { from: accounts[0] }); - let txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); - expectEvent(receipt, "ExecutionFailure"); - }); - - it("should work: setMaxDisagreement", async () => { - /// @dev setMaxDisagreement must be called from multisig - const data = await sovryn.contract.methods.setMaxDisagreement(new BN(10).pow(new BN(20))).encodeABI(); - const tx = await multisig.submitTransaction(sovryn.address, 0, data, { from: accounts[0] }); - let txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); - expectEvent(receipt, "Execution"); - }); - - it("should work: setSourceBuffer", async () => { - /// @dev setSourceBuffer must be called from multisig - const data = await sovryn.contract.methods.setSourceBuffer(new BN(10).pow(new BN(20))).encodeABI(); - const tx = await multisig.submitTransaction(sovryn.address, 0, data, { from: accounts[0] }); - let txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); - expectEvent(receipt, "Execution"); - }); - - it("should work: setMaxSwapSize", async () => { - /// @dev setMaxSwapSize must be called from multisig - let newValue = new BN(10).pow(new BN(20)); - const data = await sovryn.contract.methods.setMaxSwapSize(newValue).encodeABI(); - const tx = await multisig.submitTransaction(sovryn.address, 0, data, { from: accounts[0] }); - let txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); - expectEvent(receipt, "Execution"); - - // Check emitted event arguments - const decode = decodeLogs(receipt.rawLogs, ProtocolSettings, "SetMaxSwapSize"); - const event = decode[0].args; - expect(event["sender"] == multisig.address).to.be.true; - /// @dev Default value at State.sol: - /// Maximum support swap size in rBTC - /// uint256 public maxSwapSize = 50 ether; - /// 50000000000000000000 - expect(event["oldValue"] == new BN(50).mul(new BN(10).pow(new BN(18)))).to.be.true; - expect(event["newValue"] == newValue).to.be.true; - }); - - it("should work on setLoanPool w/ 1 pool and 1 asset previously deployed", async () => { - /// @dev setLoanPool must be called from multisig - let pools = [loanToken.address]; - let assets = [loanTokenAddress]; - const data = await sovryn.contract.methods.setLoanPool(pools, assets).encodeABI(); - const tx = await multisig.submitTransaction(sovryn.address, 0, data, { from: accounts[0] }); - let txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); - expectEvent(receipt, "Execution"); - - let list = await sovryn.getLoanPoolsList.call(0, 10); - console.log("loanPools = ", list); - }); - - it("should revert for count mismatch on setLoanPool w/ 1 pool 2 assets", async () => { - /// @dev setLoanPool must be called from multisig - let pools = [loanToken.address]; - let assets = [loanTokenAddress, loanTokenAddress]; - const data = await sovryn.contract.methods.setLoanPool(pools, assets).encodeABI(); - const tx = await multisig.submitTransaction(sovryn.address, 0, data, { from: accounts[0] }); - let txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); - expectEvent(receipt, "ExecutionFailure"); - }); - - it("should revert on setLoanPool w/ 1 pool and 1 asset that are equal", async () => { - /// @dev setLoanPool must be called from multisig - let pools = [loanToken.address]; - let assets = [loanToken.address]; - const data = await sovryn.contract.methods.setLoanPool(pools, assets).encodeABI(); - const tx = await multisig.submitTransaction(sovryn.address, 0, data, { from: accounts[0] }); - let txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); - expectEvent(receipt, "ExecutionFailure"); - }); - - it("should revert on setLoanPool w/ 1 pool equal to address(0) and 1 asset", async () => { - /// @dev setLoanPool must be called from multisig - let pools = [ZERO_ADDRESS]; - let assets = [loanToken.address]; - const data = await sovryn.contract.methods.setLoanPool(pools, assets).encodeABI(); - const tx = await multisig.submitTransaction(sovryn.address, 0, data, { from: accounts[0] }); - let txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); - expectEvent(receipt, "ExecutionFailure"); - }); - - it("should revert on setLoanPool w/ 1 pool and 1 asset equal to address(0)", async () => { - /// @dev setLoanPool must be called from multisig - let pools = [loanToken.address]; - let assets = [ZERO_ADDRESS]; - const data = await sovryn.contract.methods.setLoanPool(pools, assets).encodeABI(); - const tx = await multisig.submitTransaction(sovryn.address, 0, data, { from: accounts[0] }); - let txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); - expectEvent(receipt, "ExecutionFailure"); - }); - - it("should work on setSupportedTokens w/ 1 address and 1 toogle", async () => { - /// @dev setSupportedTokens must be called from multisig - let addresses = [ZERO_ADDRESS]; - let toggles = [true]; - const data = await sovryn.contract.methods.setSupportedTokens(addresses, toggles).encodeABI(); - const tx = await multisig.submitTransaction(sovryn.address, 0, data, { from: accounts[0] }); - let txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); - expectEvent(receipt, "Execution"); - }); - - it("should revert for count mismatch on setSupportedTokens w/ 1 address 2 toggles", async () => { - /// @dev setSupportedTokens must be called from multisig - let addresses = [ZERO_ADDRESS]; - let toggles = [true, false]; - const data = await sovryn.contract.methods.setSupportedTokens(addresses, toggles).encodeABI(); - const tx = await multisig.submitTransaction(sovryn.address, 0, data, { from: accounts[0] }); - let txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); - expectEvent(receipt, "ExecutionFailure"); - }); - - it("should work: setLendingFeePercent", async () => { - /// @dev setLendingFeePercent must be called from multisig - let newValue = new BN(10).pow(new BN(20)); - const data = await sovryn.contract.methods.setLendingFeePercent(newValue).encodeABI(); - const tx = await multisig.submitTransaction(sovryn.address, 0, data, { from: accounts[0] }); - let txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); - expectEvent(receipt, "Execution"); - - // Check emitted event arguments - const decode = decodeLogs(receipt.rawLogs, ProtocolSettings, "SetLendingFeePercent"); - const event = decode[0].args; - expect(event["sender"] == multisig.address).to.be.true; - /// @dev Default value at State.sol: - /// 10% fee /// Fee taken from lender interest payments. - /// uint256 public lendingFeePercent = 10**19; - /// 10000000000000000000 - expect(event["oldValue"] == new BN(1).mul(new BN(10).pow(new BN(19)))).to.be.true; - expect(event["newValue"] == newValue).to.be.true; - }); - - it("shouldn't work: setLendingFeePercent w/ value too high", async () => { - /// @dev setLendingFeePercent must be called from multisig - const data = await sovryn.contract.methods.setLendingFeePercent(new BN(10).pow(new BN(20)).add(new BN(1))).encodeABI(); - const tx = await multisig.submitTransaction(sovryn.address, 0, data, { from: accounts[0] }); - let txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); - expectEvent(receipt, "ExecutionFailure"); - }); - - it("should work: setTradingFeePercent", async () => { - /// @dev setTradingFeePercent must be called from multisig - let newValue = new BN(10).pow(new BN(20)); - const data = await sovryn.contract.methods.setTradingFeePercent(newValue).encodeABI(); - const tx = await multisig.submitTransaction(sovryn.address, 0, data, { from: accounts[0] }); - let txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); - expectEvent(receipt, "Execution"); - - // Check emitted event arguments - const decode = decodeLogs(receipt.rawLogs, ProtocolSettings, "SetTradingFeePercent"); - const event = decode[0].args; - expect(event["sender"] == multisig.address).to.be.true; - /// @dev Default value at State.sol: - /// 0.15% fee /// Fee paid for each trade. - /// uint256 public tradingFeePercent = 15 * 10**16; - /// 150000000000000000 - expect(event["oldValue"] == new BN(15).mul(new BN(10).pow(new BN(16)))).to.be.true; - expect(event["newValue"] == newValue).to.be.true; - }); - - it("shouldn't work: setTradingFeePercent w/ value too high", async () => { - /// @dev setTradingFeePercent must be called from multisig - const data = await sovryn.contract.methods.setTradingFeePercent(new BN(10).pow(new BN(20)).add(new BN(1))).encodeABI(); - const tx = await multisig.submitTransaction(sovryn.address, 0, data, { from: accounts[0] }); - let txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); - expectEvent(receipt, "ExecutionFailure"); - }); - - it("shouldn't revert: setSovrynSwapContractRegistryAddress w/ registryAddress not a contract", async () => { - const sovrynproxy = await sovrynProtocol.new(); - const sovryn = await ISovryn.at(sovrynproxy.address); - - await sovryn.replaceContract((await ProtocolSettings.new()).address); - await sovryn.replaceContract((await LoanSettings.new()).address); - await sovryn.replaceContract((await LoanMaintenance.new()).address); - await sovryn.replaceContract((await SwapsExternal.new()).address); - - await expectRevert(sovryn.setSovrynSwapContractRegistryAddress(ZERO_ADDRESS), "registryAddress not a contract"); - }); - - it("shouldn't work: set RolloverFlexFeePercent w/ value too high", async () => { - const new_percent = new BN(2).mul(oneEth); - const old_percent = await sovryn.rolloverFlexFeePercent(); - - const dest = sovryn.address; - const val = 0; - const data = await sovryn.contract.methods.setRolloverFlexFeePercent(new_percent).encodeABI(); - - const tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); - let txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); - expectEvent(receipt, "ExecutionFailure"); - }); - - // Should successfully change rolloverFlexFeePercent - it("should work: set RolloverFlexFeePercent percent", async () => { - const new_percent = new BN(1).mul(oneEth); - const old_percent = await sovryn.rolloverFlexFeePercent(); - - const dest = sovryn.address; - const val = 0; - const data = await sovryn.contract.methods.setRolloverFlexFeePercent(new_percent).encodeABI(); - - const tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); - let txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; - const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); - - const decode = decodeLogs(receipt.rawLogs, ProtocolSettings, "SetRolloverFlexFeePercent"); - const event = decode[0].args; - expect(event["sender"] == multisig.address).to.be.true; - expect(event["oldRolloverFlexFeePercent"] == old_percent.toString()).to.be.true; - expect(event["newRolloverFlexFeePercent"] == new_percent.toString()).to.be.true; - expect((await sovryn.rolloverFlexFeePercent()).eq(new_percent)).to.be.true; - }); - }); - - describe("LoanClosings test coverage", () => { - it("Doesn't allow fallback function call", async () => { - /// @dev the revert "fallback not allowed" is never reached because - /// fallback function (w/ no signature) is not registered in the protocol - await expectRevert(sovryn.sendTransaction({}), "target not active"); - }); - }); + tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); + txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; + await multisig.confirmTransaction(txId, { from: accounts[1] }); + + data = sovryn.contract.methods.depositProtocolToken(hunEth).encodeABI(); + + tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); + txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; + await multisig.confirmTransaction(txId, { from: accounts[1] }); + + const balanceBefore = await sov.balanceOf(accounts[1]); + + data = sovryn.contract.methods.withdrawProtocolToken(accounts[1], hunEth).encodeABI(); + + tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); + txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; + await multisig.confirmTransaction(txId, { from: accounts[1] }); + + const balanceAfter = await sov.balanceOf(accounts[1]); + expect((await sovryn.protocolTokenHeld()).eq(new BN(0))).to.be.true; + expect(balanceAfter.eq(balanceBefore.add(hunEth))).to.be.true; + }); + + // Should fail to withdraw 1e30 protocol tokens but withdraw 1e20 + it("Test fail withdraw protocol token", async () => { + const dest = sovryn.address; + const val = 0; + + let data = await sov.contract.methods.approve(sovryn.address, hunEth).encodeABI(); + + let tx = await multisig.submitTransaction(sov.address, val, data, { + from: accounts[0], + }); + let txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + await multisig.confirmTransaction(txId, { from: accounts[1] }); + + data = await sovryn.contract.methods.setProtocolTokenAddress(sov.address).encodeABI(); + + tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); + txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; + await multisig.confirmTransaction(txId, { from: accounts[1] }); + + data = sovryn.contract.methods.depositProtocolToken(hunEth).encodeABI(); + + tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); + txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; + await multisig.confirmTransaction(txId, { from: accounts[1] }); + + const balanceBefore = await sov.balanceOf(accounts[1]); + + data = sovryn.contract.methods + .withdrawProtocolToken(accounts[1], new BN(10).pow(new BN(30))) + .encodeABI(); + + tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); + txId = tx.logs.filter((item) => item.event == "Submission")[0].args["transactionId"]; + await multisig.confirmTransaction(txId, { from: accounts[1] }); + + const balanceAfter = await sov.balanceOf(accounts[1]); + expect((await sovryn.protocolTokenHeld()).eq(new BN(0))).to.be.true; + expect(balanceAfter.eq(balanceBefore.add(hunEth))).to.be.true; + }); + + // Should successfully change rollover base reward + it("Test set rollover base reward", async () => { + const new_reward = new BN(10).pow(new BN(15)); + const old_reward = await sovryn.rolloverBaseReward(); + + const dest = sovryn.address; + const val = 0; + const data = await sovryn.contract.methods + .setRolloverBaseReward(new_reward) + .encodeABI(); + + const tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); + let txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); + + const decode = decodeLogs(receipt.rawLogs, ProtocolSettings, "SetRolloverBaseReward"); + const event = decode[0].args; + expect(event["sender"] == multisig.address).to.be.true; + expect(event["oldValue"] == old_reward.toString()).to.be.true; + expect(event["newValue"] == new_reward.toString()).to.be.true; + expect((await sovryn.rolloverBaseReward()).eq(new_reward)).to.be.true; + }); + + // Should fail to change rollover base reward by unauthorized user + it("Test set rollover base reward by unauthorized user", async () => { + await expectRevert( + sovryn.setRolloverBaseReward(new BN(10).pow(new BN(15)), { from: accounts[0] }), + "unauthorized" + ); + }); + + it("Should revert when setting rollover base reward w/ 0 amount", async () => { + const dest = sovryn.address; + const val = 0; + const data = await sovryn.contract.methods + .setRolloverBaseReward(new BN(0)) + .encodeABI(); + + const tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); + let txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); + + expectEvent(receipt, "ExecutionFailure"); + }); + + // Should successfully change rebate percent + it("Test set rebate percent", async () => { + const new_percent = new BN(2).mul(oneEth); + const old_percent = await sovryn.getFeeRebatePercent(); + + const dest = sovryn.address; + const val = 0; + const data = await sovryn.contract.methods.setRebatePercent(new_percent).encodeABI(); + + const tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); + let txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); + + const decode = decodeLogs(receipt.rawLogs, ProtocolSettings, "SetRebatePercent"); + const event = decode[0].args; + expect(event["sender"] == multisig.address).to.be.true; + expect(event["oldRebatePercent"] == old_percent.toString()).to.be.true; + expect(event["newRebatePercent"] == new_percent.toString()).to.be.true; + expect((await sovryn.getFeeRebatePercent()).eq(new_percent)).to.be.true; + }); + + // Should fail to change rebate percent by unauthorized user + it("Test set rebate percent by unauthorized user", async () => { + await expectRevert( + sovryn.setRebatePercent(new BN(2).mul(oneEth), { from: accounts[0] }), + "unauthorized" + ); + }); + + it("Should revert when setting a too high fee rebate", async () => { + const dest = sovryn.address; + const val = 0; + const data = await sovryn.contract.methods + .setRebatePercent(new BN(10).pow(new BN(20)).add(new BN(1))) + .encodeABI(); + + const tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); + let txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); + + expectEvent(receipt, "ExecutionFailure"); + }); + + // Should successfully change rebate percent + it("Test set trading rebate rewards basis point", async () => { + const new_basis_point = new BN(9999); + const old_basis_point = await sovryn.getTradingRebateRewardsBasisPoint(); + + const dest = sovryn.address; + const val = 0; + const data = await sovryn.contract.methods + .setTradingRebateRewardsBasisPoint(new_basis_point) + .encodeABI(); + + const tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); + let txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); + + const decode = decodeLogs( + receipt.rawLogs, + ProtocolSettings, + "SetTradingRebateRewardsBasisPoint" + ); + const event = decode[0].args; + expect(event["sender"] == multisig.address).to.be.true; + expect(event["oldBasisPoint"] == old_basis_point.toString()).to.be.true; + expect(event["newBasisPoint"] == new_basis_point.toString()).to.be.true; + expect((await sovryn.getTradingRebateRewardsBasisPoint()).eq(new_basis_point)).to.be + .true; + }); + + // Should fail to change rebate percent by unauthorized user + it("Test set trading rebate rewards basis point by unauthorized user", async () => { + await expectRevert( + sovryn.setTradingRebateRewardsBasisPoint(new BN(10000), { from: accounts[0] }), + "unauthorized" + ); + }); + + // Should successfully change the swapExternalFeePercent + it("Test set swapExternalFeePercent", async () => { + const new_percent = new BN(2).mul(oneEth); + const old_percent = await sovryn.getSwapExternalFeePercent(); + + const dest = sovryn.address; + const val = 0; + const data = await sovryn.contract.methods + .setSwapExternalFeePercent(new_percent) + .encodeABI(); + + const tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); + let txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); + + const decode = decodeLogs( + receipt.rawLogs, + ProtocolSettings, + "SetSwapExternalFeePercent" + ); + const event = decode[0].args; + expect(event["sender"] == multisig.address).to.be.true; + expect(event["oldValue"] == old_percent.toString()).to.be.true; + expect(event["newValue"] == new_percent.toString()).to.be.true; + expect((await sovryn.getSwapExternalFeePercent()).eq(new_percent)).to.be.true; + }); + + // Should fail to change swap external fee percent by unauthorized user + it("Test set swapExternalFeePercent with unauthorized sender", async () => { + await expectRevert( + sovryn.setSwapExternalFeePercent(new BN(2).mul(oneEth), { from: accounts[0] }), + "unauthorized" + ); + }); + + it("should work: setBorrowingFeePercent", async () => { + /// @dev setBorrowingFeePercent must be called from multisig + let newValue = new BN(10).pow(new BN(20)); + const data = await sovryn.contract.methods + .setBorrowingFeePercent(newValue) + .encodeABI(); + const tx = await multisig.submitTransaction(sovryn.address, 0, data, { + from: accounts[0], + }); + let txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); + expectEvent(receipt, "Execution"); + + // Check emitted event arguments + const decode = decodeLogs(receipt.rawLogs, ProtocolSettings, "SetBorrowingFeePercent"); + const event = decode[0].args; + expect(event["sender"] == multisig.address).to.be.true; + /// @dev Default value at State.sol: + /// 0.09% fee /// Origination fee paid for each loan. + /// uint256 public borrowingFeePercent = 9 * 10**16; + /// 90000000000000000 + expect(event["oldValue"] == new BN(9).mul(new BN(10).pow(new BN(16)))).to.be.true; + expect(event["newValue"] == newValue).to.be.true; + }); + + it("shouldn't work: setBorrowingFeePercent w/ value too high", async () => { + /// @dev setBorrowingFeePercent must be called from multisig + const data = await sovryn.contract.methods + .setBorrowingFeePercent(new BN(10).pow(new BN(20)).add(new BN(1))) + .encodeABI(); + const tx = await multisig.submitTransaction(sovryn.address, 0, data, { + from: accounts[0], + }); + let txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); + expectEvent(receipt, "ExecutionFailure"); + }); + + it("should work: setLiquidationIncentivePercent", async () => { + /// @dev setLiquidationIncentivePercent must be called from multisig + let newValue = new BN(10).pow(new BN(20)); + const data = await sovryn.contract.methods + .setLiquidationIncentivePercent(newValue) + .encodeABI(); + const tx = await multisig.submitTransaction(sovryn.address, 0, data, { + from: accounts[0], + }); + let txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); + expectEvent(receipt, "Execution"); + + // Check emitted event arguments + const decode = decodeLogs( + receipt.rawLogs, + ProtocolSettings, + "SetLiquidationIncentivePercent" + ); + const event = decode[0].args; + expect(event["sender"] == multisig.address).to.be.true; + /// @dev Default value at State.sol: + /// 5% collateral discount /// Discount on collateral for liquidators. + /// uint256 public liquidationIncentivePercent = 5 * 10**18; + /// 5000000000000000000 + expect(event["oldValue"] == new BN(5).mul(new BN(10).pow(new BN(18)))).to.be.true; + expect(event["newValue"] == newValue).to.be.true; + }); + + it("shouldn't work: setLiquidationIncentivePercent w/ value too high", async () => { + /// @dev setLiquidationIncentivePercent must be called from multisig + const data = await sovryn.contract.methods + .setLiquidationIncentivePercent(new BN(10).pow(new BN(20)).add(new BN(1))) + .encodeABI(); + const tx = await multisig.submitTransaction(sovryn.address, 0, data, { + from: accounts[0], + }); + let txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); + expectEvent(receipt, "ExecutionFailure"); + }); + + it("should work: setMaxDisagreement", async () => { + /// @dev setMaxDisagreement must be called from multisig + const data = await sovryn.contract.methods + .setMaxDisagreement(new BN(10).pow(new BN(20))) + .encodeABI(); + const tx = await multisig.submitTransaction(sovryn.address, 0, data, { + from: accounts[0], + }); + let txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); + expectEvent(receipt, "Execution"); + }); + + it("should work: setSourceBuffer", async () => { + /// @dev setSourceBuffer must be called from multisig + const data = await sovryn.contract.methods + .setSourceBuffer(new BN(10).pow(new BN(20))) + .encodeABI(); + const tx = await multisig.submitTransaction(sovryn.address, 0, data, { + from: accounts[0], + }); + let txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); + expectEvent(receipt, "Execution"); + }); + + it("should work: setMaxSwapSize", async () => { + /// @dev setMaxSwapSize must be called from multisig + let newValue = new BN(10).pow(new BN(20)); + const data = await sovryn.contract.methods.setMaxSwapSize(newValue).encodeABI(); + const tx = await multisig.submitTransaction(sovryn.address, 0, data, { + from: accounts[0], + }); + let txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); + expectEvent(receipt, "Execution"); + + // Check emitted event arguments + const decode = decodeLogs(receipt.rawLogs, ProtocolSettings, "SetMaxSwapSize"); + const event = decode[0].args; + expect(event["sender"] == multisig.address).to.be.true; + /// @dev Default value at State.sol: + /// Maximum support swap size in rBTC + /// uint256 public maxSwapSize = 50 ether; + /// 50000000000000000000 + expect(event["oldValue"] == new BN(50).mul(new BN(10).pow(new BN(18)))).to.be.true; + expect(event["newValue"] == newValue).to.be.true; + }); + + it("should work on setLoanPool w/ 1 pool and 1 asset previously deployed", async () => { + /// @dev setLoanPool must be called from multisig + let pools = [loanToken.address]; + let assets = [loanTokenAddress]; + const data = await sovryn.contract.methods.setLoanPool(pools, assets).encodeABI(); + const tx = await multisig.submitTransaction(sovryn.address, 0, data, { + from: accounts[0], + }); + let txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); + expectEvent(receipt, "Execution"); + + let list = await sovryn.getLoanPoolsList.call(0, 10); + console.log("loanPools = ", list); + }); + + it("should revert for count mismatch on setLoanPool w/ 1 pool 2 assets", async () => { + /// @dev setLoanPool must be called from multisig + let pools = [loanToken.address]; + let assets = [loanTokenAddress, loanTokenAddress]; + const data = await sovryn.contract.methods.setLoanPool(pools, assets).encodeABI(); + const tx = await multisig.submitTransaction(sovryn.address, 0, data, { + from: accounts[0], + }); + let txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); + expectEvent(receipt, "ExecutionFailure"); + }); + + it("should revert on setLoanPool w/ 1 pool and 1 asset that are equal", async () => { + /// @dev setLoanPool must be called from multisig + let pools = [loanToken.address]; + let assets = [loanToken.address]; + const data = await sovryn.contract.methods.setLoanPool(pools, assets).encodeABI(); + const tx = await multisig.submitTransaction(sovryn.address, 0, data, { + from: accounts[0], + }); + let txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); + expectEvent(receipt, "ExecutionFailure"); + }); + + it("should revert on setLoanPool w/ 1 pool equal to address(0) and 1 asset", async () => { + /// @dev setLoanPool must be called from multisig + let pools = [ZERO_ADDRESS]; + let assets = [loanToken.address]; + const data = await sovryn.contract.methods.setLoanPool(pools, assets).encodeABI(); + const tx = await multisig.submitTransaction(sovryn.address, 0, data, { + from: accounts[0], + }); + let txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); + expectEvent(receipt, "ExecutionFailure"); + }); + + it("should revert on setLoanPool w/ 1 pool and 1 asset equal to address(0)", async () => { + /// @dev setLoanPool must be called from multisig + let pools = [loanToken.address]; + let assets = [ZERO_ADDRESS]; + const data = await sovryn.contract.methods.setLoanPool(pools, assets).encodeABI(); + const tx = await multisig.submitTransaction(sovryn.address, 0, data, { + from: accounts[0], + }); + let txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); + expectEvent(receipt, "ExecutionFailure"); + }); + + it("should work on setSupportedTokens w/ 1 address and 1 toogle", async () => { + /// @dev setSupportedTokens must be called from multisig + let addresses = [ZERO_ADDRESS]; + let toggles = [true]; + const data = await sovryn.contract.methods + .setSupportedTokens(addresses, toggles) + .encodeABI(); + const tx = await multisig.submitTransaction(sovryn.address, 0, data, { + from: accounts[0], + }); + let txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); + expectEvent(receipt, "Execution"); + }); + + it("should revert for count mismatch on setSupportedTokens w/ 1 address 2 toggles", async () => { + /// @dev setSupportedTokens must be called from multisig + let addresses = [ZERO_ADDRESS]; + let toggles = [true, false]; + const data = await sovryn.contract.methods + .setSupportedTokens(addresses, toggles) + .encodeABI(); + const tx = await multisig.submitTransaction(sovryn.address, 0, data, { + from: accounts[0], + }); + let txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); + expectEvent(receipt, "ExecutionFailure"); + }); + + it("should work: setLendingFeePercent", async () => { + /// @dev setLendingFeePercent must be called from multisig + let newValue = new BN(10).pow(new BN(20)); + const data = await sovryn.contract.methods.setLendingFeePercent(newValue).encodeABI(); + const tx = await multisig.submitTransaction(sovryn.address, 0, data, { + from: accounts[0], + }); + let txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); + expectEvent(receipt, "Execution"); + + // Check emitted event arguments + const decode = decodeLogs(receipt.rawLogs, ProtocolSettings, "SetLendingFeePercent"); + const event = decode[0].args; + expect(event["sender"] == multisig.address).to.be.true; + /// @dev Default value at State.sol: + /// 10% fee /// Fee taken from lender interest payments. + /// uint256 public lendingFeePercent = 10**19; + /// 10000000000000000000 + expect(event["oldValue"] == new BN(1).mul(new BN(10).pow(new BN(19)))).to.be.true; + expect(event["newValue"] == newValue).to.be.true; + }); + + it("shouldn't work: setLendingFeePercent w/ value too high", async () => { + /// @dev setLendingFeePercent must be called from multisig + const data = await sovryn.contract.methods + .setLendingFeePercent(new BN(10).pow(new BN(20)).add(new BN(1))) + .encodeABI(); + const tx = await multisig.submitTransaction(sovryn.address, 0, data, { + from: accounts[0], + }); + let txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); + expectEvent(receipt, "ExecutionFailure"); + }); + + it("should work: setTradingFeePercent", async () => { + /// @dev setTradingFeePercent must be called from multisig + let newValue = new BN(10).pow(new BN(20)); + const data = await sovryn.contract.methods.setTradingFeePercent(newValue).encodeABI(); + const tx = await multisig.submitTransaction(sovryn.address, 0, data, { + from: accounts[0], + }); + let txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); + expectEvent(receipt, "Execution"); + + // Check emitted event arguments + const decode = decodeLogs(receipt.rawLogs, ProtocolSettings, "SetTradingFeePercent"); + const event = decode[0].args; + expect(event["sender"] == multisig.address).to.be.true; + /// @dev Default value at State.sol: + /// 0.15% fee /// Fee paid for each trade. + /// uint256 public tradingFeePercent = 15 * 10**16; + /// 150000000000000000 + expect(event["oldValue"] == new BN(15).mul(new BN(10).pow(new BN(16)))).to.be.true; + expect(event["newValue"] == newValue).to.be.true; + }); + + it("shouldn't work: setTradingFeePercent w/ value too high", async () => { + /// @dev setTradingFeePercent must be called from multisig + const data = await sovryn.contract.methods + .setTradingFeePercent(new BN(10).pow(new BN(20)).add(new BN(1))) + .encodeABI(); + const tx = await multisig.submitTransaction(sovryn.address, 0, data, { + from: accounts[0], + }); + let txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); + expectEvent(receipt, "ExecutionFailure"); + }); + + it("shouldn't revert: setSovrynSwapContractRegistryAddress w/ registryAddress not a contract", async () => { + const sovrynproxy = await sovrynProtocol.new(); + const sovryn = await ISovryn.at(sovrynproxy.address); + + await sovryn.replaceContract((await ProtocolSettings.new()).address); + await sovryn.replaceContract((await LoanSettings.new()).address); + await sovryn.replaceContract((await LoanMaintenance.new()).address); + await sovryn.replaceContract((await SwapsExternal.new()).address); + + await expectRevert( + sovryn.setSovrynSwapContractRegistryAddress(ZERO_ADDRESS), + "registryAddress not a contract" + ); + }); + + it("shouldn't work: set RolloverFlexFeePercent w/ value too high", async () => { + const new_percent = new BN(2).mul(oneEth); + const old_percent = await sovryn.rolloverFlexFeePercent(); + + const dest = sovryn.address; + const val = 0; + const data = await sovryn.contract.methods + .setRolloverFlexFeePercent(new_percent) + .encodeABI(); + + const tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); + let txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); + expectEvent(receipt, "ExecutionFailure"); + }); + + // Should successfully change rolloverFlexFeePercent + it("should work: set RolloverFlexFeePercent percent", async () => { + const new_percent = new BN(1).mul(oneEth); + const old_percent = await sovryn.rolloverFlexFeePercent(); + + const dest = sovryn.address; + const val = 0; + const data = await sovryn.contract.methods + .setRolloverFlexFeePercent(new_percent) + .encodeABI(); + + const tx = await multisig.submitTransaction(dest, val, data, { from: accounts[0] }); + let txId = tx.logs.filter((item) => item.event == "Submission")[0].args[ + "transactionId" + ]; + const { receipt } = await multisig.confirmTransaction(txId, { from: accounts[1] }); + + const decode = decodeLogs( + receipt.rawLogs, + ProtocolSettings, + "SetRolloverFlexFeePercent" + ); + const event = decode[0].args; + expect(event["sender"] == multisig.address).to.be.true; + expect(event["oldRolloverFlexFeePercent"] == old_percent.toString()).to.be.true; + expect(event["newRolloverFlexFeePercent"] == new_percent.toString()).to.be.true; + expect((await sovryn.rolloverFlexFeePercent()).eq(new_percent)).to.be.true; + }); + }); + + describe("LoanClosings test coverage", () => { + it("Doesn't allow fallback function call", async () => { + /// @dev the revert "fallback not allowed" is never reached because + /// fallback function (w/ no signature) is not registered in the protocol + await expectRevert(sovryn.sendTransaction({}), "target not active"); + }); + }); }); diff --git a/tests/price-feeds/PriceFeedOracleV1Pool.js b/tests/price-feeds/PriceFeedOracleV1Pool.js index 48676b945..a40c660d2 100644 --- a/tests/price-feeds/PriceFeedOracleV1Pool.js +++ b/tests/price-feeds/PriceFeedOracleV1Pool.js @@ -33,217 +33,306 @@ const PriceFeedRSKOracle = artifacts.require("PriceFeedRSKOracle"); const PriceFeedRSKOracleMockup = artifacts.require("PriceFeedRSKOracleMockup"); const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getLoanTokenLogic, - getLoanToken, - getLoanTokenLogicWrbtc, - getLoanTokenWRBTC, - loan_pool_setup, - set_demand_curve, - getPriceFeeds, - getSovryn, - decodeLogs, - getSOV, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getLoanTokenLogic, + getLoanToken, + getLoanTokenLogicWrbtc, + getLoanTokenWRBTC, + loan_pool_setup, + set_demand_curve, + getPriceFeeds, + getSovryn, + decodeLogs, + getSOV, } = require("../Utils/initializer.js"); contract("PriceFeedOracleV1Pool", (accounts) => { - let loanTokenLogic; - let WRBTC; - let doc; - let SOV; - let sovryn; - let testToken1; - let wei = web3.utils.toWei; - let senderMock; - let priceFeedsV1PoolOracleTestToken1; - - async function deploymentAndInitFixture(_wallets, _provider) { - const provider = waffle.provider; - [senderMock] = provider.getWallets(); - - // Deploying sovrynProtocol w/ generic function from initializer.js - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - await sovryn.setSovrynProtocolAddress(sovryn.address); - - // Custom tokens - SOV = await getSOV(sovryn, priceFeeds, SUSD, accounts); - - loanTokenLogic = await LoanTokenLogicLM.new(); - doc = await TestToken.new("dollar on chain", "DOC", 18, wei("20000", "ether")); - loanToken = await LoanToken.new(owner, loanTokenLogic.address, sovryn.address, WRBTC.address); - await loanToken.initialize(doc.address, "SUSD", "SUSD"); - - // Overwritting priceFeeds - priceFeeds = await PriceFeeds.new(WRBTC.address, SOV.address, doc.address); - testToken1Precision = 18; - testToken2Precision = 18; - btcPrecision = 18; - testToken1 = await TestToken.new("test token 1", "TEST1", testToken1Precision, wei("20000", "ether")); - testToken1Price = wei("2", "ether"); - - // Set v1 convert mockup - liquidityV1ConverterMockupTestToken1 = await LiquidityPoolV1ConverterMockup.new(testToken1.address, WRBTC.address); - - priceFeedsV1PoolOracleMockupTestToken1 = await deployMockContract(senderMock, IV1PoolOracle.abi); - await priceFeedsV1PoolOracleMockupTestToken1.mock.latestAnswer.returns(testToken1Price); - await priceFeedsV1PoolOracleMockupTestToken1.mock.latestPrice.returns(testToken1Price); - await priceFeedsV1PoolOracleMockupTestToken1.mock.liquidityPool.returns(liquidityV1ConverterMockupTestToken1.address); - - priceFeedsV1PoolOracleTestToken1 = await PriceFeedV1PoolOracle.new( - priceFeedsV1PoolOracleMockupTestToken1.address, - WRBTC.address, - doc.address, - testToken1.address - ); - } - - before(async () => { - [owner, trader, referrer, account1, account2, ...accounts] = accounts; - }); - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - describe("PriceFeedOracleV1Pool unit tests", async () => { - it("set base currency should revert if set with zero address", async () => { - await expectRevert( - priceFeedsV1PoolOracleTestToken1.setBaseCurrency(constants.ZERO_ADDRESS), - "Base currency address cannot be zero address" - ); - }); - - it("set base currency should revert if set with unauthorized user", async () => { - await expectRevert(priceFeedsV1PoolOracleTestToken1.setBaseCurrency(sovryn.address, { from: accounts[2] }), "unauthorized"); - }); - - it("set base currency success", async () => { - await priceFeedsV1PoolOracleTestToken1.setBaseCurrency(sovryn.address); - expect(await priceFeedsV1PoolOracleTestToken1.baseCurrency()).to.be.equal(sovryn.address); - }); - - it("set doc address should revert if set with zero address", async () => { - await expectRevert( - priceFeedsV1PoolOracleTestToken1.setDOCAddress(constants.ZERO_ADDRESS), - "DOC address cannot be zero address" - ); - }); - - it("set doc address should revert if set with unauthorized user", async () => { - await expectRevert(priceFeedsV1PoolOracleTestToken1.setDOCAddress(sovryn.address, { from: accounts[2] }), "unauthorized"); - }); - - it("set doc address success", async () => { - await priceFeedsV1PoolOracleTestToken1.setDOCAddress(sovryn.address); - expect(await priceFeedsV1PoolOracleTestToken1.docAddress()).to.be.equal(sovryn.address); - }); - - it("set wRBTC address should revert if set with zero address", async () => { - await expectRevert( - priceFeedsV1PoolOracleTestToken1.setRBTCAddress(constants.ZERO_ADDRESS), - "wRBTC address cannot be zero address" - ); - }); - - it("set wRBTC address should revert if set with unauthorized user", async () => { - await expectRevert(priceFeedsV1PoolOracleTestToken1.setRBTCAddress(sovryn.address, { from: accounts[2] }), "unauthorized"); - }); - - it("set wRBTC address success", async () => { - await priceFeedsV1PoolOracleTestToken1.setRBTCAddress(sovryn.address); - expect(await priceFeedsV1PoolOracleTestToken1.wRBTCAddress()).to.be.equal(sovryn.address); - }); - - it("set v1PoolOracleAddress should revert if set with non-contract address", async () => { - await expectRevert( - priceFeedsV1PoolOracleTestToken1.setV1PoolOracleAddress(constants.ZERO_ADDRESS), - "_v1PoolOracleAddress not a contract" - ); - }); - - it("set v1PoolOracleAddress should revert if set with unauthorized user", async () => { - await expectRevert( - priceFeedsV1PoolOracleTestToken1.setV1PoolOracleAddress(sovryn.address, { from: accounts[2] }), - "unauthorized" - ); - }); - - it("set v1PoolOracleAddress address should revert if one of the reserve tokens is wrbtc address", async () => { - liquidityV1ConverterMockupBTC = await LiquidityPoolV1ConverterMockup.new(testToken1.address, SOV.address); - priceFeedsV1PoolOracleMockupTestToken2 = await deployMockContract(senderMock, IV1PoolOracle.abi); - await priceFeedsV1PoolOracleMockupTestToken2.mock.latestAnswer.returns(testToken1Price); - await priceFeedsV1PoolOracleMockupTestToken2.mock.latestPrice.returns(testToken1Price); - await priceFeedsV1PoolOracleMockupTestToken2.mock.liquidityPool.returns(liquidityV1ConverterMockupBTC.address); - - await expectRevert( - priceFeedsV1PoolOracleTestToken1.setV1PoolOracleAddress(priceFeedsV1PoolOracleMockupTestToken2.address), - "one of the two reserves needs to be wrbtc" - ); - }); - - it("set v1PoolOracleAddress address success", async () => { - priceFeedsV1PoolOracleMockupTestToken2 = await deployMockContract(senderMock, IV1PoolOracle.abi); - await priceFeedsV1PoolOracleMockupTestToken2.mock.latestAnswer.returns(testToken1Price); - await priceFeedsV1PoolOracleMockupTestToken2.mock.latestPrice.returns(testToken1Price); - await priceFeedsV1PoolOracleMockupTestToken2.mock.liquidityPool.returns(liquidityV1ConverterMockupTestToken1.address); - - await priceFeedsV1PoolOracleTestToken1.setV1PoolOracleAddress(priceFeedsV1PoolOracleMockupTestToken2.address); - expect(await priceFeedsV1PoolOracleTestToken1.v1PoolOracleAddress()).to.be.equal( - priceFeedsV1PoolOracleMockupTestToken2.address - ); - }); - - it("Should revert if price in usd is 0", async () => { - const wrBTCPrice = wei("8", "ether"); - const docPrice = wei("7", "ether"); - const testToken2 = await TestToken.new("test token 2", "TEST2", testToken2Precision, wei("20000", "ether")); - priceFeedsV1PoolOracleMockupTestToken2 = await deployMockContract(senderMock, IV1PoolOracle.abi); - await priceFeedsV1PoolOracleMockupTestToken2.mock.latestAnswer.returns(0); - await priceFeedsV1PoolOracleMockupTestToken2.mock.latestPrice.returns(0); - await priceFeedsV1PoolOracleMockupTestToken2.mock.liquidityPool.returns(liquidityV1ConverterMockupTestToken1.address); - - priceFeedsV1PoolOracleTestToken2 = await PriceFeedV1PoolOracle.new( - priceFeedsV1PoolOracleMockupTestToken2.address, - WRBTC.address, - doc.address, - testToken2.address - ); - - // // Set rBTC feed - using rsk oracle - priceFeedsV1PoolOracleMockupBTC = await PriceFeedRSKOracleMockup.new(); - await priceFeedsV1PoolOracleMockupBTC.setValue(wrBTCPrice); - priceFeedsV1PoolOracleBTC = await PriceFeedRSKOracle.new(priceFeedsV1PoolOracleMockupBTC.address); - - // Set DOC feed -- price 1 BTC - liquidityV1ConverterMockupDOC = await LiquidityPoolV1ConverterMockup.new(doc.address, WRBTC.address); - - priceFeedsV1PoolOracleMockupDOC = await deployMockContract(senderMock, IV1PoolOracle.abi); - await priceFeedsV1PoolOracleMockupDOC.mock.latestAnswer.returns(docPrice); - await priceFeedsV1PoolOracleMockupDOC.mock.latestPrice.returns(docPrice); - await priceFeedsV1PoolOracleMockupDOC.mock.liquidityPool.returns(liquidityV1ConverterMockupDOC.address); - - priceFeedsV1PoolOracleDOC = await PriceFeedV1PoolOracle.new( - priceFeedsV1PoolOracleMockupDOC.address, - WRBTC.address, - doc.address, - doc.address - ); - - // await priceFeeds.setPriceFeed([WRBTC.address, doc.address], [priceFeedsV1PoolOracle.address, priceFeedsV1PoolOracle.address]) - await priceFeeds.setPriceFeed( - [testToken2.address, doc.address, WRBTC.address], - [priceFeedsV1PoolOracleTestToken2.address, priceFeedsV1PoolOracleDOC.address, priceFeedsV1PoolOracleBTC.address] - ); - - await expectRevert(priceFeeds.queryRate(testToken2.address, doc.address), "price error"); - }); - }); + let loanTokenLogic; + let WRBTC; + let doc; + let SOV; + let sovryn; + let testToken1; + let wei = web3.utils.toWei; + let senderMock; + let priceFeedsV1PoolOracleTestToken1; + + async function deploymentAndInitFixture(_wallets, _provider) { + const provider = waffle.provider; + [senderMock] = provider.getWallets(); + + // Deploying sovrynProtocol w/ generic function from initializer.js + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + await sovryn.setSovrynProtocolAddress(sovryn.address); + + // Custom tokens + SOV = await getSOV(sovryn, priceFeeds, SUSD, accounts); + + loanTokenLogic = await LoanTokenLogicLM.new(); + doc = await TestToken.new("dollar on chain", "DOC", 18, wei("20000", "ether")); + loanToken = await LoanToken.new( + owner, + loanTokenLogic.address, + sovryn.address, + WRBTC.address + ); + await loanToken.initialize(doc.address, "SUSD", "SUSD"); + + // Overwritting priceFeeds + priceFeeds = await PriceFeeds.new(WRBTC.address, SOV.address, doc.address); + testToken1Precision = 18; + testToken2Precision = 18; + btcPrecision = 18; + testToken1 = await TestToken.new( + "test token 1", + "TEST1", + testToken1Precision, + wei("20000", "ether") + ); + testToken1Price = wei("2", "ether"); + + // Set v1 convert mockup + liquidityV1ConverterMockupTestToken1 = await LiquidityPoolV1ConverterMockup.new( + testToken1.address, + WRBTC.address + ); + + priceFeedsV1PoolOracleMockupTestToken1 = await deployMockContract( + senderMock, + IV1PoolOracle.abi + ); + await priceFeedsV1PoolOracleMockupTestToken1.mock.latestAnswer.returns(testToken1Price); + await priceFeedsV1PoolOracleMockupTestToken1.mock.latestPrice.returns(testToken1Price); + await priceFeedsV1PoolOracleMockupTestToken1.mock.liquidityPool.returns( + liquidityV1ConverterMockupTestToken1.address + ); + + priceFeedsV1PoolOracleTestToken1 = await PriceFeedV1PoolOracle.new( + priceFeedsV1PoolOracleMockupTestToken1.address, + WRBTC.address, + doc.address, + testToken1.address + ); + } + + before(async () => { + [owner, trader, referrer, account1, account2, ...accounts] = accounts; + }); + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + describe("PriceFeedOracleV1Pool unit tests", async () => { + it("set base currency should revert if set with zero address", async () => { + await expectRevert( + priceFeedsV1PoolOracleTestToken1.setBaseCurrency(constants.ZERO_ADDRESS), + "Base currency address cannot be zero address" + ); + }); + + it("set base currency should revert if set with unauthorized user", async () => { + await expectRevert( + priceFeedsV1PoolOracleTestToken1.setBaseCurrency(sovryn.address, { + from: accounts[2], + }), + "unauthorized" + ); + }); + + it("set base currency success", async () => { + await priceFeedsV1PoolOracleTestToken1.setBaseCurrency(sovryn.address); + expect(await priceFeedsV1PoolOracleTestToken1.baseCurrency()).to.be.equal( + sovryn.address + ); + }); + + it("set doc address should revert if set with zero address", async () => { + await expectRevert( + priceFeedsV1PoolOracleTestToken1.setDOCAddress(constants.ZERO_ADDRESS), + "DOC address cannot be zero address" + ); + }); + + it("set doc address should revert if set with unauthorized user", async () => { + await expectRevert( + priceFeedsV1PoolOracleTestToken1.setDOCAddress(sovryn.address, { + from: accounts[2], + }), + "unauthorized" + ); + }); + + it("set doc address success", async () => { + await priceFeedsV1PoolOracleTestToken1.setDOCAddress(sovryn.address); + expect(await priceFeedsV1PoolOracleTestToken1.docAddress()).to.be.equal( + sovryn.address + ); + }); + + it("set wRBTC address should revert if set with zero address", async () => { + await expectRevert( + priceFeedsV1PoolOracleTestToken1.setRBTCAddress(constants.ZERO_ADDRESS), + "wRBTC address cannot be zero address" + ); + }); + + it("set wRBTC address should revert if set with unauthorized user", async () => { + await expectRevert( + priceFeedsV1PoolOracleTestToken1.setRBTCAddress(sovryn.address, { + from: accounts[2], + }), + "unauthorized" + ); + }); + + it("set wRBTC address success", async () => { + await priceFeedsV1PoolOracleTestToken1.setRBTCAddress(sovryn.address); + expect(await priceFeedsV1PoolOracleTestToken1.wRBTCAddress()).to.be.equal( + sovryn.address + ); + }); + + it("set v1PoolOracleAddress should revert if set with non-contract address", async () => { + await expectRevert( + priceFeedsV1PoolOracleTestToken1.setV1PoolOracleAddress(constants.ZERO_ADDRESS), + "_v1PoolOracleAddress not a contract" + ); + }); + + it("set v1PoolOracleAddress should revert if set with unauthorized user", async () => { + await expectRevert( + priceFeedsV1PoolOracleTestToken1.setV1PoolOracleAddress(sovryn.address, { + from: accounts[2], + }), + "unauthorized" + ); + }); + + it("set v1PoolOracleAddress address should revert if one of the reserve tokens is wrbtc address", async () => { + liquidityV1ConverterMockupBTC = await LiquidityPoolV1ConverterMockup.new( + testToken1.address, + SOV.address + ); + priceFeedsV1PoolOracleMockupTestToken2 = await deployMockContract( + senderMock, + IV1PoolOracle.abi + ); + await priceFeedsV1PoolOracleMockupTestToken2.mock.latestAnswer.returns( + testToken1Price + ); + await priceFeedsV1PoolOracleMockupTestToken2.mock.latestPrice.returns(testToken1Price); + await priceFeedsV1PoolOracleMockupTestToken2.mock.liquidityPool.returns( + liquidityV1ConverterMockupBTC.address + ); + + await expectRevert( + priceFeedsV1PoolOracleTestToken1.setV1PoolOracleAddress( + priceFeedsV1PoolOracleMockupTestToken2.address + ), + "one of the two reserves needs to be wrbtc" + ); + }); + + it("set v1PoolOracleAddress address success", async () => { + priceFeedsV1PoolOracleMockupTestToken2 = await deployMockContract( + senderMock, + IV1PoolOracle.abi + ); + await priceFeedsV1PoolOracleMockupTestToken2.mock.latestAnswer.returns( + testToken1Price + ); + await priceFeedsV1PoolOracleMockupTestToken2.mock.latestPrice.returns(testToken1Price); + await priceFeedsV1PoolOracleMockupTestToken2.mock.liquidityPool.returns( + liquidityV1ConverterMockupTestToken1.address + ); + + await priceFeedsV1PoolOracleTestToken1.setV1PoolOracleAddress( + priceFeedsV1PoolOracleMockupTestToken2.address + ); + expect(await priceFeedsV1PoolOracleTestToken1.v1PoolOracleAddress()).to.be.equal( + priceFeedsV1PoolOracleMockupTestToken2.address + ); + }); + + it("Should revert if price in usd is 0", async () => { + const wrBTCPrice = wei("8", "ether"); + const docPrice = wei("7", "ether"); + const testToken2 = await TestToken.new( + "test token 2", + "TEST2", + testToken2Precision, + wei("20000", "ether") + ); + priceFeedsV1PoolOracleMockupTestToken2 = await deployMockContract( + senderMock, + IV1PoolOracle.abi + ); + await priceFeedsV1PoolOracleMockupTestToken2.mock.latestAnswer.returns(0); + await priceFeedsV1PoolOracleMockupTestToken2.mock.latestPrice.returns(0); + await priceFeedsV1PoolOracleMockupTestToken2.mock.liquidityPool.returns( + liquidityV1ConverterMockupTestToken1.address + ); + + priceFeedsV1PoolOracleTestToken2 = await PriceFeedV1PoolOracle.new( + priceFeedsV1PoolOracleMockupTestToken2.address, + WRBTC.address, + doc.address, + testToken2.address + ); + + // // Set rBTC feed - using rsk oracle + priceFeedsV1PoolOracleMockupBTC = await PriceFeedRSKOracleMockup.new(); + await priceFeedsV1PoolOracleMockupBTC.setValue(wrBTCPrice); + priceFeedsV1PoolOracleBTC = await PriceFeedRSKOracle.new( + priceFeedsV1PoolOracleMockupBTC.address + ); + + // Set DOC feed -- price 1 BTC + liquidityV1ConverterMockupDOC = await LiquidityPoolV1ConverterMockup.new( + doc.address, + WRBTC.address + ); + + priceFeedsV1PoolOracleMockupDOC = await deployMockContract( + senderMock, + IV1PoolOracle.abi + ); + await priceFeedsV1PoolOracleMockupDOC.mock.latestAnswer.returns(docPrice); + await priceFeedsV1PoolOracleMockupDOC.mock.latestPrice.returns(docPrice); + await priceFeedsV1PoolOracleMockupDOC.mock.liquidityPool.returns( + liquidityV1ConverterMockupDOC.address + ); + + priceFeedsV1PoolOracleDOC = await PriceFeedV1PoolOracle.new( + priceFeedsV1PoolOracleMockupDOC.address, + WRBTC.address, + doc.address, + doc.address + ); + + // await priceFeeds.setPriceFeed([WRBTC.address, doc.address], [priceFeedsV1PoolOracle.address, priceFeedsV1PoolOracle.address]) + await priceFeeds.setPriceFeed( + [testToken2.address, doc.address, WRBTC.address], + [ + priceFeedsV1PoolOracleTestToken2.address, + priceFeedsV1PoolOracleDOC.address, + priceFeedsV1PoolOracleBTC.address, + ] + ); + + await expectRevert( + priceFeeds.queryRate(testToken2.address, doc.address), + "price error" + ); + }); + }); }); diff --git a/tests/price-feeds/PriceFeeds.js b/tests/price-feeds/PriceFeeds.js index dc81e7e8e..89b4202b4 100644 --- a/tests/price-feeds/PriceFeeds.js +++ b/tests/price-feeds/PriceFeeds.js @@ -23,328 +23,386 @@ const wei = web3.utils.toWei; const TOTAL_SUPPLY = "10000000000000000000000000"; contract("PriceFeeds", (accounts) => { - let priceFeeds; - let sender, receiver; - let testWrbtc, doc; - - before(async () => { - [sender, receiver] = await ethers.getSigners(); - tokenSOV = await SOV.new(TOTAL_SUPPLY); - - testToken1 = await TestToken.new("test token 1", "TEST1", 18, wei("20000", "ether")); - testToken2 = await TestToken.new("test token 2", "TEST2", 16, wei("20000", "ether")); - testToken1Price = wei("60", "ether"); - testToken2Price = wei("2", "ether"); - docPrice = wei("10", "ether"); - wrBTCPrice = wei("10", "ether"); - - testWrbtc = await TestWrbtc.new(); - doc = await TestToken.new("dollar on chain", "DOC", 18, wei("20000", "ether")); - priceFeeds = await PriceFeeds.new(testWrbtc.address, tokenSOV.address, doc.address); - - liquidityV1ConverterMockupTestToken1 = await LiquidityPoolV1ConverterMockup.new(testToken1.address, testWrbtc.address); - priceFeedsV1PoolOracleMockupTestToken1 = await waffle.deployMockContract(sender, IV1PoolOracle.abi); - await priceFeedsV1PoolOracleMockupTestToken1.mock.latestAnswer.returns(testToken1Price); - await priceFeedsV1PoolOracleMockupTestToken1.mock.latestPrice.returns(testToken1Price); - await priceFeedsV1PoolOracleMockupTestToken1.mock.liquidityPool.returns(liquidityV1ConverterMockupTestToken1.address); - priceFeedsV1PoolOracleTestToken1 = await PriceFeedV1PoolOracle.new( - priceFeedsV1PoolOracleMockupTestToken1.address, - testWrbtc.address, - doc.address, - testToken1.address - ); - - liquidityV1ConverterMockupTestToken2 = await LiquidityPoolV1ConverterMockup.new(testToken2.address, testWrbtc.address); - priceFeedsV1PoolOracleMockupTestToken2 = await waffle.deployMockContract(sender, IV1PoolOracle.abi); - await priceFeedsV1PoolOracleMockupTestToken2.mock.latestAnswer.returns(testToken2Price); - await priceFeedsV1PoolOracleMockupTestToken2.mock.latestPrice.returns(testToken2Price); - await priceFeedsV1PoolOracleMockupTestToken2.mock.liquidityPool.returns(liquidityV1ConverterMockupTestToken2.address); - priceFeedsV1PoolOracleTestToken2 = await PriceFeedV1PoolOracle.new( - priceFeedsV1PoolOracleMockupTestToken2.address, - testWrbtc.address, - doc.address, - testToken2.address - ); - - // Set DOC feed -- price 1 BTC - liquidityV1ConverterMockupDOC = await LiquidityPoolV1ConverterMockup.new(doc.address, testWrbtc.address); - priceFeedsV1PoolOracleMockupDOC = await waffle.deployMockContract(sender, IV1PoolOracle.abi); - await priceFeedsV1PoolOracleMockupDOC.mock.latestAnswer.returns(docPrice); - await priceFeedsV1PoolOracleMockupDOC.mock.latestPrice.returns(docPrice); - await priceFeedsV1PoolOracleMockupDOC.mock.liquidityPool.returns(liquidityV1ConverterMockupDOC.address); - priceFeedsV1PoolOracleDOC = await PriceFeedV1PoolOracle.new( - priceFeedsV1PoolOracleMockupDOC.address, - testWrbtc.address, - doc.address, - doc.address - ); - - // Set rBTC feed - using rsk oracle - priceFeedsV1PoolOracleMockupBTC = await PriceFeedRSKOracleMockup.new(); - await priceFeedsV1PoolOracleMockupBTC.setValue(wrBTCPrice); - priceFeedsV1PoolOracleBTC = await PriceFeedRSKOracle.new(priceFeedsV1PoolOracleMockupBTC.address); - - await priceFeeds.setPriceFeed( - [testToken1.address, testToken2.address, doc.address, testWrbtc.address], - [ - priceFeedsV1PoolOracleTestToken1.address, - priceFeedsV1PoolOracleTestToken2.address, - priceFeedsV1PoolOracleDOC.address, - priceFeedsV1PoolOracleBTC.address, - ] - ); - - await priceFeeds.setDecimals([testToken1.address, testToken2.address]); - }); - - describe("PriceFeed unit tests", async () => { - it("amountInEth for wRBTC token should return passes amount", async () => { - const wrbtcToken = await priceFeeds.wrbtcToken(); - await expect(await priceFeeds.amountInEth(wrbtcToken, new BN(wei("100", "ether")))).to.be.bignumber.equal( - new BN(wei("100", "ether")) - ); - }); - - it("getMaxDrawdown collateral token == loan token & comibed > collateral", async () => { - await expect( - await priceFeeds.getMaxDrawdown( - testToken1.address, - testToken1.address, - new BN(wei("100", "ether")), - new BN(wei("100", "ether")), - new BN(20).mul(new BN(10).pow(new BN(18))) - ) - ).to.be.bignumber.equal(new BN(0)); - }); - - it("getMaxDrawdown collateral token == loan token & collateral > combined", async () => { - await expect( - await priceFeeds.getMaxDrawdown( - testToken1.address, - testToken1.address, - new BN(wei("100", "ether")), - new BN(wei("150", "ether")), - new BN(20).mul(new BN(10).pow(new BN(18))) - ) - ).to.be.bignumber.equal(new BN(wei("30", "ether"))); - }); - - it("getMaxDrawdown collateral token != loan token", async () => { - await expect( - await priceFeeds.getMaxDrawdown( - testToken1.address, //mock loan token address - testToken2.address, //mock collateral token address - new BN(wei("100", "ether")), //loan token amount - new BN(wei("150", "ether")), //collateral token amount - new BN(20).mul(new BN(10).pow(new BN(18))) - ) - ).to.be.bignumber.equal(new BN(wei("114", "ether"))); - - await expect( - await priceFeeds.getMaxDrawdown( - testToken1.address, - testToken2.address, - new BN(wei("100", "ether")), - new BN(wei("30", "ether")), // <36 - new BN(20).mul(new BN(wei("1", "ether"))) - ) - ).to.be.bignumber.equal(new BN(0)); - }); - - it("getMaxDrawdown - unsupported src feed", async () => { - await expectRevert( - priceFeeds.getMaxDrawdown( - accounts[9], //mock loan token address - testToken2.address, //mock collateral token address - new BN(wei("100", "ether")), //loan token amount - new BN(wei("150", "ether")), //collateral token amount - new BN(20).mul(new BN(10).pow(new BN(18))) - ), - "unsupported src feed" - ); - }); - - it("getMaxDrawdown - unsupported dst feed", async () => { - await expectRevert( - priceFeeds.getMaxDrawdown( - testToken1.address, //mock loan token address - accounts[9], //mock collateral token address - new BN(wei("100", "ether")), //loan token amount - new BN(wei("150", "ether")), //collateral token amount - new BN(20).mul(new BN(10).pow(new BN(18))) - ), - "unsupported dst feed" - ); - }); - - it("getCurrentMargin collateralToken == loanToken", async () => { - let ret = await priceFeeds.getCurrentMargin( - testToken1.address, //mock loan token address - testToken1.address, //mock collateral token address - new BN(wei("100", "ether")), //loan token amount - new BN(wei("150", "ether")) //collateral token amount - ); - await expect(ret[0]).to.be.bignumber.equal(new BN(wei("50", "ether"))); - await expect(ret[1]).to.be.bignumber.equal(new BN(wei("1", "ether"))); - - ret = await priceFeeds.getCurrentMargin( - testToken1.address, //mock loan token address - testToken1.address, //mock collateral token address - new BN(wei("100", "ether")), //loan token amount - new BN(wei("30", "ether")) //collateral token amount - ); - await expect(ret[0]).to.be.bignumber.equal(new BN(0)); - await expect(ret[1]).to.be.bignumber.equal(new BN(wei("1", "ether"))); - - ret = await priceFeeds.getCurrentMargin( - testToken1.address, //mock loan token address - testToken1.address, //mock collateral token address - new BN(wei("0", "ether")), //loan token amount - new BN(wei("30", "ether")) //collateral token amount - ); - await expect(ret[0]).to.be.bignumber.equal(new BN(0)); - await expect(ret[1]).to.be.bignumber.equal(new BN(wei("1", "ether"))); - }); - - it("getCurrentMarginAndCollateralSize", async () => { - let ret = await priceFeeds.getCurrentMarginAndCollateralSize( - testToken1.address, - testToken2.address, - new BN(wei("100", "ether")), //loan token amount - new BN(wei("100", "ether")) //collateral token amount - ); - await expect(ret[0]).to.be.bignumber.equal("233333333333333330000"); - await expect(ret[1]).to.be.bignumber.equal(new BN(wei("20000", "ether"))); // we are testing using 16 decimals for testToken2, so we need to multiply 10^2 for the expectation - }); - - it("shouldLiquidate(...) runs correctly", async () => { - let ret = await priceFeeds.shouldLiquidate( - testToken1.address, //mock loan token address - testToken1.address, //mock collateral token address - new BN(wei("100", "ether")), //loan token amount - new BN(wei("150", "ether")), //collateral token amount - new BN(10).mul(new BN(wei("1", "ether"))) //maintenance margin threshold 10% - ); - expect(ret).to.be.false; //50<=10 - - ret = await priceFeeds.shouldLiquidate( - testToken1.address, - testToken2.address, - new BN(wei("100", "ether")), //loan token amount - new BN(wei("450", "ether")), //collateral token amount - new BN(30).mul(new BN(wei("1", "ether"))) //maintenance margin - ); - expect(ret).to.be.false; //1399<=30 - - ret = await priceFeeds.shouldLiquidate( - testToken1.address, - testToken2.address, - new BN(wei("100", "ether")), //loan token amount - new BN(wei("450", "ether")), //collateral token amount - new BN(1500).mul(new BN(wei("1", "ether"))) //maintenance margin - ); - expect(ret).to.be.true; //1399<=1500 - }); - - it("setProtocolTokenEthPrice runs correctly", async () => { - expect(await priceFeeds.protocolTokenEthPrice()).to.be.bignumber.equal(new BN(2).mul(new BN(10).pow(new BN(14)))); - - await priceFeeds.setProtocolTokenEthPrice(wei("10", "ether")); - expect(await priceFeeds.protocolTokenEthPrice()).to.be.bignumber.equal(new BN(wei("10", "ether"))); - - await expectRevert(priceFeeds.setProtocolTokenEthPrice(new BN(0)), "invalid price"); - await expectRevert(priceFeeds.setProtocolTokenEthPrice(wei("10", "ether"), { from: accounts[10] }), "unauthorized"); - }); - - it("setGlobalPricingPaused", async () => { - let isPaused = await priceFeeds.globalPricingPaused(); - await priceFeeds.setGlobalPricingPaused(true); - expect(await priceFeeds.globalPricingPaused()).to.not.equal(isPaused); - await priceFeeds.setGlobalPricingPaused(false); - expect(await priceFeeds.globalPricingPaused()).to.equal(isPaused); - }); - - it("setDecimals", async () => { - testTokens = []; - let decimals = []; - for (let i = 0; i < 3; i++) { - decimals[i] = Math.round(Math.random() * 10 + 8); - testTokens[i] = await getTestToken({ decimals: decimals[i] }); - } - await priceFeeds.setDecimals(testTokens.map((item) => item.address)); - for (i = 0; i < 3; i++) { - expect((await priceFeeds.decimals(await testTokens[i].address)).toNumber()).to.be.eq(decimals[i]); - } - }); - - it("_queryRate internal", async () => { - testTokens = []; - let decimals = []; - let addresses = []; - for (let i = 0; i < 2; i++) { - decimals[i] = (i + 1) * 10 + i; - testTokens[i] = await getTestToken({ decimals: decimals[i] }); - addresses[i] = testTokens[i].address; - } - expect(await priceFeeds.queryPrecision(addresses[0], addresses[0])).to.be.bignumber.equal(new BN(wei("1", "ether"))); - expect(await priceFeeds.queryPrecision(addresses[1], addresses[1])).to.be.bignumber.equal(new BN(wei("1", "ether"))); - //source decimals > dest decimals - expect(await priceFeeds.queryPrecision(addresses[1], addresses[0])).to.be.bignumber.equal(new BN(10).pow(new BN(29))); //10^7 (18+diff) - //source decimals < dest decimals - expect(await priceFeeds.queryPrecision(addresses[0], addresses[1])).to.be.bignumber.equal(new BN(10).pow(new BN(7))); //10^29 (18-diff) - }); - - it("queryPrecision source token = dest token", async () => { - expect(await priceFeeds.queryPrecision(testToken1.address, testToken1.address)).to.be.bignumber.equal( - new BN(wei("1", "ether")) - ); - }); - - it("should return destination token amount", async () => { - await expect( - await priceFeeds.queryReturn(testToken1.address, testToken2.address, new BN(wei("100", "ether"))) - ).to.be.bignumber.equal(new BN(wei("30", "ether"))); - }); - - it("should return default values when source token = dest token", async () => { - let ret = await priceFeeds.queryRate(testToken1.address, testToken1.address); - await expect(ret[0]).to.be.bignumber.equal(new BN(wei("1", "ether"))); - await expect(ret[1]).to.be.bignumber.equal(new BN(wei("1", "ether"))); - }); - - it("should return 0 destination token amount when paused", async () => { - await priceFeeds.setGlobalPricingPaused(true); - await expect( - await priceFeeds.queryReturn(testToken1.address, testToken2.address, new BN(wei("100", "ether"))) - ).to.be.bignumber.equal(new BN(wei("0", "ether"))); - }); - - it("should not check price disagreements when paused", async () => { - await expectRevert( - priceFeeds.checkPriceDisagreement( - testToken1.address, - testToken2.address, - new BN(wei("100", "ether")), - new BN(wei("150", "ether")), - new BN(10).mul(new BN(wei("1", "ether"))) - ), - "pricing is paused" - ); - }); - - it("should revert for count mismatch while setting PriceFeed", async () => { - await expectRevert( - priceFeeds.setPriceFeed( - [testToken1.address, testToken2.address, doc.address, testWrbtc.address], - [priceFeedsV1PoolOracleTestToken1.address, priceFeedsV1PoolOracleTestToken2.address, priceFeedsV1PoolOracleDOC.address] - ), - "count mismatch" - ); - }); - - const getTestToken = async ({ decimals = 18, totalSupply = wei("100000000", "ether") }) => { - const token = await TestToken.new("TST", "TST", decimals, totalSupply); - return token; - }; - }); + let priceFeeds; + let sender, receiver; + let testWrbtc, doc; + + before(async () => { + [sender, receiver] = await ethers.getSigners(); + tokenSOV = await SOV.new(TOTAL_SUPPLY); + + testToken1 = await TestToken.new("test token 1", "TEST1", 18, wei("20000", "ether")); + testToken2 = await TestToken.new("test token 2", "TEST2", 16, wei("20000", "ether")); + testToken1Price = wei("60", "ether"); + testToken2Price = wei("2", "ether"); + docPrice = wei("10", "ether"); + wrBTCPrice = wei("10", "ether"); + + testWrbtc = await TestWrbtc.new(); + doc = await TestToken.new("dollar on chain", "DOC", 18, wei("20000", "ether")); + priceFeeds = await PriceFeeds.new(testWrbtc.address, tokenSOV.address, doc.address); + + liquidityV1ConverterMockupTestToken1 = await LiquidityPoolV1ConverterMockup.new( + testToken1.address, + testWrbtc.address + ); + priceFeedsV1PoolOracleMockupTestToken1 = await waffle.deployMockContract( + sender, + IV1PoolOracle.abi + ); + await priceFeedsV1PoolOracleMockupTestToken1.mock.latestAnswer.returns(testToken1Price); + await priceFeedsV1PoolOracleMockupTestToken1.mock.latestPrice.returns(testToken1Price); + await priceFeedsV1PoolOracleMockupTestToken1.mock.liquidityPool.returns( + liquidityV1ConverterMockupTestToken1.address + ); + priceFeedsV1PoolOracleTestToken1 = await PriceFeedV1PoolOracle.new( + priceFeedsV1PoolOracleMockupTestToken1.address, + testWrbtc.address, + doc.address, + testToken1.address + ); + + liquidityV1ConverterMockupTestToken2 = await LiquidityPoolV1ConverterMockup.new( + testToken2.address, + testWrbtc.address + ); + priceFeedsV1PoolOracleMockupTestToken2 = await waffle.deployMockContract( + sender, + IV1PoolOracle.abi + ); + await priceFeedsV1PoolOracleMockupTestToken2.mock.latestAnswer.returns(testToken2Price); + await priceFeedsV1PoolOracleMockupTestToken2.mock.latestPrice.returns(testToken2Price); + await priceFeedsV1PoolOracleMockupTestToken2.mock.liquidityPool.returns( + liquidityV1ConverterMockupTestToken2.address + ); + priceFeedsV1PoolOracleTestToken2 = await PriceFeedV1PoolOracle.new( + priceFeedsV1PoolOracleMockupTestToken2.address, + testWrbtc.address, + doc.address, + testToken2.address + ); + + // Set DOC feed -- price 1 BTC + liquidityV1ConverterMockupDOC = await LiquidityPoolV1ConverterMockup.new( + doc.address, + testWrbtc.address + ); + priceFeedsV1PoolOracleMockupDOC = await waffle.deployMockContract( + sender, + IV1PoolOracle.abi + ); + await priceFeedsV1PoolOracleMockupDOC.mock.latestAnswer.returns(docPrice); + await priceFeedsV1PoolOracleMockupDOC.mock.latestPrice.returns(docPrice); + await priceFeedsV1PoolOracleMockupDOC.mock.liquidityPool.returns( + liquidityV1ConverterMockupDOC.address + ); + priceFeedsV1PoolOracleDOC = await PriceFeedV1PoolOracle.new( + priceFeedsV1PoolOracleMockupDOC.address, + testWrbtc.address, + doc.address, + doc.address + ); + + // Set rBTC feed - using rsk oracle + priceFeedsV1PoolOracleMockupBTC = await PriceFeedRSKOracleMockup.new(); + await priceFeedsV1PoolOracleMockupBTC.setValue(wrBTCPrice); + priceFeedsV1PoolOracleBTC = await PriceFeedRSKOracle.new( + priceFeedsV1PoolOracleMockupBTC.address + ); + + await priceFeeds.setPriceFeed( + [testToken1.address, testToken2.address, doc.address, testWrbtc.address], + [ + priceFeedsV1PoolOracleTestToken1.address, + priceFeedsV1PoolOracleTestToken2.address, + priceFeedsV1PoolOracleDOC.address, + priceFeedsV1PoolOracleBTC.address, + ] + ); + + await priceFeeds.setDecimals([testToken1.address, testToken2.address]); + }); + + describe("PriceFeed unit tests", async () => { + it("amountInEth for wRBTC token should return passes amount", async () => { + const wrbtcToken = await priceFeeds.wrbtcToken(); + await expect( + await priceFeeds.amountInEth(wrbtcToken, new BN(wei("100", "ether"))) + ).to.be.bignumber.equal(new BN(wei("100", "ether"))); + }); + + it("getMaxDrawdown collateral token == loan token & comibed > collateral", async () => { + await expect( + await priceFeeds.getMaxDrawdown( + testToken1.address, + testToken1.address, + new BN(wei("100", "ether")), + new BN(wei("100", "ether")), + new BN(20).mul(new BN(10).pow(new BN(18))) + ) + ).to.be.bignumber.equal(new BN(0)); + }); + + it("getMaxDrawdown collateral token == loan token & collateral > combined", async () => { + await expect( + await priceFeeds.getMaxDrawdown( + testToken1.address, + testToken1.address, + new BN(wei("100", "ether")), + new BN(wei("150", "ether")), + new BN(20).mul(new BN(10).pow(new BN(18))) + ) + ).to.be.bignumber.equal(new BN(wei("30", "ether"))); + }); + + it("getMaxDrawdown collateral token != loan token", async () => { + await expect( + await priceFeeds.getMaxDrawdown( + testToken1.address, //mock loan token address + testToken2.address, //mock collateral token address + new BN(wei("100", "ether")), //loan token amount + new BN(wei("150", "ether")), //collateral token amount + new BN(20).mul(new BN(10).pow(new BN(18))) + ) + ).to.be.bignumber.equal(new BN(wei("114", "ether"))); + + await expect( + await priceFeeds.getMaxDrawdown( + testToken1.address, + testToken2.address, + new BN(wei("100", "ether")), + new BN(wei("30", "ether")), // <36 + new BN(20).mul(new BN(wei("1", "ether"))) + ) + ).to.be.bignumber.equal(new BN(0)); + }); + + it("getMaxDrawdown - unsupported src feed", async () => { + await expectRevert( + priceFeeds.getMaxDrawdown( + accounts[9], //mock loan token address + testToken2.address, //mock collateral token address + new BN(wei("100", "ether")), //loan token amount + new BN(wei("150", "ether")), //collateral token amount + new BN(20).mul(new BN(10).pow(new BN(18))) + ), + "unsupported src feed" + ); + }); + + it("getMaxDrawdown - unsupported dst feed", async () => { + await expectRevert( + priceFeeds.getMaxDrawdown( + testToken1.address, //mock loan token address + accounts[9], //mock collateral token address + new BN(wei("100", "ether")), //loan token amount + new BN(wei("150", "ether")), //collateral token amount + new BN(20).mul(new BN(10).pow(new BN(18))) + ), + "unsupported dst feed" + ); + }); + + it("getCurrentMargin collateralToken == loanToken", async () => { + let ret = await priceFeeds.getCurrentMargin( + testToken1.address, //mock loan token address + testToken1.address, //mock collateral token address + new BN(wei("100", "ether")), //loan token amount + new BN(wei("150", "ether")) //collateral token amount + ); + await expect(ret[0]).to.be.bignumber.equal(new BN(wei("50", "ether"))); + await expect(ret[1]).to.be.bignumber.equal(new BN(wei("1", "ether"))); + + ret = await priceFeeds.getCurrentMargin( + testToken1.address, //mock loan token address + testToken1.address, //mock collateral token address + new BN(wei("100", "ether")), //loan token amount + new BN(wei("30", "ether")) //collateral token amount + ); + await expect(ret[0]).to.be.bignumber.equal(new BN(0)); + await expect(ret[1]).to.be.bignumber.equal(new BN(wei("1", "ether"))); + + ret = await priceFeeds.getCurrentMargin( + testToken1.address, //mock loan token address + testToken1.address, //mock collateral token address + new BN(wei("0", "ether")), //loan token amount + new BN(wei("30", "ether")) //collateral token amount + ); + await expect(ret[0]).to.be.bignumber.equal(new BN(0)); + await expect(ret[1]).to.be.bignumber.equal(new BN(wei("1", "ether"))); + }); + + it("getCurrentMarginAndCollateralSize", async () => { + let ret = await priceFeeds.getCurrentMarginAndCollateralSize( + testToken1.address, + testToken2.address, + new BN(wei("100", "ether")), //loan token amount + new BN(wei("100", "ether")) //collateral token amount + ); + await expect(ret[0]).to.be.bignumber.equal("233333333333333330000"); + await expect(ret[1]).to.be.bignumber.equal(new BN(wei("20000", "ether"))); // we are testing using 16 decimals for testToken2, so we need to multiply 10^2 for the expectation + }); + + it("shouldLiquidate(...) runs correctly", async () => { + let ret = await priceFeeds.shouldLiquidate( + testToken1.address, //mock loan token address + testToken1.address, //mock collateral token address + new BN(wei("100", "ether")), //loan token amount + new BN(wei("150", "ether")), //collateral token amount + new BN(10).mul(new BN(wei("1", "ether"))) //maintenance margin threshold 10% + ); + expect(ret).to.be.false; //50<=10 + + ret = await priceFeeds.shouldLiquidate( + testToken1.address, + testToken2.address, + new BN(wei("100", "ether")), //loan token amount + new BN(wei("450", "ether")), //collateral token amount + new BN(30).mul(new BN(wei("1", "ether"))) //maintenance margin + ); + expect(ret).to.be.false; //1399<=30 + + ret = await priceFeeds.shouldLiquidate( + testToken1.address, + testToken2.address, + new BN(wei("100", "ether")), //loan token amount + new BN(wei("450", "ether")), //collateral token amount + new BN(1500).mul(new BN(wei("1", "ether"))) //maintenance margin + ); + expect(ret).to.be.true; //1399<=1500 + }); + + it("setProtocolTokenEthPrice runs correctly", async () => { + expect(await priceFeeds.protocolTokenEthPrice()).to.be.bignumber.equal( + new BN(2).mul(new BN(10).pow(new BN(14))) + ); + + await priceFeeds.setProtocolTokenEthPrice(wei("10", "ether")); + expect(await priceFeeds.protocolTokenEthPrice()).to.be.bignumber.equal( + new BN(wei("10", "ether")) + ); + + await expectRevert(priceFeeds.setProtocolTokenEthPrice(new BN(0)), "invalid price"); + await expectRevert( + priceFeeds.setProtocolTokenEthPrice(wei("10", "ether"), { from: accounts[10] }), + "unauthorized" + ); + }); + + it("setGlobalPricingPaused", async () => { + let isPaused = await priceFeeds.globalPricingPaused(); + await priceFeeds.setGlobalPricingPaused(true); + expect(await priceFeeds.globalPricingPaused()).to.not.equal(isPaused); + await priceFeeds.setGlobalPricingPaused(false); + expect(await priceFeeds.globalPricingPaused()).to.equal(isPaused); + }); + + it("setDecimals", async () => { + testTokens = []; + let decimals = []; + for (let i = 0; i < 3; i++) { + decimals[i] = Math.round(Math.random() * 10 + 8); + testTokens[i] = await getTestToken({ decimals: decimals[i] }); + } + await priceFeeds.setDecimals(testTokens.map((item) => item.address)); + for (i = 0; i < 3; i++) { + expect( + (await priceFeeds.decimals(await testTokens[i].address)).toNumber() + ).to.be.eq(decimals[i]); + } + }); + + it("_queryRate internal", async () => { + testTokens = []; + let decimals = []; + let addresses = []; + for (let i = 0; i < 2; i++) { + decimals[i] = (i + 1) * 10 + i; + testTokens[i] = await getTestToken({ decimals: decimals[i] }); + addresses[i] = testTokens[i].address; + } + expect( + await priceFeeds.queryPrecision(addresses[0], addresses[0]) + ).to.be.bignumber.equal(new BN(wei("1", "ether"))); + expect( + await priceFeeds.queryPrecision(addresses[1], addresses[1]) + ).to.be.bignumber.equal(new BN(wei("1", "ether"))); + //source decimals > dest decimals + expect( + await priceFeeds.queryPrecision(addresses[1], addresses[0]) + ).to.be.bignumber.equal(new BN(10).pow(new BN(29))); //10^7 (18+diff) + //source decimals < dest decimals + expect( + await priceFeeds.queryPrecision(addresses[0], addresses[1]) + ).to.be.bignumber.equal(new BN(10).pow(new BN(7))); //10^29 (18-diff) + }); + + it("queryPrecision source token = dest token", async () => { + expect( + await priceFeeds.queryPrecision(testToken1.address, testToken1.address) + ).to.be.bignumber.equal(new BN(wei("1", "ether"))); + }); + + it("should return destination token amount", async () => { + await expect( + await priceFeeds.queryReturn( + testToken1.address, + testToken2.address, + new BN(wei("100", "ether")) + ) + ).to.be.bignumber.equal(new BN(wei("30", "ether"))); + }); + + it("should return default values when source token = dest token", async () => { + let ret = await priceFeeds.queryRate(testToken1.address, testToken1.address); + await expect(ret[0]).to.be.bignumber.equal(new BN(wei("1", "ether"))); + await expect(ret[1]).to.be.bignumber.equal(new BN(wei("1", "ether"))); + }); + + it("should return 0 destination token amount when paused", async () => { + await priceFeeds.setGlobalPricingPaused(true); + await expect( + await priceFeeds.queryReturn( + testToken1.address, + testToken2.address, + new BN(wei("100", "ether")) + ) + ).to.be.bignumber.equal(new BN(wei("0", "ether"))); + }); + + it("should not check price disagreements when paused", async () => { + await expectRevert( + priceFeeds.checkPriceDisagreement( + testToken1.address, + testToken2.address, + new BN(wei("100", "ether")), + new BN(wei("150", "ether")), + new BN(10).mul(new BN(wei("1", "ether"))) + ), + "pricing is paused" + ); + }); + + it("should revert for count mismatch while setting PriceFeed", async () => { + await expectRevert( + priceFeeds.setPriceFeed( + [testToken1.address, testToken2.address, doc.address, testWrbtc.address], + [ + priceFeedsV1PoolOracleTestToken1.address, + priceFeedsV1PoolOracleTestToken2.address, + priceFeedsV1PoolOracleDOC.address, + ] + ), + "count mismatch" + ); + }); + + const getTestToken = async ({ + decimals = 18, + totalSupply = wei("100000000", "ether"), + }) => { + const token = await TestToken.new("TST", "TST", decimals, totalSupply); + return token; + }; + }); }); diff --git a/tests/price-feeds/PriceFeedsLocal.js b/tests/price-feeds/PriceFeedsLocal.js index 96d57af80..f765bdfbc 100644 --- a/tests/price-feeds/PriceFeedsLocal.js +++ b/tests/price-feeds/PriceFeedsLocal.js @@ -10,106 +10,114 @@ const TestToken = artifacts.require("TestToken"); const PriceFeedsLocal = artifacts.require("PriceFeedsLocal"); const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getLoanTokenLogic, - getLoanToken, - getLoanTokenLogicWrbtc, - getLoanTokenWRBTC, - loan_pool_setup, - set_demand_curve, - getPriceFeeds, - getSovryn, - decodeLogs, - getSOV, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getLoanTokenLogic, + getLoanToken, + getLoanTokenLogicWrbtc, + getLoanTokenWRBTC, + loan_pool_setup, + set_demand_curve, + getPriceFeeds, + getSovryn, + decodeLogs, + getSOV, } = require("../Utils/initializer.js"); contract("Affiliates", (accounts) => { - let WRBTC; - let doc; - let SUSD; - let sovryn; - let feeds; - let wei = web3.utils.toWei; - - async function deploymentAndInitFixture(_wallets, _provider) { - // Deploying sovrynProtocol w/ generic function from initializer.js - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - - // Protocol token - SOV = await getSOV(sovryn, priceFeeds, SUSD, accounts); - - // Another token - doc = await TestToken.new("dollar on chain", "DOC", 18, wei("20000", "ether")); - - // New PriceFeeds - feeds = await PriceFeedsLocal.new(WRBTC.address, SOV.address); - await feeds.setRates(doc.address, WRBTC.address, wei("0.01", "ether")); - } - - before(async () => { - [owner, trader, referrer, account1, account2, ...accounts] = accounts; - }); - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - /// @dev Test coverage of PriceFeedsLocal - describe("PriceFeedsLocal", () => { - let testToken1, testToken2; - - before(async () => { - testToken1 = await TestToken.new("test token 1", "TEST1", 18, wei("20000", "ether")); - testToken2 = await TestToken.new("test token 2", "TEST2", 16, wei("20000", "ether")); - }); - - it("PriceFeedsLocal::setGlobalPricingPaused test", async () => { - await feeds.setGlobalPricingPaused(true); - await expect( - await feeds.queryReturn(testToken1.address, testToken2.address, new BN(wei("100", "ether"))) - ).to.be.bignumber.equal(new BN(wei("0", "ether"))); - }); - - it("PriceFeedsLocal::queryRate should fail when paused", async () => { - await feeds.setGlobalPricingPaused(true); - await expectRevert(feeds.queryRate(doc.address, doc.address), "pricing is paused"); - }); - - it("PriceFeedsLocal::setRates when sourceToken==destToken", async () => { - // Check rate before setting - let doc2doc = await feeds.queryRate(doc.address, doc.address); - expect(doc2doc[0]).to.be.bignumber.equal(new BN(wei("1", "ether"))); - - // Trying to set a new rate for doc/doc change; it doesn't revert, just ignores it. - await feeds.setRates(doc.address, doc.address, wei("0.01", "ether")); - - // Check rate after setting, it shouldn't have changed even though we tried to - doc2doc = await feeds.queryRate(doc.address, doc.address); - expect(doc2doc[0]).to.be.bignumber.equal(new BN(wei("1", "ether"))); - }); - - it("PriceFeedsLocal::queryRate when sourceToken or destToken == protocolTokenAddress", async () => { - // Set protocol token price - await feeds.setProtocolTokenEthPrice(wei("1234", "ether")); - expect(await feeds.protocolTokenEthPrice()).to.be.bignumber.equal(new BN(wei("1234", "ether"))); - - // Check rate when sourceToken == protocolTokenAddress - // Rate should be exactly the one previously defined w/ setProtocolTokenEthPrice() method - let rate = await feeds.queryRate(SOV.address, doc.address); - expect(rate[0]).to.be.bignumber.equal(new BN(wei("1234", "ether"))); - - // Check rate when destToken == protocolTokenAddress - // Rate should be the inverse of the previously defined w/ setProtocolTokenEthPrice() method - rate = await feeds.queryRate(doc.address, SOV.address); - expect(rate[0]).to.be.bignumber.equal(new BN(10).pow(new BN(36)).div(new BN(wei("1234", "ether")))); - }); - }); + let WRBTC; + let doc; + let SUSD; + let sovryn; + let feeds; + let wei = web3.utils.toWei; + + async function deploymentAndInitFixture(_wallets, _provider) { + // Deploying sovrynProtocol w/ generic function from initializer.js + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + + // Protocol token + SOV = await getSOV(sovryn, priceFeeds, SUSD, accounts); + + // Another token + doc = await TestToken.new("dollar on chain", "DOC", 18, wei("20000", "ether")); + + // New PriceFeeds + feeds = await PriceFeedsLocal.new(WRBTC.address, SOV.address); + await feeds.setRates(doc.address, WRBTC.address, wei("0.01", "ether")); + } + + before(async () => { + [owner, trader, referrer, account1, account2, ...accounts] = accounts; + }); + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + /// @dev Test coverage of PriceFeedsLocal + describe("PriceFeedsLocal", () => { + let testToken1, testToken2; + + before(async () => { + testToken1 = await TestToken.new("test token 1", "TEST1", 18, wei("20000", "ether")); + testToken2 = await TestToken.new("test token 2", "TEST2", 16, wei("20000", "ether")); + }); + + it("PriceFeedsLocal::setGlobalPricingPaused test", async () => { + await feeds.setGlobalPricingPaused(true); + await expect( + await feeds.queryReturn( + testToken1.address, + testToken2.address, + new BN(wei("100", "ether")) + ) + ).to.be.bignumber.equal(new BN(wei("0", "ether"))); + }); + + it("PriceFeedsLocal::queryRate should fail when paused", async () => { + await feeds.setGlobalPricingPaused(true); + await expectRevert(feeds.queryRate(doc.address, doc.address), "pricing is paused"); + }); + + it("PriceFeedsLocal::setRates when sourceToken==destToken", async () => { + // Check rate before setting + let doc2doc = await feeds.queryRate(doc.address, doc.address); + expect(doc2doc[0]).to.be.bignumber.equal(new BN(wei("1", "ether"))); + + // Trying to set a new rate for doc/doc change; it doesn't revert, just ignores it. + await feeds.setRates(doc.address, doc.address, wei("0.01", "ether")); + + // Check rate after setting, it shouldn't have changed even though we tried to + doc2doc = await feeds.queryRate(doc.address, doc.address); + expect(doc2doc[0]).to.be.bignumber.equal(new BN(wei("1", "ether"))); + }); + + it("PriceFeedsLocal::queryRate when sourceToken or destToken == protocolTokenAddress", async () => { + // Set protocol token price + await feeds.setProtocolTokenEthPrice(wei("1234", "ether")); + expect(await feeds.protocolTokenEthPrice()).to.be.bignumber.equal( + new BN(wei("1234", "ether")) + ); + + // Check rate when sourceToken == protocolTokenAddress + // Rate should be exactly the one previously defined w/ setProtocolTokenEthPrice() method + let rate = await feeds.queryRate(SOV.address, doc.address); + expect(rate[0]).to.be.bignumber.equal(new BN(wei("1234", "ether"))); + + // Check rate when destToken == protocolTokenAddress + // Rate should be the inverse of the previously defined w/ setProtocolTokenEthPrice() method + rate = await feeds.queryRate(doc.address, SOV.address); + expect(rate[0]).to.be.bignumber.equal( + new BN(10).pow(new BN(36)).div(new BN(wei("1234", "ether"))) + ); + }); + }); }); diff --git a/tests/price-feeds/USDTPriceFeed.js b/tests/price-feeds/USDTPriceFeed.js index 41bdc0894..ad4a0e8f8 100644 --- a/tests/price-feeds/USDTPriceFeed.js +++ b/tests/price-feeds/USDTPriceFeed.js @@ -11,18 +11,22 @@ const { BN } = require("@openzeppelin/test-helpers"); const USDTPriceFeed = artifacts.require("USDTPriceFeed"); contract("USDTPriceFeed", (accounts) => { - let priceFeed; - before(async () => { - priceFeed = await USDTPriceFeed.new(); - }); - describe("USDTPriceFeed unit tests", async () => { - it("Exchange rate USDT/USDT should be 1", async () => { - await expect(await priceFeed.latestAnswer()).to.be.bignumber.equal(new BN(10).pow(new BN(18))); - }); + let priceFeed; + before(async () => { + priceFeed = await USDTPriceFeed.new(); + }); + describe("USDTPriceFeed unit tests", async () => { + it("Exchange rate USDT/USDT should be 1", async () => { + await expect(await priceFeed.latestAnswer()).to.be.bignumber.equal( + new BN(10).pow(new BN(18)) + ); + }); - it("Returns current block timestamp", async () => { - let block = await web3.eth.getBlock("latest"); - await expect(await priceFeed.latestTimestamp()).to.be.bignumber.equal(new BN(block.timestamp)); - }); - }); + it("Returns current block timestamp", async () => { + let block = await web3.eth.getBlock("latest"); + await expect(await priceFeed.latestTimestamp()).to.be.bignumber.equal( + new BN(block.timestamp) + ); + }); + }); }); diff --git a/tests/protocol/ChangeLoanDurationTestToken.test.js b/tests/protocol/ChangeLoanDurationTestToken.test.js index 41b583263..4433341a0 100644 --- a/tests/protocol/ChangeLoanDurationTestToken.test.js +++ b/tests/protocol/ChangeLoanDurationTestToken.test.js @@ -22,21 +22,21 @@ const LockedSOVMockup = artifacts.require("LockedSOVMockup"); const LockedSOVFailedMockup = artifacts.require("LockedSOVFailedMockup"); const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getLoanToken, - getLoanTokenWRBTC, - loan_pool_setup, - set_demand_curve, - lend_to_pool, - getPriceFeeds, - getSovryn, - decodeLogs, - open_margin_trade_position, - borrow_indefinite_loan, - getSOV, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getLoanToken, + getLoanTokenWRBTC, + loan_pool_setup, + set_demand_curve, + lend_to_pool, + getPriceFeeds, + getSovryn, + decodeLogs, + open_margin_trade_position, + borrow_indefinite_loan, + getSOV, } = require("../Utils/initializer.js"); const wei = web3.utils.toWei; @@ -63,1156 +63,1615 @@ Test extending and reducing loan durations. */ contract("ProtocolChangeLoanDuration", (accounts) => { - let owner; - let sovryn, SUSD, WRBTC, RBTC, BZRX, loanToken, loanTokenWRBTC, priceFeeds, SOV; - - async function deploymentAndInitFixture(_wallets, _provider) { - // Deploying sovrynProtocol w/ generic function from initializer.js - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - - loanToken = await getLoanToken(owner, sovryn, WRBTC, SUSD); - loanTokenWRBTC = await getLoanTokenWRBTC(owner, sovryn, WRBTC, SUSD); - await loan_pool_setup(sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC); - - // Protocol token - SOV = await getSOV(sovryn, priceFeeds, SUSD, accounts); - - /// @dev Moved from some tests that require this initialization - /// and is not interfering w/ any others. - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, owner); - } - - before(async () => { - [owner] = accounts; - }); - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - describe("Test LoanMaintenance::getUserLoans and getActiveLoans", () => { - it("should return empty if start >= end", async () => { - // no loan created - const loansData = await sovryn.getUserLoans(accounts[1], 0, 10, 0, 0, 0); /// @dev parameters: user, start, count, loanType, isLender, unsafeOnly - assert.equal(loansData.length, 0); - }); - - it("should return empty if count == 0", async () => { - // prepare the test - const [loan_id, borrower] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - - const loansData = await sovryn.getUserLoans(borrower, 0, 0, 0, 0, 0); /// @dev parameters: user, start, count, loanType, isLender, unsafeOnly - assert.equal(loansData.length, 0); - }); - - it("should exist a loan, check values", async () => { - // prepare the test - const [loan_id, borrower] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - - const loansData = await sovryn.getUserLoans(borrower, 0, 10, 0, 0, 0); /// @dev parameters: user, start, count, loanType, isLender, unsafeOnly - // console.log("loansData = ", loansData); - - assert.equal(loansData[0]["loanId"], loan_id); - assert.equal(loansData[0]["loanToken"], SUSD.address); - assert.equal(loansData[0]["collateralToken"], RBTC.address); - }); - - it("should not exist an unsafe loan", async () => { - // prepare the test - const [loan_id, borrower] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - - // Check UnsafeOnly, no results to expect - const loansDataUnsafeOnly = await sovryn.getUserLoans(borrower, 0, 10, 0, 0, true); /// @dev parameters: user, start, count, loanType, isLender, unsafeOnly - // console.log("loansDataUnsafeOnly = ", loansDataUnsafeOnly); - - assert.equal(loansDataUnsafeOnly.length, 0); - }); - - it("should exist an active loan, check values", async () => { - // prepare the test - const [loan_id, borrower] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - - const loansData = await sovryn.getActiveLoans(0, 10, 0); /// @dev parameters: start, count, unsafeOnly - // console.log("loansData = ", loansData); - - assert.equal(loansData[0]["loanId"], loan_id); - assert.equal(loansData[0]["loanToken"], SUSD.address); - assert.equal(loansData[0]["collateralToken"], RBTC.address); - }); - - it("Coverage to avoid final conditional (itemCount < count) on getActiveLoans", async () => { - // prepare the test - const [loan_id, borrower] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - - const loansData = await sovryn.getActiveLoans(0, 1, 0); /// @dev parameters: start, count, unsafeOnly - // console.log("loansData = ", loansData); - - assert.equal(loansData[0]["loanId"], loan_id); - assert.equal(loansData[0]["loanToken"], SUSD.address); - assert.equal(loansData[0]["collateralToken"], RBTC.address); - }); - - it("Coverage to meet the conditional (start >= end) on getActiveLoans", async () => { - // prepare the test - const [loan_id, borrower] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - - const loansData = await sovryn.getActiveLoans(1, 1, 0); /// @dev parameters: start, count, unsafeOnly - // console.log("loansData = ", loansData); - - assert.equal(loansData.length, 0); - }); - - it("should not exist any active unsafe loan", async () => { - // prepare the test - const [loan_id, borrower] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - - // Check UnsafeOnly, no results to expect - const loansDataUnsafeOnly = await sovryn.getActiveLoans(0, 10, true); /// @dev parameters: start, count, unsafeOnly - // console.log("loansDataUnsafeOnly = ", loansDataUnsafeOnly); - - assert.equal(loansDataUnsafeOnly.length, 0); - }); - - it("should get a loanType 1: a margin trade loan", async () => { - // prepare the test - const [loan_id] = await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, accounts[1]); - - const loansData = await sovryn.getUserLoans(accounts[1], 0, 10, 1, 0, 0); /// @dev parameters: user, start, count, loanType, isLender, unsafeOnly - // console.log("loansData = ", loansData); - - assert.equal(loansData[0]["loanId"], loan_id); - assert.equal(loansData[0]["loanToken"], SUSD.address); - assert.equal(loansData[0]["collateralToken"], RBTC.address); - }); - - it("should get a loanType 2: a non-margin trade loan", async () => { - // prepare the test - const [loan_id, borrower] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - - const loansData = await sovryn.getUserLoans(borrower, 0, 10, 2, 0, 0); /// @dev parameters: user, start, count, loanType, isLender, unsafeOnly - // console.log("loansData = ", loansData); - - assert.equal(loansData[0]["loanId"], loan_id); - assert.equal(loansData[0]["loanToken"], SUSD.address); - assert.equal(loansData[0]["collateralToken"], RBTC.address); - }); - - it("Coverage to avoid final conditional (itemCount < count) on getUserLoans", async () => { - // prepare the test - const [loan_id, borrower] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - - const loansData = await sovryn.getUserLoans(borrower, 0, 1, 2, 0, 0); /// @dev parameters: user, start, count, loanType, isLender, unsafeOnly - // console.log("loansData = ", loansData); - - assert.equal(loansData[0]["loanId"], loan_id); - assert.equal(loansData[0]["loanToken"], SUSD.address); - assert.equal(loansData[0]["collateralToken"], RBTC.address); - }); - - it("Coverage to meet the conditional (start >= end) on getUserLoans", async () => { - // prepare the test - const [loan_id, borrower] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - - const loansData = await sovryn.getUserLoans(borrower, 1, 1, 2, 0, 0); /// @dev parameters: user, start, count, loanType, isLender, unsafeOnly - // console.log("loansData = ", loansData); - - assert.equal(loansData.length, 0); - }); - }); - - describe("Test LoanMaintenance::getUserLoans and getActiveLoans (V2 LoanData)", () => { - it("should return empty if start >= end", async () => { - // no loan created - const loansData = await sovryn.getUserLoansV2(accounts[1], 0, 10, 0, 0, 0); /// @dev parameters: user, start, count, loanType, isLender, unsafeOnly - assert.equal(loansData.length, 0); - }); - - it("should return empty if count == 0", async () => { - // prepare the test - const [loan_id, borrower] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - - const loansData = await sovryn.getUserLoansV2(borrower, 0, 0, 0, 0, 0); /// @dev parameters: user, start, count, loanType, isLender, unsafeOnly - assert.equal(loansData.length, 0); - }); - - it("should exist a loan, check values", async () => { - // prepare the test - const [loan_id, borrower] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - - const loansData = await sovryn.getUserLoansV2(borrower, 0, 10, 0, 0, 0); /// @dev parameters: user, start, count, loanType, isLender, unsafeOnly - // console.log("loansData = ", loansData); - - assert.equal(loansData[0]["loanId"], loan_id); - assert.equal(loansData[0]["loanToken"], SUSD.address); - assert.equal(loansData[0]["collateralToken"], RBTC.address); - assert.equal(loansData[0]["borrower"], borrower); - }); - - it("should not exist an unsafe loan", async () => { - // prepare the test - const [loan_id, borrower] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - - // Check UnsafeOnly, no results to expect - const loansDataUnsafeOnly = await sovryn.getUserLoansV2(borrower, 0, 10, 0, 0, true); /// @dev parameters: user, start, count, loanType, isLender, unsafeOnly - // console.log("loansDataUnsafeOnly = ", loansDataUnsafeOnly); - - assert.equal(loansDataUnsafeOnly.length, 0); - }); - - it("should exist an active loan, check values", async () => { - // prepare the test - const [loan_id, borrower] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - - const loansData = await sovryn.getActiveLoansV2(0, 10, 0); /// @dev parameters: start, count, unsafeOnly - // console.log("loansData = ", loansData); - - assert.equal(loansData[0]["loanId"], loan_id); - assert.equal(loansData[0]["loanToken"], SUSD.address); - assert.equal(loansData[0]["collateralToken"], RBTC.address); - assert.equal(loansData[0]["borrower"], borrower); - }); - - it("Coverage to avoid final conditional (itemCount < count) on getActiveLoans", async () => { - // prepare the test - const [loan_id, borrower] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - - const loansData = await sovryn.getActiveLoansV2(0, 1, 0); /// @dev parameters: start, count, unsafeOnly - // console.log("loansData = ", loansData); - - assert.equal(loansData[0]["loanId"], loan_id); - assert.equal(loansData[0]["loanToken"], SUSD.address); - assert.equal(loansData[0]["collateralToken"], RBTC.address); - assert.equal(loansData[0]["borrower"], borrower); - }); - - it("Coverage to meet the conditional (start >= end) on getActiveLoans", async () => { - // prepare the test - const [loan_id, borrower] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - - const loansData = await sovryn.getActiveLoansV2(1, 1, 0); /// @dev parameters: start, count, unsafeOnly - // console.log("loansData = ", loansData); - - assert.equal(loansData.length, 0); - }); - - it("should not exist any active unsafe loan", async () => { - // prepare the test - const [loan_id, borrower] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - - // Check UnsafeOnly, no results to expect - const loansDataUnsafeOnly = await sovryn.getActiveLoansV2(0, 10, true); /// @dev parameters: start, count, unsafeOnly - // console.log("loansDataUnsafeOnly = ", loansDataUnsafeOnly); - - assert.equal(loansDataUnsafeOnly.length, 0); - }); - - it("should get a loanType 1: a margin trade loan", async () => { - // prepare the test - const [loan_id] = await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, accounts[1]); - - const loansData = await sovryn.getUserLoansV2(accounts[1], 0, 10, 1, 0, 0); /// @dev parameters: user, start, count, loanType, isLender, unsafeOnly - // console.log("loansData = ", loansData); - - assert.equal(loansData[0]["loanId"], loan_id); - assert.equal(loansData[0]["loanToken"], SUSD.address); - assert.equal(loansData[0]["collateralToken"], RBTC.address); - assert.equal(loansData[0]["borrower"], accounts[1]); - }); - - it("should get a loanType 2: a non-margin trade loan", async () => { - // prepare the test - const [loan_id, borrower] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - - const loansData = await sovryn.getUserLoansV2(borrower, 0, 10, 2, 0, 0); /// @dev parameters: user, start, count, loanType, isLender, unsafeOnly - // console.log("loansData = ", loansData); - - assert.equal(loansData[0]["loanId"], loan_id); - assert.equal(loansData[0]["loanToken"], SUSD.address); - assert.equal(loansData[0]["collateralToken"], RBTC.address); - assert.equal(loansData[0]["borrower"], borrower); - }); - - it("Coverage to avoid final conditional (itemCount < count) on getUserLoans", async () => { - // prepare the test - const [loan_id, borrower] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - - const loansData = await sovryn.getUserLoansV2(borrower, 0, 1, 2, 0, 0); /// @dev parameters: user, start, count, loanType, isLender, unsafeOnly - // console.log("loansData = ", loansData); - - assert.equal(loansData[0]["loanId"], loan_id); - assert.equal(loansData[0]["loanToken"], SUSD.address); - assert.equal(loansData[0]["collateralToken"], RBTC.address); - assert.equal(loansData[0]["borrower"], borrower); - }); - - it("Coverage to meet the conditional (start >= end) on getUserLoans", async () => { - // prepare the test - const [loan_id, borrower] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - - const loansData = await sovryn.getUserLoansV2(borrower, 1, 1, 2, 0, 0); /// @dev parameters: user, start, count, loanType, isLender, unsafeOnly - // console.log("loansData = ", loansData); - - assert.equal(loansData.length, 0); - }); - }); - - describe("Test set new special rebates.", () => { - it("Test set new special rebates for specific pairs", async () => { - const invalidValue = wei("1001", "ether"); - await expectRevert(sovryn.setSpecialRebates(SUSD.address, RBTC.address, 0, { from: accounts[8] }), "unauthorized"); - await expectRevert(sovryn.setSpecialRebates(SUSD.address, RBTC.address, invalidValue), "Special fee rebate is too high"); - - const validValue = wei("200", "ether"); - await sovryn.setSpecialRebates(SUSD.address, RBTC.address, validValue); - expect((await sovryn.specialRebates(SUSD.address, RBTC.address)).toString(), "Incorrect new special fee rebates").to.be.equal( - validValue.toString() - ); - }); - }); - - describe("Test extending and reducing loan durations.", () => { - /* + let owner; + let sovryn, SUSD, WRBTC, RBTC, BZRX, loanToken, loanTokenWRBTC, priceFeeds, SOV; + + async function deploymentAndInitFixture(_wallets, _provider) { + // Deploying sovrynProtocol w/ generic function from initializer.js + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + + loanToken = await getLoanToken(owner, sovryn, WRBTC, SUSD); + loanTokenWRBTC = await getLoanTokenWRBTC(owner, sovryn, WRBTC, SUSD); + await loan_pool_setup(sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC); + + // Protocol token + SOV = await getSOV(sovryn, priceFeeds, SUSD, accounts); + + /// @dev Moved from some tests that require this initialization + /// and is not interfering w/ any others. + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, owner); + } + + before(async () => { + [owner] = accounts; + }); + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + describe("Test LoanMaintenance::getUserLoans and getActiveLoans", () => { + it("should return empty if start >= end", async () => { + // no loan created + const loansData = await sovryn.getUserLoans(accounts[1], 0, 10, 0, 0, 0); /// @dev parameters: user, start, count, loanType, isLender, unsafeOnly + assert.equal(loansData.length, 0); + }); + + it("should return empty if count == 0", async () => { + // prepare the test + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + + const loansData = await sovryn.getUserLoans(borrower, 0, 0, 0, 0, 0); /// @dev parameters: user, start, count, loanType, isLender, unsafeOnly + assert.equal(loansData.length, 0); + }); + + it("should exist a loan, check values", async () => { + // prepare the test + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + + const loansData = await sovryn.getUserLoans(borrower, 0, 10, 0, 0, 0); /// @dev parameters: user, start, count, loanType, isLender, unsafeOnly + // console.log("loansData = ", loansData); + + assert.equal(loansData[0]["loanId"], loan_id); + assert.equal(loansData[0]["loanToken"], SUSD.address); + assert.equal(loansData[0]["collateralToken"], RBTC.address); + }); + + it("should not exist an unsafe loan", async () => { + // prepare the test + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + + // Check UnsafeOnly, no results to expect + const loansDataUnsafeOnly = await sovryn.getUserLoans(borrower, 0, 10, 0, 0, true); /// @dev parameters: user, start, count, loanType, isLender, unsafeOnly + // console.log("loansDataUnsafeOnly = ", loansDataUnsafeOnly); + + assert.equal(loansDataUnsafeOnly.length, 0); + }); + + it("should exist an active loan, check values", async () => { + // prepare the test + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + + const loansData = await sovryn.getActiveLoans(0, 10, 0); /// @dev parameters: start, count, unsafeOnly + // console.log("loansData = ", loansData); + + assert.equal(loansData[0]["loanId"], loan_id); + assert.equal(loansData[0]["loanToken"], SUSD.address); + assert.equal(loansData[0]["collateralToken"], RBTC.address); + }); + + it("Coverage to avoid final conditional (itemCount < count) on getActiveLoans", async () => { + // prepare the test + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + + const loansData = await sovryn.getActiveLoans(0, 1, 0); /// @dev parameters: start, count, unsafeOnly + // console.log("loansData = ", loansData); + + assert.equal(loansData[0]["loanId"], loan_id); + assert.equal(loansData[0]["loanToken"], SUSD.address); + assert.equal(loansData[0]["collateralToken"], RBTC.address); + }); + + it("Coverage to meet the conditional (start >= end) on getActiveLoans", async () => { + // prepare the test + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + + const loansData = await sovryn.getActiveLoans(1, 1, 0); /// @dev parameters: start, count, unsafeOnly + // console.log("loansData = ", loansData); + + assert.equal(loansData.length, 0); + }); + + it("should not exist any active unsafe loan", async () => { + // prepare the test + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + + // Check UnsafeOnly, no results to expect + const loansDataUnsafeOnly = await sovryn.getActiveLoans(0, 10, true); /// @dev parameters: start, count, unsafeOnly + // console.log("loansDataUnsafeOnly = ", loansDataUnsafeOnly); + + assert.equal(loansDataUnsafeOnly.length, 0); + }); + + it("should get a loanType 1: a margin trade loan", async () => { + // prepare the test + const [loan_id] = await open_margin_trade_position( + loanToken, + RBTC, + WRBTC, + SUSD, + accounts[1] + ); + + const loansData = await sovryn.getUserLoans(accounts[1], 0, 10, 1, 0, 0); /// @dev parameters: user, start, count, loanType, isLender, unsafeOnly + // console.log("loansData = ", loansData); + + assert.equal(loansData[0]["loanId"], loan_id); + assert.equal(loansData[0]["loanToken"], SUSD.address); + assert.equal(loansData[0]["collateralToken"], RBTC.address); + }); + + it("should get a loanType 2: a non-margin trade loan", async () => { + // prepare the test + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + + const loansData = await sovryn.getUserLoans(borrower, 0, 10, 2, 0, 0); /// @dev parameters: user, start, count, loanType, isLender, unsafeOnly + // console.log("loansData = ", loansData); + + assert.equal(loansData[0]["loanId"], loan_id); + assert.equal(loansData[0]["loanToken"], SUSD.address); + assert.equal(loansData[0]["collateralToken"], RBTC.address); + }); + + it("Coverage to avoid final conditional (itemCount < count) on getUserLoans", async () => { + // prepare the test + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + + const loansData = await sovryn.getUserLoans(borrower, 0, 1, 2, 0, 0); /// @dev parameters: user, start, count, loanType, isLender, unsafeOnly + // console.log("loansData = ", loansData); + + assert.equal(loansData[0]["loanId"], loan_id); + assert.equal(loansData[0]["loanToken"], SUSD.address); + assert.equal(loansData[0]["collateralToken"], RBTC.address); + }); + + it("Coverage to meet the conditional (start >= end) on getUserLoans", async () => { + // prepare the test + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + + const loansData = await sovryn.getUserLoans(borrower, 1, 1, 2, 0, 0); /// @dev parameters: user, start, count, loanType, isLender, unsafeOnly + // console.log("loansData = ", loansData); + + assert.equal(loansData.length, 0); + }); + }); + + describe("Test LoanMaintenance::getUserLoans and getActiveLoans (V2 LoanData)", () => { + it("should return empty if start >= end", async () => { + // no loan created + const loansData = await sovryn.getUserLoansV2(accounts[1], 0, 10, 0, 0, 0); /// @dev parameters: user, start, count, loanType, isLender, unsafeOnly + assert.equal(loansData.length, 0); + }); + + it("should return empty if count == 0", async () => { + // prepare the test + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + + const loansData = await sovryn.getUserLoansV2(borrower, 0, 0, 0, 0, 0); /// @dev parameters: user, start, count, loanType, isLender, unsafeOnly + assert.equal(loansData.length, 0); + }); + + it("should exist a loan, check values", async () => { + // prepare the test + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + + const loansData = await sovryn.getUserLoansV2(borrower, 0, 10, 0, 0, 0); /// @dev parameters: user, start, count, loanType, isLender, unsafeOnly + // console.log("loansData = ", loansData); + + assert.equal(loansData[0]["loanId"], loan_id); + assert.equal(loansData[0]["loanToken"], SUSD.address); + assert.equal(loansData[0]["collateralToken"], RBTC.address); + assert.equal(loansData[0]["borrower"], borrower); + }); + + it("should not exist an unsafe loan", async () => { + // prepare the test + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + + // Check UnsafeOnly, no results to expect + const loansDataUnsafeOnly = await sovryn.getUserLoansV2(borrower, 0, 10, 0, 0, true); /// @dev parameters: user, start, count, loanType, isLender, unsafeOnly + // console.log("loansDataUnsafeOnly = ", loansDataUnsafeOnly); + + assert.equal(loansDataUnsafeOnly.length, 0); + }); + + it("should exist an active loan, check values", async () => { + // prepare the test + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + + const loansData = await sovryn.getActiveLoansV2(0, 10, 0); /// @dev parameters: start, count, unsafeOnly + // console.log("loansData = ", loansData); + + assert.equal(loansData[0]["loanId"], loan_id); + assert.equal(loansData[0]["loanToken"], SUSD.address); + assert.equal(loansData[0]["collateralToken"], RBTC.address); + assert.equal(loansData[0]["borrower"], borrower); + }); + + it("Coverage to avoid final conditional (itemCount < count) on getActiveLoans", async () => { + // prepare the test + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + + const loansData = await sovryn.getActiveLoansV2(0, 1, 0); /// @dev parameters: start, count, unsafeOnly + // console.log("loansData = ", loansData); + + assert.equal(loansData[0]["loanId"], loan_id); + assert.equal(loansData[0]["loanToken"], SUSD.address); + assert.equal(loansData[0]["collateralToken"], RBTC.address); + assert.equal(loansData[0]["borrower"], borrower); + }); + + it("Coverage to meet the conditional (start >= end) on getActiveLoans", async () => { + // prepare the test + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + + const loansData = await sovryn.getActiveLoansV2(1, 1, 0); /// @dev parameters: start, count, unsafeOnly + // console.log("loansData = ", loansData); + + assert.equal(loansData.length, 0); + }); + + it("should not exist any active unsafe loan", async () => { + // prepare the test + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + + // Check UnsafeOnly, no results to expect + const loansDataUnsafeOnly = await sovryn.getActiveLoansV2(0, 10, true); /// @dev parameters: start, count, unsafeOnly + // console.log("loansDataUnsafeOnly = ", loansDataUnsafeOnly); + + assert.equal(loansDataUnsafeOnly.length, 0); + }); + + it("should get a loanType 1: a margin trade loan", async () => { + // prepare the test + const [loan_id] = await open_margin_trade_position( + loanToken, + RBTC, + WRBTC, + SUSD, + accounts[1] + ); + + const loansData = await sovryn.getUserLoansV2(accounts[1], 0, 10, 1, 0, 0); /// @dev parameters: user, start, count, loanType, isLender, unsafeOnly + // console.log("loansData = ", loansData); + + assert.equal(loansData[0]["loanId"], loan_id); + assert.equal(loansData[0]["loanToken"], SUSD.address); + assert.equal(loansData[0]["collateralToken"], RBTC.address); + assert.equal(loansData[0]["borrower"], accounts[1]); + }); + + it("should get a loanType 2: a non-margin trade loan", async () => { + // prepare the test + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + + const loansData = await sovryn.getUserLoansV2(borrower, 0, 10, 2, 0, 0); /// @dev parameters: user, start, count, loanType, isLender, unsafeOnly + // console.log("loansData = ", loansData); + + assert.equal(loansData[0]["loanId"], loan_id); + assert.equal(loansData[0]["loanToken"], SUSD.address); + assert.equal(loansData[0]["collateralToken"], RBTC.address); + assert.equal(loansData[0]["borrower"], borrower); + }); + + it("Coverage to avoid final conditional (itemCount < count) on getUserLoans", async () => { + // prepare the test + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + + const loansData = await sovryn.getUserLoansV2(borrower, 0, 1, 2, 0, 0); /// @dev parameters: user, start, count, loanType, isLender, unsafeOnly + // console.log("loansData = ", loansData); + + assert.equal(loansData[0]["loanId"], loan_id); + assert.equal(loansData[0]["loanToken"], SUSD.address); + assert.equal(loansData[0]["collateralToken"], RBTC.address); + assert.equal(loansData[0]["borrower"], borrower); + }); + + it("Coverage to meet the conditional (start >= end) on getUserLoans", async () => { + // prepare the test + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + + const loansData = await sovryn.getUserLoansV2(borrower, 1, 1, 2, 0, 0); /// @dev parameters: user, start, count, loanType, isLender, unsafeOnly + // console.log("loansData = ", loansData); + + assert.equal(loansData.length, 0); + }); + }); + + describe("Test set new special rebates.", () => { + it("Test set new special rebates for specific pairs", async () => { + const invalidValue = wei("1001", "ether"); + await expectRevert( + sovryn.setSpecialRebates(SUSD.address, RBTC.address, 0, { from: accounts[8] }), + "unauthorized" + ); + await expectRevert( + sovryn.setSpecialRebates(SUSD.address, RBTC.address, invalidValue), + "Special fee rebate is too high" + ); + + const validValue = wei("200", "ether"); + await sovryn.setSpecialRebates(SUSD.address, RBTC.address, validValue); + expect( + (await sovryn.specialRebates(SUSD.address, RBTC.address)).toString(), + "Incorrect new special fee rebates" + ).to.be.equal(validValue.toString()); + }); + }); + + describe("Test extending and reducing loan durations.", () => { + /* At this moment the maxLoanTerm is always 28 because it is hardcoded in setupLoanParams. So there are only fix-term loans. */ - it("Test extend fix term loan duration should fail", async () => { - // prepare the test - const [loan_id, trader, loan_token_sent] = await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, owner); - const loanMaintenance = await LoanMaintenance.at(sovryn.address); - await expectRevert( - loanMaintenance.extendLoanDuration(loan_id, loan_token_sent, false, "0x", { from: trader }), - "indefinite-term only" - ); - }); - - /* + it("Test extend fix term loan duration should fail", async () => { + // prepare the test + const [loan_id, trader, loan_token_sent] = await open_margin_trade_position( + loanToken, + RBTC, + WRBTC, + SUSD, + owner + ); + const loanMaintenance = await LoanMaintenance.at(sovryn.address); + await expectRevert( + loanMaintenance.extendLoanDuration(loan_id, loan_token_sent, false, "0x", { + from: trader, + }), + "indefinite-term only" + ); + }); + + /* Extend the loan duration and see if the new timestamp is the expected, the interest increase, the borrower SUSD balance decrease and the sovryn SUSD balance increase */ - it("Test extend loan duration", async () => { - // prepare the test - const [loan_id, borrower] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - const initial_loan = await sovryn.getLoan(loan_id); - const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); - const initial_loan_token_lender_balance = await SUSD.balanceOf(sovryn.address); - - const days_to_extend = new BN(10); - const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; - const deposit_amount = owed_per_day.mul(days_to_extend); - - // Approve the transfer of loan token - await SUSD.mint(borrower, deposit_amount); - await SUSD.approve(sovryn.address, deposit_amount, { from: borrower }); - const initial_borrower_balance = await SUSD.balanceOf(borrower); - - const loanMaintenance = await LoanMaintenance.at(sovryn.address); - await loanMaintenance.extendLoanDuration(loan_id, deposit_amount, false, "0x", { from: borrower }); - - const end_loan_interest_data = await sovryn.getLoanInterestData(loan_id); - const end_loan = await sovryn.getLoan(loan_id); - - expect(end_loan["endTimestamp"] == parseInt(initial_loan["endTimestamp"]) + days_to_extend.toNumber() * 24 * 60 * 60).to.be - .true; - expect( - end_loan_interest_data["interestDepositTotal"].eq(initial_loan_interest_data["interestDepositTotal"].add(deposit_amount)) - ).to.be.true; - expect((await SUSD.balanceOf(borrower)).eq(initial_borrower_balance.sub(deposit_amount))).to.be.true; - // Due to block timestamp could be paying outstanding interest to lender or not - expect((await SUSD.balanceOf(sovryn.address)).lte(initial_loan_token_lender_balance.add(deposit_amount))).to.be.true; - }); - - it("Test extend loan duration and loan endtime passed but the loan remained open", async () => { - // prepare the test - const [loan_id, borrower] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); - const loanMaintenance = await LoanMaintenance.at(sovryn.address); - - const days_to_extend = new BN(10); - const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; - const deposit_amount = owed_per_day.mul(days_to_extend); - - // Approve the transfer of loan token - await SUSD.mint(borrower, deposit_amount); - await SUSD.approve(sovryn.address, deposit_amount, { from: borrower }); - - // 5 days after loan endtime is overpassed - await increaseTime(15 * 86400); - await expectRevert( - loanMaintenance.extendLoanDuration(loan_id, deposit_amount, false, "0x", { from: borrower }), - "loan too short" - ); - - // 20 days after loan endtime is overpassed - await increaseTime(15 * 86400); - await expectRevert( - loanMaintenance.extendLoanDuration(loan_id, deposit_amount, false, "0x", { from: borrower }), - "deposit cannot cover back interest" - ); - }); - - it("Test extend loan_duration 0 deposit should fail", async () => { - // prepare the test - const [loan_id, borrower] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - const loanMaintenance = await LoanMaintenance.at(sovryn.address); - await expectRevert(loanMaintenance.extendLoanDuration(loan_id, 0, false, "0x", { from: borrower }), "depositAmount is 0"); - }); - - it("Test extend closed loan_duration should fail", async () => { - // prepare the test - const [loan_id, borrower, , , , , args] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - const collateral = args["newCollateral"]; - const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); - - const days_to_extend = new BN(10); - const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; - const deposit_amount = owed_per_day.mul(days_to_extend); - - await sovryn.closeWithSwap(loan_id, borrower, collateral, false, "0x", { from: borrower }); - - const loanMaintenance = await LoanMaintenance.at(sovryn.address); - await expectRevert( - loanMaintenance.extendLoanDuration(loan_id, deposit_amount, false, "0x", { from: borrower }), - "loan is closed" - ); - }); - - it("Test extend loan_duration user unauthorized should fail", async () => { - // prepare the test - const [loan_id, , receiver, , , ,] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); - - const days_to_extend = new BN(10); - const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; - const deposit_amount = owed_per_day.mul(days_to_extend); - - const loanMaintenance = await LoanMaintenance.at(sovryn.address); - await expectRevert(loanMaintenance.extendLoanDuration(loan_id, deposit_amount, true, "0x", { from: receiver }), "unauthorized"); - }); - - /* + it("Test extend loan duration", async () => { + // prepare the test + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + const initial_loan = await sovryn.getLoan(loan_id); + const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); + const initial_loan_token_lender_balance = await SUSD.balanceOf(sovryn.address); + + const days_to_extend = new BN(10); + const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; + const deposit_amount = owed_per_day.mul(days_to_extend); + + // Approve the transfer of loan token + await SUSD.mint(borrower, deposit_amount); + await SUSD.approve(sovryn.address, deposit_amount, { from: borrower }); + const initial_borrower_balance = await SUSD.balanceOf(borrower); + + const loanMaintenance = await LoanMaintenance.at(sovryn.address); + await loanMaintenance.extendLoanDuration(loan_id, deposit_amount, false, "0x", { + from: borrower, + }); + + const end_loan_interest_data = await sovryn.getLoanInterestData(loan_id); + const end_loan = await sovryn.getLoan(loan_id); + + expect( + end_loan["endTimestamp"] == + parseInt(initial_loan["endTimestamp"]) + + days_to_extend.toNumber() * 24 * 60 * 60 + ).to.be.true; + expect( + end_loan_interest_data["interestDepositTotal"].eq( + initial_loan_interest_data["interestDepositTotal"].add(deposit_amount) + ) + ).to.be.true; + expect( + (await SUSD.balanceOf(borrower)).eq(initial_borrower_balance.sub(deposit_amount)) + ).to.be.true; + // Due to block timestamp could be paying outstanding interest to lender or not + expect( + (await SUSD.balanceOf(sovryn.address)).lte( + initial_loan_token_lender_balance.add(deposit_amount) + ) + ).to.be.true; + }); + + it("Test extend loan duration and loan endtime passed but the loan remained open", async () => { + // prepare the test + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); + const loanMaintenance = await LoanMaintenance.at(sovryn.address); + + const days_to_extend = new BN(10); + const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; + const deposit_amount = owed_per_day.mul(days_to_extend); + + // Approve the transfer of loan token + await SUSD.mint(borrower, deposit_amount); + await SUSD.approve(sovryn.address, deposit_amount, { from: borrower }); + + // 5 days after loan endtime is overpassed + await increaseTime(15 * 86400); + await expectRevert( + loanMaintenance.extendLoanDuration(loan_id, deposit_amount, false, "0x", { + from: borrower, + }), + "loan too short" + ); + + // 20 days after loan endtime is overpassed + await increaseTime(15 * 86400); + await expectRevert( + loanMaintenance.extendLoanDuration(loan_id, deposit_amount, false, "0x", { + from: borrower, + }), + "deposit cannot cover back interest" + ); + }); + + it("Test extend loan_duration 0 deposit should fail", async () => { + // prepare the test + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + const loanMaintenance = await LoanMaintenance.at(sovryn.address); + await expectRevert( + loanMaintenance.extendLoanDuration(loan_id, 0, false, "0x", { from: borrower }), + "depositAmount is 0" + ); + }); + + it("Test extend closed loan_duration should fail", async () => { + // prepare the test + const [loan_id, borrower, , , , , args] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + const collateral = args["newCollateral"]; + const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); + + const days_to_extend = new BN(10); + const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; + const deposit_amount = owed_per_day.mul(days_to_extend); + + await sovryn.closeWithSwap(loan_id, borrower, collateral, false, "0x", { + from: borrower, + }); + + const loanMaintenance = await LoanMaintenance.at(sovryn.address); + await expectRevert( + loanMaintenance.extendLoanDuration(loan_id, deposit_amount, false, "0x", { + from: borrower, + }), + "loan is closed" + ); + }); + + it("Test extend loan_duration user unauthorized should fail", async () => { + // prepare the test + const [loan_id, , receiver, , , ,] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); + + const days_to_extend = new BN(10); + const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; + const deposit_amount = owed_per_day.mul(days_to_extend); + + const loanMaintenance = await LoanMaintenance.at(sovryn.address); + await expectRevert( + loanMaintenance.extendLoanDuration(loan_id, deposit_amount, true, "0x", { + from: receiver, + }), + "unauthorized" + ); + }); + + /* Extend the loan duration with collateral and see if the new timestamp is the expected, the interest increase, the loan's collateral decrease, sovryn SUSD balance increase and RBTC decrease */ - it("Test extend loan_duration with collateral", async () => { - // prepare the test - const [loan_id, borrower] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - - const initial_loan = await sovryn.getLoan(loan_id); - const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); - const initial_loan_token_lender_balance = await SUSD.balanceOf(sovryn.address); - const initial_collateral_token_lender_balance = await RBTC.balanceOf(sovryn.address); - - const days_to_extend = new BN(10); - const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; - const deposit_amount = owed_per_day.mul(days_to_extend); - - const { rate, precision } = await priceFeeds.queryRate(RBTC.address, SUSD.address); - let deposit_amount_in_collateral = deposit_amount.mul(precision).div(rate); - - // if the actual rate is exactly the same as the worst case rate, we get rounding issues. So, add a small buffer. - // buffer = min(estimatedSourceAmount/1000 , sourceBuffer) with sourceBuffer = 10000 - // code from SwapsImplSovrynSwap.sol - - let buffer = deposit_amount_in_collateral.div(new BN(1000)); - if (buffer.gt(new BN(10000))) buffer = new BN(10000); - deposit_amount_in_collateral = deposit_amount_in_collateral.add(buffer); - - const loanMaintenance = await LoanMaintenance.at(sovryn.address); - await loanMaintenance.extendLoanDuration(loan_id, deposit_amount, true, "0x", { from: borrower }); - - const end_loan_interest_data = await sovryn.getLoanInterestData(loan_id); - const end_loan = await sovryn.getLoan(loan_id); - - expect(end_loan["endTimestamp"] == parseInt(initial_loan["endTimestamp"]) + days_to_extend.toNumber() * 24 * 60 * 60).to.be - .true; - expect( - end_loan_interest_data["interestDepositTotal"].eq(initial_loan_interest_data["interestDepositTotal"].add(deposit_amount)) - ).to.be.true; - expect(new BN(end_loan["collateral"])).to.be.a.bignumber.eq( - new BN(initial_loan["collateral"]).sub(deposit_amount_in_collateral) - ); - expect(await RBTC.balanceOf(sovryn.address)).to.be.a.bignumber.eq( - initial_collateral_token_lender_balance.sub(deposit_amount_in_collateral) - ); - expect((await SUSD.balanceOf(sovryn.address)).lte(initial_loan_token_lender_balance.add(deposit_amount))).to.be.true; - }); - - it("EarnRewardFail events should be fired if lockedSOV reverted when extend loan_duration with collateral sov token reward (special rebates not set or 0)", async () => { - // prepare the test - await sovryn.setLockedSOVAddress((await LockedSOVFailedMockup.new(SOV.address, [owner])).address); - const [loan_id, borrower] = await borrow_indefinite_loan( - loanToken, - sovryn, - SUSD, - RBTC, - accounts, - (withdraw_amount = new BN(10).mul(oneEth).toString()), - (margin = new BN(50).mul(oneEth).toString()), - (duration_in_seconds = 60 * 60 * 24 * 20) - ); - - const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); - - const days_to_extend = new BN(10); - const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; - const deposit_amount = owed_per_day.mul(days_to_extend); - - await increaseTime(10 * 24 * 60 * 60); - lockedSOV = await LockedSOVFailedMockup.at(await sovryn.lockedSOVAddress()); - const borrower_initial_balance_before_extend = (await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower)); - - const loanMaintenance = await LoanMaintenance.at(sovryn.address); - const { receipt } = await loanMaintenance.extendLoanDuration(loan_id, deposit_amount, true, "0x", { from: borrower }); - const borrower_initial_balance = borrower_initial_balance_before_extend.sub( - (await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower)) - ); - - expect(borrower_initial_balance_before_extend.toString()).to.eq(borrower_initial_balance.toString()); - - const feeRebatePercent = await sovryn.feeRebatePercent(); - const decode = decodeLogs(receipt.rawLogs, FeesEvents, "EarnRewardFail"); - const args = decode[0].args; - expect(args["receiver"] == borrower).to.be.true; - expect(args["token"] == SOV.address).to.be.true; - expect(args["loanId"] == loan_id).to.be.true; - expect(args["feeRebatePercent"] == feeRebatePercent).to.be.true; - expect(args["basisPoint"] == 0).to.be.true; - }); - - it("EarnRewardFail events should be fired if lockedSOV reverted when extend loan_duration with collateral sov token reward (special rebates not set or 0) with basis point applied", async () => { - const basisPoint = 9000; - // prepare the test - await sovryn.setLockedSOVAddress((await LockedSOVFailedMockup.new(SOV.address, [owner])).address); - await sovryn.setTradingRebateRewardsBasisPoint(basisPoint); - const [loan_id, borrower] = await borrow_indefinite_loan( - loanToken, - sovryn, - SUSD, - RBTC, - accounts, - (withdraw_amount = new BN(10).mul(oneEth).toString()), - (margin = new BN(50).mul(oneEth).toString()), - (duration_in_seconds = 60 * 60 * 24 * 20) - ); - - const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); - - const days_to_extend = new BN(10); - const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; - const deposit_amount = owed_per_day.mul(days_to_extend); - - await increaseTime(10 * 24 * 60 * 60); - lockedSOV = await LockedSOVFailedMockup.at(await sovryn.lockedSOVAddress()); - const borrower_initial_balance_before_extend = (await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower)); - - const loanMaintenance = await LoanMaintenance.at(sovryn.address); - const { receipt } = await loanMaintenance.extendLoanDuration(loan_id, deposit_amount, true, "0x", { from: borrower }); - const borrower_initial_balance = borrower_initial_balance_before_extend.sub( - (await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower)) - ); - - expect(borrower_initial_balance_before_extend.toString()).to.eq(borrower_initial_balance.toString()); - - const feeRebatePercent = await sovryn.feeRebatePercent(); - const decode = decodeLogs(receipt.rawLogs, FeesEvents, "EarnRewardFail"); - const args = decode[0].args; - expect(args["receiver"] == borrower).to.be.true; - expect(args["token"] == SOV.address).to.be.true; - expect(args["loanId"] == loan_id).to.be.true; - expect(args["feeRebatePercent"] == feeRebatePercent).to.be.true; - expect(args["basisPoint"] == basisPoint).to.be.true; - }); - - it("EarnRewardFail events should be fired if lockedSOV reverted when extend loan_duration with collateral sov token reward payment (special rebates is set)", async () => { - // prepare the test - await sovryn.setLockedSOVAddress((await LockedSOVFailedMockup.new(SOV.address, [owner])).address); - await sovryn.setSpecialRebates(SUSD.address, RBTC.address, wei("200", "ether")); - const [loan_id, borrower] = await borrow_indefinite_loan( - loanToken, - sovryn, - SUSD, - RBTC, - accounts, - (withdraw_amount = new BN(10).mul(oneEth).toString()), - (margin = new BN(50).mul(oneEth).toString()), - (duration_in_seconds = 60 * 60 * 24 * 20) - ); - - const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); - - const days_to_extend = new BN(10); - const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; - const deposit_amount = owed_per_day.mul(days_to_extend); - - await increaseTime(10 * 24 * 60 * 60); - lockedSOV = await LockedSOVFailedMockup.at(await sovryn.lockedSOVAddress()); - const borrower_initial_balance_before_extend = (await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower)); - - const loanMaintenance = await LoanMaintenance.at(sovryn.address); - const { receipt } = await loanMaintenance.extendLoanDuration(loan_id, deposit_amount, true, "0x", { from: borrower }); - const borrower_initial_balance = borrower_initial_balance_before_extend.sub( - (await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower)) - ); - - expect(borrower_initial_balance_before_extend.toString()).to.eq(borrower_initial_balance.toString()); - - const feeRebatePercent = await sovryn.specialRebates(SUSD.address, RBTC.address); - const decode = decodeLogs(receipt.rawLogs, FeesEvents, "EarnRewardFail"); - const args = decode[0].args; - expect(args["receiver"] == borrower).to.be.true; - expect(args["token"] == SOV.address).to.be.true; - expect(args["loanId"] == loan_id).to.be.true; - expect(args["feeRebatePercent"] == feeRebatePercent).to.be.true; - expect(args["basisPoint"] == 0).to.be.true; - }); - - it("Test extend loan_duration with collateral sov token reward payment (special rebates not set / 0)", async () => { - // prepare the test - const [loan_id, borrower] = await borrow_indefinite_loan( - loanToken, - sovryn, - SUSD, - RBTC, - accounts, - (withdraw_amount = new BN(10).mul(oneEth).toString()), - (margin = new BN(50).mul(oneEth).toString()), - (duration_in_seconds = 60 * 60 * 24 * 20) - ); - - const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); - - const days_to_extend = new BN(10); - const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; - const deposit_amount = owed_per_day.mul(days_to_extend); - - await increaseTime(10 * 24 * 60 * 60); - lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); - const borrower_initial_balance_before_extend = (await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower)); - - const loanMaintenance = await LoanMaintenance.at(sovryn.address); - const { receipt } = await loanMaintenance.extendLoanDuration(loan_id, deposit_amount, true, "0x", { from: borrower }); - const borrower_initial_balance = borrower_initial_balance_before_extend.sub( - (await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower)) - ); - - const feeRebatePercent = await sovryn.feeRebatePercent(); - const decode = decodeLogs(receipt.rawLogs, FeesEvents, "EarnReward"); - const args = decode[0].args; - expect(args["receiver"] == borrower).to.be.true; - expect(args["token"] == SOV.address).to.be.true; - expect(args["loanId"] == loan_id).to.be.true; - expect(args["amount"]).to.eq((await SOV.balanceOf(borrower)).sub(borrower_initial_balance).toString()); - expect(args["feeRebatePercent"] == feeRebatePercent).to.be.true; - expect(args["basisPoint"] == 0).to.be.true; - }); - - it("Test extend loan_duration with collateral sov token reward payment (special rebates is set) & basis point applied", async () => { - const basisPoint = 9000; - // prepare the test - await sovryn.setSpecialRebates(SUSD.address, RBTC.address, wei("200", "ether")); - await sovryn.setTradingRebateRewardsBasisPoint(basisPoint); - const [loan_id, borrower] = await borrow_indefinite_loan( - loanToken, - sovryn, - SUSD, - RBTC, - accounts, - (withdraw_amount = new BN(10).mul(oneEth).toString()), - (margin = new BN(50).mul(oneEth).toString()), - (duration_in_seconds = 60 * 60 * 24 * 20) - ); - - const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); - - const days_to_extend = new BN(10); - const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; - const deposit_amount = owed_per_day.mul(days_to_extend); - - await increaseTime(10 * 24 * 60 * 60); - lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); - const borrower_initial_balance_before_extend = (await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower)); - const borrower_initial_unlock_balance_before_extend = (await SOV.balanceOf(borrower)).add( - await lockedSOV.getUnlockedBalance(borrower) - ); - - const loanMaintenance = await LoanMaintenance.at(sovryn.address); - const { receipt } = await loanMaintenance.extendLoanDuration(loan_id, deposit_amount, true, "0x", { from: borrower }); - - const feeRebatePercent = await sovryn.specialRebates(SUSD.address, RBTC.address); - const decode = decodeLogs(receipt.rawLogs, FeesEvents, "EarnReward"); - const args = decode[0].args; - expect(args["receiver"] == borrower).to.be.true; - expect(args["token"] == SOV.address).to.be.true; - expect(args["loanId"] == loan_id).to.be.true; - expect(args["feeRebatePercent"] == feeRebatePercent).to.be.true; - expect(args["basisPoint"] == basisPoint).to.be.true; - - // vested SOV rewards - vestedSOVRewards = new BN(args["amount"]).mul(new BN(10000).sub(new BN(basisPoint))).divRound(new BN(10000)); - /// @dev The following check may suffer rounding error - /// @dev So, we ignore differences in least significant digits - expect( - Math.abs( - vestedSOVRewards - .add(borrower_initial_balance_before_extend) - .sub(await lockedSOV.getLockedBalance(borrower)) - .toNumber() - ) < 100 - ).to.be.true; - - // liquid SOV rewards - liquidSOVRewards = new BN(args["amount"]).mul(new BN(basisPoint)).divRound(new BN(10000)); - expect(liquidSOVRewards.toString()).to.eq( - (await lockedSOV.getUnlockedBalance(borrower)).sub(borrower_initial_unlock_balance_before_extend).toString() - ); - }); - - it("Test extend loan_duration with collateral sov token reward payment (special rebates is set)", async () => { - // prepare the test - await sovryn.setSpecialRebates(SUSD.address, RBTC.address, wei("200", "ether")); - const [loan_id, borrower] = await borrow_indefinite_loan( - loanToken, - sovryn, - SUSD, - RBTC, - accounts, - (withdraw_amount = new BN(10).mul(oneEth).toString()), - (margin = new BN(50).mul(oneEth).toString()), - (duration_in_seconds = 60 * 60 * 24 * 20) - ); - - const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); - - const days_to_extend = new BN(10); - const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; - const deposit_amount = owed_per_day.mul(days_to_extend); - - await increaseTime(10 * 24 * 60 * 60); - lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); - const borrower_initial_balance_before_extend = (await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower)); - - const loanMaintenance = await LoanMaintenance.at(sovryn.address); - const { receipt } = await loanMaintenance.extendLoanDuration(loan_id, deposit_amount, true, "0x", { from: borrower }); - const borrower_initial_balance = borrower_initial_balance_before_extend.sub( - (await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower)) - ); - - const feeRebatePercent = await sovryn.specialRebates(SUSD.address, RBTC.address); - const decode = decodeLogs(receipt.rawLogs, FeesEvents, "EarnReward"); - const args = decode[0].args; - expect(args["receiver"] == borrower).to.be.true; - expect(args["token"] == SOV.address).to.be.true; - expect(args["loanId"] == loan_id).to.be.true; - expect(args["amount"]).to.eq((await SOV.balanceOf(borrower)).sub(borrower_initial_balance).toString()); - expect(args["feeRebatePercent"] == feeRebatePercent).to.be.true; - expect(args["basisPoint"] == 0).to.be.true; - }); - - it("Test extend loan_duration with collateral and eth should fail", async () => { - // prepare the test - const [loan_id, borrower] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - - const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); - - const days_to_extend = new BN(10); - const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; - const deposit_amount = owed_per_day.mul(days_to_extend); - - const loanMaintenance = await LoanMaintenance.at(sovryn.address); - await expectRevert( - loanMaintenance.extendLoanDuration(loan_id, deposit_amount, true, "0x", { from: borrower, value: deposit_amount }), - "wrong asset sent" - ); - }); - - it("Test reduce fix term loan duration should fail", async () => { - // prepare the test - const [loan_id, trader, loan_token_sent] = await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, owner); - const loanMaintenance = await LoanMaintenance.at(sovryn.address); - await expectRevert( - loanMaintenance.reduceLoanDuration(loan_id, trader, loan_token_sent, { from: trader }), - "indefinite-term only" - ); - }); - - /* + it("Test extend loan_duration with collateral", async () => { + // prepare the test + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + + const initial_loan = await sovryn.getLoan(loan_id); + const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); + const initial_loan_token_lender_balance = await SUSD.balanceOf(sovryn.address); + const initial_collateral_token_lender_balance = await RBTC.balanceOf(sovryn.address); + + const days_to_extend = new BN(10); + const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; + const deposit_amount = owed_per_day.mul(days_to_extend); + + const { rate, precision } = await priceFeeds.queryRate(RBTC.address, SUSD.address); + let deposit_amount_in_collateral = deposit_amount.mul(precision).div(rate); + + // if the actual rate is exactly the same as the worst case rate, we get rounding issues. So, add a small buffer. + // buffer = min(estimatedSourceAmount/1000 , sourceBuffer) with sourceBuffer = 10000 + // code from SwapsImplSovrynSwap.sol + + let buffer = deposit_amount_in_collateral.div(new BN(1000)); + if (buffer.gt(new BN(10000))) buffer = new BN(10000); + deposit_amount_in_collateral = deposit_amount_in_collateral.add(buffer); + + const loanMaintenance = await LoanMaintenance.at(sovryn.address); + await loanMaintenance.extendLoanDuration(loan_id, deposit_amount, true, "0x", { + from: borrower, + }); + + const end_loan_interest_data = await sovryn.getLoanInterestData(loan_id); + const end_loan = await sovryn.getLoan(loan_id); + + expect( + end_loan["endTimestamp"] == + parseInt(initial_loan["endTimestamp"]) + + days_to_extend.toNumber() * 24 * 60 * 60 + ).to.be.true; + expect( + end_loan_interest_data["interestDepositTotal"].eq( + initial_loan_interest_data["interestDepositTotal"].add(deposit_amount) + ) + ).to.be.true; + expect(new BN(end_loan["collateral"])).to.be.a.bignumber.eq( + new BN(initial_loan["collateral"]).sub(deposit_amount_in_collateral) + ); + expect(await RBTC.balanceOf(sovryn.address)).to.be.a.bignumber.eq( + initial_collateral_token_lender_balance.sub(deposit_amount_in_collateral) + ); + expect( + (await SUSD.balanceOf(sovryn.address)).lte( + initial_loan_token_lender_balance.add(deposit_amount) + ) + ).to.be.true; + }); + + it("EarnRewardFail events should be fired if lockedSOV reverted when extend loan_duration with collateral sov token reward (special rebates not set or 0)", async () => { + // prepare the test + await sovryn.setLockedSOVAddress( + ( + await LockedSOVFailedMockup.new(SOV.address, [owner]) + ).address + ); + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts, + (withdraw_amount = new BN(10).mul(oneEth).toString()), + (margin = new BN(50).mul(oneEth).toString()), + (duration_in_seconds = 60 * 60 * 24 * 20) + ); + + const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); + + const days_to_extend = new BN(10); + const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; + const deposit_amount = owed_per_day.mul(days_to_extend); + + await increaseTime(10 * 24 * 60 * 60); + lockedSOV = await LockedSOVFailedMockup.at(await sovryn.lockedSOVAddress()); + const borrower_initial_balance_before_extend = (await SOV.balanceOf(borrower)).add( + await lockedSOV.getLockedBalance(borrower) + ); + + const loanMaintenance = await LoanMaintenance.at(sovryn.address); + const { receipt } = await loanMaintenance.extendLoanDuration( + loan_id, + deposit_amount, + true, + "0x", + { from: borrower } + ); + const borrower_initial_balance = borrower_initial_balance_before_extend.sub( + (await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower)) + ); + + expect(borrower_initial_balance_before_extend.toString()).to.eq( + borrower_initial_balance.toString() + ); + + const feeRebatePercent = await sovryn.feeRebatePercent(); + const decode = decodeLogs(receipt.rawLogs, FeesEvents, "EarnRewardFail"); + const args = decode[0].args; + expect(args["receiver"] == borrower).to.be.true; + expect(args["token"] == SOV.address).to.be.true; + expect(args["loanId"] == loan_id).to.be.true; + expect(args["feeRebatePercent"] == feeRebatePercent).to.be.true; + expect(args["basisPoint"] == 0).to.be.true; + }); + + it("EarnRewardFail events should be fired if lockedSOV reverted when extend loan_duration with collateral sov token reward (special rebates not set or 0) with basis point applied", async () => { + const basisPoint = 9000; + // prepare the test + await sovryn.setLockedSOVAddress( + ( + await LockedSOVFailedMockup.new(SOV.address, [owner]) + ).address + ); + await sovryn.setTradingRebateRewardsBasisPoint(basisPoint); + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts, + (withdraw_amount = new BN(10).mul(oneEth).toString()), + (margin = new BN(50).mul(oneEth).toString()), + (duration_in_seconds = 60 * 60 * 24 * 20) + ); + + const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); + + const days_to_extend = new BN(10); + const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; + const deposit_amount = owed_per_day.mul(days_to_extend); + + await increaseTime(10 * 24 * 60 * 60); + lockedSOV = await LockedSOVFailedMockup.at(await sovryn.lockedSOVAddress()); + const borrower_initial_balance_before_extend = (await SOV.balanceOf(borrower)).add( + await lockedSOV.getLockedBalance(borrower) + ); + + const loanMaintenance = await LoanMaintenance.at(sovryn.address); + const { receipt } = await loanMaintenance.extendLoanDuration( + loan_id, + deposit_amount, + true, + "0x", + { from: borrower } + ); + const borrower_initial_balance = borrower_initial_balance_before_extend.sub( + (await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower)) + ); + + expect(borrower_initial_balance_before_extend.toString()).to.eq( + borrower_initial_balance.toString() + ); + + const feeRebatePercent = await sovryn.feeRebatePercent(); + const decode = decodeLogs(receipt.rawLogs, FeesEvents, "EarnRewardFail"); + const args = decode[0].args; + expect(args["receiver"] == borrower).to.be.true; + expect(args["token"] == SOV.address).to.be.true; + expect(args["loanId"] == loan_id).to.be.true; + expect(args["feeRebatePercent"] == feeRebatePercent).to.be.true; + expect(args["basisPoint"] == basisPoint).to.be.true; + }); + + it("EarnRewardFail events should be fired if lockedSOV reverted when extend loan_duration with collateral sov token reward payment (special rebates is set)", async () => { + // prepare the test + await sovryn.setLockedSOVAddress( + ( + await LockedSOVFailedMockup.new(SOV.address, [owner]) + ).address + ); + await sovryn.setSpecialRebates(SUSD.address, RBTC.address, wei("200", "ether")); + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts, + (withdraw_amount = new BN(10).mul(oneEth).toString()), + (margin = new BN(50).mul(oneEth).toString()), + (duration_in_seconds = 60 * 60 * 24 * 20) + ); + + const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); + + const days_to_extend = new BN(10); + const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; + const deposit_amount = owed_per_day.mul(days_to_extend); + + await increaseTime(10 * 24 * 60 * 60); + lockedSOV = await LockedSOVFailedMockup.at(await sovryn.lockedSOVAddress()); + const borrower_initial_balance_before_extend = (await SOV.balanceOf(borrower)).add( + await lockedSOV.getLockedBalance(borrower) + ); + + const loanMaintenance = await LoanMaintenance.at(sovryn.address); + const { receipt } = await loanMaintenance.extendLoanDuration( + loan_id, + deposit_amount, + true, + "0x", + { from: borrower } + ); + const borrower_initial_balance = borrower_initial_balance_before_extend.sub( + (await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower)) + ); + + expect(borrower_initial_balance_before_extend.toString()).to.eq( + borrower_initial_balance.toString() + ); + + const feeRebatePercent = await sovryn.specialRebates(SUSD.address, RBTC.address); + const decode = decodeLogs(receipt.rawLogs, FeesEvents, "EarnRewardFail"); + const args = decode[0].args; + expect(args["receiver"] == borrower).to.be.true; + expect(args["token"] == SOV.address).to.be.true; + expect(args["loanId"] == loan_id).to.be.true; + expect(args["feeRebatePercent"] == feeRebatePercent).to.be.true; + expect(args["basisPoint"] == 0).to.be.true; + }); + + it("Test extend loan_duration with collateral sov token reward payment (special rebates not set / 0)", async () => { + // prepare the test + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts, + (withdraw_amount = new BN(10).mul(oneEth).toString()), + (margin = new BN(50).mul(oneEth).toString()), + (duration_in_seconds = 60 * 60 * 24 * 20) + ); + + const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); + + const days_to_extend = new BN(10); + const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; + const deposit_amount = owed_per_day.mul(days_to_extend); + + await increaseTime(10 * 24 * 60 * 60); + lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); + const borrower_initial_balance_before_extend = (await SOV.balanceOf(borrower)).add( + await lockedSOV.getLockedBalance(borrower) + ); + + const loanMaintenance = await LoanMaintenance.at(sovryn.address); + const { receipt } = await loanMaintenance.extendLoanDuration( + loan_id, + deposit_amount, + true, + "0x", + { from: borrower } + ); + const borrower_initial_balance = borrower_initial_balance_before_extend.sub( + (await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower)) + ); + + const feeRebatePercent = await sovryn.feeRebatePercent(); + const decode = decodeLogs(receipt.rawLogs, FeesEvents, "EarnReward"); + const args = decode[0].args; + expect(args["receiver"] == borrower).to.be.true; + expect(args["token"] == SOV.address).to.be.true; + expect(args["loanId"] == loan_id).to.be.true; + expect(args["amount"]).to.eq( + (await SOV.balanceOf(borrower)).sub(borrower_initial_balance).toString() + ); + expect(args["feeRebatePercent"] == feeRebatePercent).to.be.true; + expect(args["basisPoint"] == 0).to.be.true; + }); + + it("Test extend loan_duration with collateral sov token reward payment (special rebates is set) & basis point applied", async () => { + const basisPoint = 9000; + // prepare the test + await sovryn.setSpecialRebates(SUSD.address, RBTC.address, wei("200", "ether")); + await sovryn.setTradingRebateRewardsBasisPoint(basisPoint); + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts, + (withdraw_amount = new BN(10).mul(oneEth).toString()), + (margin = new BN(50).mul(oneEth).toString()), + (duration_in_seconds = 60 * 60 * 24 * 20) + ); + + const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); + + const days_to_extend = new BN(10); + const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; + const deposit_amount = owed_per_day.mul(days_to_extend); + + await increaseTime(10 * 24 * 60 * 60); + lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); + const borrower_initial_balance_before_extend = (await SOV.balanceOf(borrower)).add( + await lockedSOV.getLockedBalance(borrower) + ); + const borrower_initial_unlock_balance_before_extend = ( + await SOV.balanceOf(borrower) + ).add(await lockedSOV.getUnlockedBalance(borrower)); + + const loanMaintenance = await LoanMaintenance.at(sovryn.address); + const { receipt } = await loanMaintenance.extendLoanDuration( + loan_id, + deposit_amount, + true, + "0x", + { from: borrower } + ); + + const feeRebatePercent = await sovryn.specialRebates(SUSD.address, RBTC.address); + const decode = decodeLogs(receipt.rawLogs, FeesEvents, "EarnReward"); + const args = decode[0].args; + expect(args["receiver"] == borrower).to.be.true; + expect(args["token"] == SOV.address).to.be.true; + expect(args["loanId"] == loan_id).to.be.true; + expect(args["feeRebatePercent"] == feeRebatePercent).to.be.true; + expect(args["basisPoint"] == basisPoint).to.be.true; + + // vested SOV rewards + vestedSOVRewards = new BN(args["amount"]) + .mul(new BN(10000).sub(new BN(basisPoint))) + .divRound(new BN(10000)); + /// @dev The following check may suffer rounding error + /// @dev So, we ignore differences in least significant digits + expect( + Math.abs( + vestedSOVRewards + .add(borrower_initial_balance_before_extend) + .sub(await lockedSOV.getLockedBalance(borrower)) + .toNumber() + ) < 100 + ).to.be.true; + + // liquid SOV rewards + liquidSOVRewards = new BN(args["amount"]) + .mul(new BN(basisPoint)) + .divRound(new BN(10000)); + expect(liquidSOVRewards.toString()).to.eq( + (await lockedSOV.getUnlockedBalance(borrower)) + .sub(borrower_initial_unlock_balance_before_extend) + .toString() + ); + }); + + it("Test extend loan_duration with collateral sov token reward payment (special rebates is set)", async () => { + // prepare the test + await sovryn.setSpecialRebates(SUSD.address, RBTC.address, wei("200", "ether")); + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts, + (withdraw_amount = new BN(10).mul(oneEth).toString()), + (margin = new BN(50).mul(oneEth).toString()), + (duration_in_seconds = 60 * 60 * 24 * 20) + ); + + const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); + + const days_to_extend = new BN(10); + const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; + const deposit_amount = owed_per_day.mul(days_to_extend); + + await increaseTime(10 * 24 * 60 * 60); + lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); + const borrower_initial_balance_before_extend = (await SOV.balanceOf(borrower)).add( + await lockedSOV.getLockedBalance(borrower) + ); + + const loanMaintenance = await LoanMaintenance.at(sovryn.address); + const { receipt } = await loanMaintenance.extendLoanDuration( + loan_id, + deposit_amount, + true, + "0x", + { from: borrower } + ); + const borrower_initial_balance = borrower_initial_balance_before_extend.sub( + (await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower)) + ); + + const feeRebatePercent = await sovryn.specialRebates(SUSD.address, RBTC.address); + const decode = decodeLogs(receipt.rawLogs, FeesEvents, "EarnReward"); + const args = decode[0].args; + expect(args["receiver"] == borrower).to.be.true; + expect(args["token"] == SOV.address).to.be.true; + expect(args["loanId"] == loan_id).to.be.true; + expect(args["amount"]).to.eq( + (await SOV.balanceOf(borrower)).sub(borrower_initial_balance).toString() + ); + expect(args["feeRebatePercent"] == feeRebatePercent).to.be.true; + expect(args["basisPoint"] == 0).to.be.true; + }); + + it("Test extend loan_duration with collateral and eth should fail", async () => { + // prepare the test + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + + const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); + + const days_to_extend = new BN(10); + const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; + const deposit_amount = owed_per_day.mul(days_to_extend); + + const loanMaintenance = await LoanMaintenance.at(sovryn.address); + await expectRevert( + loanMaintenance.extendLoanDuration(loan_id, deposit_amount, true, "0x", { + from: borrower, + value: deposit_amount, + }), + "wrong asset sent" + ); + }); + + it("Test reduce fix term loan duration should fail", async () => { + // prepare the test + const [loan_id, trader, loan_token_sent] = await open_margin_trade_position( + loanToken, + RBTC, + WRBTC, + SUSD, + owner + ); + const loanMaintenance = await LoanMaintenance.at(sovryn.address); + await expectRevert( + loanMaintenance.reduceLoanDuration(loan_id, trader, loan_token_sent, { + from: trader, + }), + "indefinite-term only" + ); + }); + + /* Reduce the loan duration and see if the new timestamp is the expected, the interest decrease, the receiver SUSD balance increase and the sovryn SUSD balance decrease */ - it("Test reduce loan duration", async () => { - // prepare the test - const [loan_id, borrower] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - const initial_loan = await sovryn.getLoan(loan_id); - const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); - const initial_loan_token_lender_balance = await SUSD.balanceOf(sovryn.address); - - const days_to_reduce = new BN(5); - const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; - const withdraw_amount = owed_per_day.mul(days_to_reduce); - - const receiver = accounts[3]; - const initial_receiver_balance = await SUSD.balanceOf(receiver); - - const loanMaintenance = await LoanMaintenance.at(sovryn.address); - await loanMaintenance.reduceLoanDuration(loan_id, receiver, withdraw_amount, { from: borrower }); - - const end_loan_interest_data = await sovryn.getLoanInterestData(loan_id); - const end_loan = await sovryn.getLoan(loan_id); - - expect(end_loan["endTimestamp"] == parseInt(initial_loan["endTimestamp"]) - days_to_reduce.toNumber() * 24 * 60 * 60).to.be - .true; - expect( - end_loan_interest_data["interestDepositTotal"].eq(initial_loan_interest_data["interestDepositTotal"].sub(withdraw_amount)) - ).to.be.true; - expect((await SUSD.balanceOf(receiver)).eq(initial_receiver_balance.add(withdraw_amount))).to.be.true; - // Due to block timestamp could be paying outstanding interest to lender or not - expect((await SUSD.balanceOf(sovryn.address)).lte(initial_loan_token_lender_balance.sub(withdraw_amount))).to.be.true; - }); - - it("EarnRewardFail events should be fired if lockedSOV reverted when Test reduce loan_duration with collateral sov token reward payment (special rebates not set or 0", async () => { - // prepare the test - await sovryn.setLockedSOVAddress((await LockedSOVFailedMockup.new(SOV.address, [owner])).address); - const duration_in_seconds = 20 * 24 * 60 * 60; // 20 days - const [loan_id, borrower] = await borrow_indefinite_loan( - loanToken, - sovryn, - SUSD, - RBTC, - accounts, - new BN(10).mul(oneEth).toString(), - new BN(50).mul(oneEth).toString(), - duration_in_seconds - ); - - const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); - - const days_to_reduce = new BN(5); - const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; - const withdraw_amount = owed_per_day.mul(days_to_reduce); - - const receiver = accounts[3]; - - await increaseTime(10 * 24 * 60 * 60); - lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); - const borrower_initial_balance_before_reduce = (await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower)); - - const loanMaintenance = await LoanMaintenance.at(sovryn.address); - const { receipt } = await loanMaintenance.reduceLoanDuration(loan_id, receiver, withdraw_amount, { from: borrower }); - - const borrower_initial_balance = borrower_initial_balance_before_reduce.sub( - (await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower)) - ); - - expect(borrower_initial_balance_before_reduce.toString()).to.eq(borrower_initial_balance.toString()); - - const feeRebatePercent = await sovryn.feeRebatePercent(); - const decode = decodeLogs(receipt.rawLogs, FeesEvents, "EarnRewardFail"); - const args = decode[0].args; - expect(args["receiver"] == borrower).to.be.true; - expect(args["token"] == SOV.address).to.be.true; - expect(args["loanId"] == loan_id).to.be.true; - expect(args["feeRebatePercent"] == feeRebatePercent).to.be.true; - expect(args["basisPoint"] == 0).to.be.true; - }); - - it("EarnRewardFail events should be fired if lockedSOV reverted when Test reduce loan_duration with collateral sov token reward payment (special rebates is set)", async () => { - // prepare the test - await sovryn.setLockedSOVAddress((await LockedSOVFailedMockup.new(SOV.address, [owner])).address); - await sovryn.setSpecialRebates(SUSD.address, RBTC.address, wei("200", "ether")); - const duration_in_seconds = 20 * 24 * 60 * 60; // 20 days - const [loan_id, borrower] = await borrow_indefinite_loan( - loanToken, - sovryn, - SUSD, - RBTC, - accounts, - new BN(10).mul(oneEth).toString(), - new BN(50).mul(oneEth).toString(), - duration_in_seconds - ); - - const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); - - const days_to_reduce = new BN(5); - const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; - const withdraw_amount = owed_per_day.mul(days_to_reduce); - - const receiver = accounts[3]; - - await increaseTime(10 * 24 * 60 * 60); - lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); - const borrower_initial_balance_before_reduce = (await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower)); - - const loanMaintenance = await LoanMaintenance.at(sovryn.address); - const { receipt } = await loanMaintenance.reduceLoanDuration(loan_id, receiver, withdraw_amount, { from: borrower }); - - const borrower_initial_balance = borrower_initial_balance_before_reduce.sub( - (await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower)) - ); - - expect(borrower_initial_balance_before_reduce.toString()).to.eq(borrower_initial_balance.toString()); - - const feeRebatePercent = await sovryn.specialRebates(SUSD.address, RBTC.address); - const decode = decodeLogs(receipt.rawLogs, FeesEvents, "EarnRewardFail"); - const args = decode[0].args; - expect(args["receiver"] == borrower).to.be.true; - expect(args["token"] == SOV.address).to.be.true; - expect(args["loanId"] == loan_id).to.be.true; - expect(args["feeRebatePercent"] == feeRebatePercent).to.be.true; - expect(args["basisPoint"] == 0).to.be.true; - }); - - it("EarnRewardFailed should be fired if protocol does not have enough dedicated SOV to pay the rebate rewards", async () => { - const basisPoint = 9000; - // prepare the test - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, owner); - await sovryn.setSpecialRebates(SUSD.address, RBTC.address, wei("200", "ether")); - await sovryn.setTradingRebateRewardsBasisPoint(basisPoint); - await sovryn.withdrawProtocolToken(accounts[0], new BN(10).pow(new BN(20))); - const [loan_id, borrower] = await borrow_indefinite_loan( - loanToken, - sovryn, - SUSD, - RBTC, - accounts, - (withdraw_amount = new BN(10).mul(oneEth).toString()), - (margin = new BN(50).mul(oneEth).toString()), - (duration_in_seconds = 60 * 60 * 24 * 20) - ); - - const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); - - const days_to_extend = new BN(10); - const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; - const deposit_amount = owed_per_day.mul(days_to_extend); - - await increaseTime(10 * 24 * 60 * 60); - lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); - const borrower_initial_balance_before_extend = (await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower)); - const borrower_initial_unlock_balance_before_extend = (await SOV.balanceOf(borrower)).add( - await lockedSOV.getUnlockedBalance(borrower) - ); - - const loanMaintenance = await LoanMaintenance.at(sovryn.address); - const { receipt } = await loanMaintenance.extendLoanDuration(loan_id, deposit_amount, true, "0x", { from: borrower }); - - const feeRebatePercent = await sovryn.specialRebates(SUSD.address, RBTC.address); - const decode = decodeLogs(receipt.rawLogs, FeesEvents, "EarnRewardFail"); - const args = decode[0].args; - expect(args["receiver"] == borrower).to.be.true; - expect(args["token"] == SOV.address).to.be.true; - expect(args["loanId"] == loan_id).to.be.true; - expect(args["feeRebatePercent"] == feeRebatePercent).to.be.true; - expect(args["basisPoint"] == basisPoint).to.be.true; - - // vested SOV rewards - expect((await lockedSOV.getLockedBalance(borrower)).toString()).to.eq(new BN(0).toString()); - - expect((await lockedSOV.getUnlockedBalance(borrower)).toString()).to.eq(new BN(0).toString()); - }); - - it("Test reduce loan_duration 0 withdraw should fail", async () => { - // prepare the test - const [loan_id, borrower] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - const receiver = accounts[3]; - - const loanMaintenance = await LoanMaintenance.at(sovryn.address); - await expectRevert(loanMaintenance.reduceLoanDuration(loan_id, receiver, 0, { from: borrower }), "withdrawAmount is 0"); - }); - - it("Test reduce loan_duration with collateral sov token reward payment (special rebates not set or 0)", async () => { - // prepare the test - const duration_in_seconds = 20 * 24 * 60 * 60; // 20 days - const [loan_id, borrower] = await borrow_indefinite_loan( - loanToken, - sovryn, - SUSD, - RBTC, - accounts, - new BN(10).mul(oneEth).toString(), - new BN(50).mul(oneEth).toString(), - duration_in_seconds - ); - - const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); - - const days_to_reduce = new BN(5); - const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; - const withdraw_amount = owed_per_day.mul(days_to_reduce); - - const receiver = accounts[3]; - - await increaseTime(10 * 24 * 60 * 60); - lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); - const borrower_initial_balance_before_reduce = (await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower)); - - const loanMaintenance = await LoanMaintenance.at(sovryn.address); - const { receipt } = await loanMaintenance.reduceLoanDuration(loan_id, receiver, withdraw_amount, { from: borrower }); - - const borrower_initial_balance = borrower_initial_balance_before_reduce.sub( - (await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower)) - ); - - const feeRebatePercent = await sovryn.feeRebatePercent(); - const decode = decodeLogs(receipt.rawLogs, FeesEvents, "EarnReward"); - const args = decode[0].args; - expect(args["receiver"] == borrower).to.be.true; - expect(args["token"] == SOV.address).to.be.true; - expect(args["loanId"] == loan_id).to.be.true; - expect(args["amount"]).to.eq((await SOV.balanceOf(borrower)).sub(borrower_initial_balance).toString()); - expect(args["feeRebatePercent"] == feeRebatePercent).to.be.true; - expect(args["basisPoint"] == 0).to.be.true; - }); - - it("Test reduce loan_duration with collateral sov token reward payment (special rebates is set)", async () => { - // prepare the test - await sovryn.setSpecialRebates(SUSD.address, RBTC.address, wei("200", "ether")); - const duration_in_seconds = 20 * 24 * 60 * 60; // 20 days - const [loan_id, borrower] = await borrow_indefinite_loan( - loanToken, - sovryn, - SUSD, - RBTC, - accounts, - new BN(10).mul(oneEth).toString(), - new BN(50).mul(oneEth).toString(), - duration_in_seconds - ); - - const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); - - const days_to_reduce = new BN(5); - const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; - const withdraw_amount = owed_per_day.mul(days_to_reduce); - - const receiver = accounts[3]; - - await increaseTime(10 * 24 * 60 * 60); - lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); - const borrower_initial_balance_before_reduce = (await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower)); - - const loanMaintenance = await LoanMaintenance.at(sovryn.address); - const { receipt } = await loanMaintenance.reduceLoanDuration(loan_id, receiver, withdraw_amount, { from: borrower }); - - const borrower_initial_balance = borrower_initial_balance_before_reduce.sub( - (await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower)) - ); - - const feeRebatePercent = await sovryn.specialRebates(SUSD.address, RBTC.address); - const decode = decodeLogs(receipt.rawLogs, FeesEvents, "EarnReward"); - const args = decode[0].args; - expect(args["receiver"] == borrower).to.be.true; - expect(args["token"] == SOV.address).to.be.true; - expect(args["loanId"] == loan_id).to.be.true; - expect(args["amount"]).to.eq((await SOV.balanceOf(borrower)).sub(borrower_initial_balance).toString()); - expect(args["feeRebatePercent"] == feeRebatePercent).to.be.true; - expect(args["basisPoint"] == 0).to.be.true; - }); - - it("Test reduce loan_duration 0 withdraw should fail", async () => { - // prepare the test - const [loan_id, borrower] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - const receiver = accounts[3]; - - const loanMaintenance = await LoanMaintenance.at(sovryn.address); - await expectRevert(loanMaintenance.reduceLoanDuration(loan_id, receiver, 0, { from: borrower }), "withdrawAmount is 0"); - }); - - it("Test reduce closed loan_duration should fail", async () => { - // prepare the test - const [loan_id, borrower, , , , , args] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - const collateral = args["newCollateral"]; - const receiver = accounts[3]; - - const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); - await sovryn.closeWithSwap(loan_id, borrower, collateral, false, "0x", { from: borrower }); - - const days_to_reduce = new BN(5); - const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; - const withdraw_amount = owed_per_day.mul(days_to_reduce); - - const loanMaintenance = await LoanMaintenance.at(sovryn.address); - await expectRevert( - loanMaintenance.reduceLoanDuration(loan_id, receiver, withdraw_amount, { from: borrower }), - "loan is closed" - ); - }); - - it("Test reduce loan_duration user unauthorized should fail", async () => { - // prepare the test - const [loan_id] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - - const receiver = accounts[3]; - const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); - - const days_to_reduce = new BN(5); - const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; - const withdraw_amount = owed_per_day.mul(days_to_reduce); - - const loanMaintenance = await LoanMaintenance.at(sovryn.address); - await expectRevert(loanMaintenance.reduceLoanDuration(loan_id, receiver, withdraw_amount, { from: receiver }), "unauthorized"); - }); - - it("Test reduce loan_duration loan term ended should fail", async () => { - // prepare the test - const [loan_id, borrower] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - - const receiver = accounts[3]; - const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); - - const days_to_reduce = new BN(5); - const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; - const withdraw_amount = owed_per_day.mul(days_to_reduce); - - const initial_loan = await sovryn.getLoan(loan_id); - const loan_end_timestamp = parseInt(initial_loan["endTimestamp"]); - - await increaseTime(loan_end_timestamp); - - const loanMaintenance = await LoanMaintenance.at(sovryn.address); - await expectRevert( - loanMaintenance.reduceLoanDuration(loan_id, receiver, withdraw_amount, { from: borrower }), - "loan term has ended" - ); - }); - - it("Test reduce loan_duration withdraw amount too high should fail", async () => { - // prepare the test - const [loan_id, borrower, , withdraw_amount] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - - const receiver = accounts[3]; - - const loanMaintenance = await LoanMaintenance.at(sovryn.address); - await expectRevert( - loanMaintenance.reduceLoanDuration(loan_id, receiver, new BN(withdraw_amount).mul(new BN(2)), { from: borrower }), - "withdraw amount too high" - ); - }); - - it("Test reduce loan_duration less than one hour should fail", async () => { - // prepare the test - const [loan_id, borrower, , , duration_in_seconds] = await borrow_indefinite_loan(loanToken, sovryn, SUSD, RBTC, accounts); - - const receiver = accounts[3]; - const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); - - const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; - // reduce the loan upto 50 minutes - const withdraw_amount = owed_per_day.mul(new BN(duration_in_seconds - 50 * 60)).div(new BN(24 * 60 * 60)); - - const loanMaintenance = await LoanMaintenance.at(sovryn.address); - await expectRevert( - loanMaintenance.reduceLoanDuration(loan_id, receiver, withdraw_amount, { from: borrower }), - "loan too short" - ); - }); - }); + it("Test reduce loan duration", async () => { + // prepare the test + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + const initial_loan = await sovryn.getLoan(loan_id); + const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); + const initial_loan_token_lender_balance = await SUSD.balanceOf(sovryn.address); + + const days_to_reduce = new BN(5); + const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; + const withdraw_amount = owed_per_day.mul(days_to_reduce); + + const receiver = accounts[3]; + const initial_receiver_balance = await SUSD.balanceOf(receiver); + + const loanMaintenance = await LoanMaintenance.at(sovryn.address); + await loanMaintenance.reduceLoanDuration(loan_id, receiver, withdraw_amount, { + from: borrower, + }); + + const end_loan_interest_data = await sovryn.getLoanInterestData(loan_id); + const end_loan = await sovryn.getLoan(loan_id); + + expect( + end_loan["endTimestamp"] == + parseInt(initial_loan["endTimestamp"]) - + days_to_reduce.toNumber() * 24 * 60 * 60 + ).to.be.true; + expect( + end_loan_interest_data["interestDepositTotal"].eq( + initial_loan_interest_data["interestDepositTotal"].sub(withdraw_amount) + ) + ).to.be.true; + expect( + (await SUSD.balanceOf(receiver)).eq(initial_receiver_balance.add(withdraw_amount)) + ).to.be.true; + // Due to block timestamp could be paying outstanding interest to lender or not + expect( + (await SUSD.balanceOf(sovryn.address)).lte( + initial_loan_token_lender_balance.sub(withdraw_amount) + ) + ).to.be.true; + }); + + it("EarnRewardFail events should be fired if lockedSOV reverted when Test reduce loan_duration with collateral sov token reward payment (special rebates not set or 0", async () => { + // prepare the test + await sovryn.setLockedSOVAddress( + ( + await LockedSOVFailedMockup.new(SOV.address, [owner]) + ).address + ); + const duration_in_seconds = 20 * 24 * 60 * 60; // 20 days + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts, + new BN(10).mul(oneEth).toString(), + new BN(50).mul(oneEth).toString(), + duration_in_seconds + ); + + const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); + + const days_to_reduce = new BN(5); + const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; + const withdraw_amount = owed_per_day.mul(days_to_reduce); + + const receiver = accounts[3]; + + await increaseTime(10 * 24 * 60 * 60); + lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); + const borrower_initial_balance_before_reduce = (await SOV.balanceOf(borrower)).add( + await lockedSOV.getLockedBalance(borrower) + ); + + const loanMaintenance = await LoanMaintenance.at(sovryn.address); + const { receipt } = await loanMaintenance.reduceLoanDuration( + loan_id, + receiver, + withdraw_amount, + { from: borrower } + ); + + const borrower_initial_balance = borrower_initial_balance_before_reduce.sub( + (await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower)) + ); + + expect(borrower_initial_balance_before_reduce.toString()).to.eq( + borrower_initial_balance.toString() + ); + + const feeRebatePercent = await sovryn.feeRebatePercent(); + const decode = decodeLogs(receipt.rawLogs, FeesEvents, "EarnRewardFail"); + const args = decode[0].args; + expect(args["receiver"] == borrower).to.be.true; + expect(args["token"] == SOV.address).to.be.true; + expect(args["loanId"] == loan_id).to.be.true; + expect(args["feeRebatePercent"] == feeRebatePercent).to.be.true; + expect(args["basisPoint"] == 0).to.be.true; + }); + + it("EarnRewardFail events should be fired if lockedSOV reverted when Test reduce loan_duration with collateral sov token reward payment (special rebates is set)", async () => { + // prepare the test + await sovryn.setLockedSOVAddress( + ( + await LockedSOVFailedMockup.new(SOV.address, [owner]) + ).address + ); + await sovryn.setSpecialRebates(SUSD.address, RBTC.address, wei("200", "ether")); + const duration_in_seconds = 20 * 24 * 60 * 60; // 20 days + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts, + new BN(10).mul(oneEth).toString(), + new BN(50).mul(oneEth).toString(), + duration_in_seconds + ); + + const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); + + const days_to_reduce = new BN(5); + const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; + const withdraw_amount = owed_per_day.mul(days_to_reduce); + + const receiver = accounts[3]; + + await increaseTime(10 * 24 * 60 * 60); + lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); + const borrower_initial_balance_before_reduce = (await SOV.balanceOf(borrower)).add( + await lockedSOV.getLockedBalance(borrower) + ); + + const loanMaintenance = await LoanMaintenance.at(sovryn.address); + const { receipt } = await loanMaintenance.reduceLoanDuration( + loan_id, + receiver, + withdraw_amount, + { from: borrower } + ); + + const borrower_initial_balance = borrower_initial_balance_before_reduce.sub( + (await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower)) + ); + + expect(borrower_initial_balance_before_reduce.toString()).to.eq( + borrower_initial_balance.toString() + ); + + const feeRebatePercent = await sovryn.specialRebates(SUSD.address, RBTC.address); + const decode = decodeLogs(receipt.rawLogs, FeesEvents, "EarnRewardFail"); + const args = decode[0].args; + expect(args["receiver"] == borrower).to.be.true; + expect(args["token"] == SOV.address).to.be.true; + expect(args["loanId"] == loan_id).to.be.true; + expect(args["feeRebatePercent"] == feeRebatePercent).to.be.true; + expect(args["basisPoint"] == 0).to.be.true; + }); + + it("EarnRewardFailed should be fired if protocol does not have enough dedicated SOV to pay the rebate rewards", async () => { + const basisPoint = 9000; + // prepare the test + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, owner); + await sovryn.setSpecialRebates(SUSD.address, RBTC.address, wei("200", "ether")); + await sovryn.setTradingRebateRewardsBasisPoint(basisPoint); + await sovryn.withdrawProtocolToken(accounts[0], new BN(10).pow(new BN(20))); + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts, + (withdraw_amount = new BN(10).mul(oneEth).toString()), + (margin = new BN(50).mul(oneEth).toString()), + (duration_in_seconds = 60 * 60 * 24 * 20) + ); + + const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); + + const days_to_extend = new BN(10); + const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; + const deposit_amount = owed_per_day.mul(days_to_extend); + + await increaseTime(10 * 24 * 60 * 60); + lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); + const borrower_initial_balance_before_extend = (await SOV.balanceOf(borrower)).add( + await lockedSOV.getLockedBalance(borrower) + ); + const borrower_initial_unlock_balance_before_extend = ( + await SOV.balanceOf(borrower) + ).add(await lockedSOV.getUnlockedBalance(borrower)); + + const loanMaintenance = await LoanMaintenance.at(sovryn.address); + const { receipt } = await loanMaintenance.extendLoanDuration( + loan_id, + deposit_amount, + true, + "0x", + { from: borrower } + ); + + const feeRebatePercent = await sovryn.specialRebates(SUSD.address, RBTC.address); + const decode = decodeLogs(receipt.rawLogs, FeesEvents, "EarnRewardFail"); + const args = decode[0].args; + expect(args["receiver"] == borrower).to.be.true; + expect(args["token"] == SOV.address).to.be.true; + expect(args["loanId"] == loan_id).to.be.true; + expect(args["feeRebatePercent"] == feeRebatePercent).to.be.true; + expect(args["basisPoint"] == basisPoint).to.be.true; + + // vested SOV rewards + expect((await lockedSOV.getLockedBalance(borrower)).toString()).to.eq( + new BN(0).toString() + ); + + expect((await lockedSOV.getUnlockedBalance(borrower)).toString()).to.eq( + new BN(0).toString() + ); + }); + + it("Test reduce loan_duration 0 withdraw should fail", async () => { + // prepare the test + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + const receiver = accounts[3]; + + const loanMaintenance = await LoanMaintenance.at(sovryn.address); + await expectRevert( + loanMaintenance.reduceLoanDuration(loan_id, receiver, 0, { from: borrower }), + "withdrawAmount is 0" + ); + }); + + it("Test reduce loan_duration with collateral sov token reward payment (special rebates not set or 0)", async () => { + // prepare the test + const duration_in_seconds = 20 * 24 * 60 * 60; // 20 days + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts, + new BN(10).mul(oneEth).toString(), + new BN(50).mul(oneEth).toString(), + duration_in_seconds + ); + + const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); + + const days_to_reduce = new BN(5); + const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; + const withdraw_amount = owed_per_day.mul(days_to_reduce); + + const receiver = accounts[3]; + + await increaseTime(10 * 24 * 60 * 60); + lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); + const borrower_initial_balance_before_reduce = (await SOV.balanceOf(borrower)).add( + await lockedSOV.getLockedBalance(borrower) + ); + + const loanMaintenance = await LoanMaintenance.at(sovryn.address); + const { receipt } = await loanMaintenance.reduceLoanDuration( + loan_id, + receiver, + withdraw_amount, + { from: borrower } + ); + + const borrower_initial_balance = borrower_initial_balance_before_reduce.sub( + (await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower)) + ); + + const feeRebatePercent = await sovryn.feeRebatePercent(); + const decode = decodeLogs(receipt.rawLogs, FeesEvents, "EarnReward"); + const args = decode[0].args; + expect(args["receiver"] == borrower).to.be.true; + expect(args["token"] == SOV.address).to.be.true; + expect(args["loanId"] == loan_id).to.be.true; + expect(args["amount"]).to.eq( + (await SOV.balanceOf(borrower)).sub(borrower_initial_balance).toString() + ); + expect(args["feeRebatePercent"] == feeRebatePercent).to.be.true; + expect(args["basisPoint"] == 0).to.be.true; + }); + + it("Test reduce loan_duration with collateral sov token reward payment (special rebates is set)", async () => { + // prepare the test + await sovryn.setSpecialRebates(SUSD.address, RBTC.address, wei("200", "ether")); + const duration_in_seconds = 20 * 24 * 60 * 60; // 20 days + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts, + new BN(10).mul(oneEth).toString(), + new BN(50).mul(oneEth).toString(), + duration_in_seconds + ); + + const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); + + const days_to_reduce = new BN(5); + const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; + const withdraw_amount = owed_per_day.mul(days_to_reduce); + + const receiver = accounts[3]; + + await increaseTime(10 * 24 * 60 * 60); + lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); + const borrower_initial_balance_before_reduce = (await SOV.balanceOf(borrower)).add( + await lockedSOV.getLockedBalance(borrower) + ); + + const loanMaintenance = await LoanMaintenance.at(sovryn.address); + const { receipt } = await loanMaintenance.reduceLoanDuration( + loan_id, + receiver, + withdraw_amount, + { from: borrower } + ); + + const borrower_initial_balance = borrower_initial_balance_before_reduce.sub( + (await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower)) + ); + + const feeRebatePercent = await sovryn.specialRebates(SUSD.address, RBTC.address); + const decode = decodeLogs(receipt.rawLogs, FeesEvents, "EarnReward"); + const args = decode[0].args; + expect(args["receiver"] == borrower).to.be.true; + expect(args["token"] == SOV.address).to.be.true; + expect(args["loanId"] == loan_id).to.be.true; + expect(args["amount"]).to.eq( + (await SOV.balanceOf(borrower)).sub(borrower_initial_balance).toString() + ); + expect(args["feeRebatePercent"] == feeRebatePercent).to.be.true; + expect(args["basisPoint"] == 0).to.be.true; + }); + + it("Test reduce loan_duration 0 withdraw should fail", async () => { + // prepare the test + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + const receiver = accounts[3]; + + const loanMaintenance = await LoanMaintenance.at(sovryn.address); + await expectRevert( + loanMaintenance.reduceLoanDuration(loan_id, receiver, 0, { from: borrower }), + "withdrawAmount is 0" + ); + }); + + it("Test reduce closed loan_duration should fail", async () => { + // prepare the test + const [loan_id, borrower, , , , , args] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + const collateral = args["newCollateral"]; + const receiver = accounts[3]; + + const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); + await sovryn.closeWithSwap(loan_id, borrower, collateral, false, "0x", { + from: borrower, + }); + + const days_to_reduce = new BN(5); + const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; + const withdraw_amount = owed_per_day.mul(days_to_reduce); + + const loanMaintenance = await LoanMaintenance.at(sovryn.address); + await expectRevert( + loanMaintenance.reduceLoanDuration(loan_id, receiver, withdraw_amount, { + from: borrower, + }), + "loan is closed" + ); + }); + + it("Test reduce loan_duration user unauthorized should fail", async () => { + // prepare the test + const [loan_id] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + + const receiver = accounts[3]; + const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); + + const days_to_reduce = new BN(5); + const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; + const withdraw_amount = owed_per_day.mul(days_to_reduce); + + const loanMaintenance = await LoanMaintenance.at(sovryn.address); + await expectRevert( + loanMaintenance.reduceLoanDuration(loan_id, receiver, withdraw_amount, { + from: receiver, + }), + "unauthorized" + ); + }); + + it("Test reduce loan_duration loan term ended should fail", async () => { + // prepare the test + const [loan_id, borrower] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + + const receiver = accounts[3]; + const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); + + const days_to_reduce = new BN(5); + const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; + const withdraw_amount = owed_per_day.mul(days_to_reduce); + + const initial_loan = await sovryn.getLoan(loan_id); + const loan_end_timestamp = parseInt(initial_loan["endTimestamp"]); + + await increaseTime(loan_end_timestamp); + + const loanMaintenance = await LoanMaintenance.at(sovryn.address); + await expectRevert( + loanMaintenance.reduceLoanDuration(loan_id, receiver, withdraw_amount, { + from: borrower, + }), + "loan term has ended" + ); + }); + + it("Test reduce loan_duration withdraw amount too high should fail", async () => { + // prepare the test + const [loan_id, borrower, , withdraw_amount] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + + const receiver = accounts[3]; + + const loanMaintenance = await LoanMaintenance.at(sovryn.address); + await expectRevert( + loanMaintenance.reduceLoanDuration( + loan_id, + receiver, + new BN(withdraw_amount).mul(new BN(2)), + { from: borrower } + ), + "withdraw amount too high" + ); + }); + + it("Test reduce loan_duration less than one hour should fail", async () => { + // prepare the test + const [loan_id, borrower, , , duration_in_seconds] = await borrow_indefinite_loan( + loanToken, + sovryn, + SUSD, + RBTC, + accounts + ); + + const receiver = accounts[3]; + const initial_loan_interest_data = await sovryn.getLoanInterestData(loan_id); + + const owed_per_day = initial_loan_interest_data["interestOwedPerDay"]; + // reduce the loan upto 50 minutes + const withdraw_amount = owed_per_day + .mul(new BN(duration_in_seconds - 50 * 60)) + .div(new BN(24 * 60 * 60)); + + const loanMaintenance = await LoanMaintenance.at(sovryn.address); + await expectRevert( + loanMaintenance.reduceLoanDuration(loan_id, receiver, withdraw_amount, { + from: borrower, + }), + "loan too short" + ); + }); + }); }); diff --git a/tests/protocol/CloseDepositTestToken.test.js b/tests/protocol/CloseDepositTestToken.test.js index 0db89e16f..847c81d3a 100644 --- a/tests/protocol/CloseDepositTestToken.test.js +++ b/tests/protocol/CloseDepositTestToken.test.js @@ -21,21 +21,21 @@ const IERC20 = artifacts.require("IERC20"); const LockedSOVMockup = artifacts.require("LockedSOVMockup"); const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getLoanToken, - getLoanTokenWRBTC, - loan_pool_setup, - set_demand_curve, - lend_to_pool, - getPriceFeeds, - getSovryn, - decodeLogs, - open_margin_trade_position, - getSOV, - verify_sov_reward_payment, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getLoanToken, + getLoanTokenWRBTC, + loan_pool_setup, + set_demand_curve, + lend_to_pool, + getPriceFeeds, + getSovryn, + decodeLogs, + open_margin_trade_position, + getSOV, + verify_sov_reward_payment, } = require("../Utils/initializer.js"); const wei = web3.utils.toWei; @@ -54,386 +54,465 @@ Note: close with swap is tested in loanToken/trading */ contract("ProtocolCloseDeposit", (accounts) => { - let owner; - let sovryn, SUSD, WRBTC, RBTC, BZRX, loanToken, loanTokenWRBTC, priceFeeds, SOV; - let borrower, receiver; - - async function deploymentAndInitFixture(_wallets, _provider) { - // Deploying sovrynProtocol w/ generic function from initializer.js - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - - loanToken = await getLoanToken(owner, sovryn, WRBTC, SUSD); - loanTokenWRBTC = await getLoanTokenWRBTC(owner, sovryn, WRBTC, SUSD); - await loan_pool_setup(sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC); - - /// @dev SOV test token deployment w/ initializer.js - SOV = await getSOV(sovryn, priceFeeds, SUSD, accounts); - - /// @dev Moved from some tests that require this initialization - /// and is not interfering w/ any others. - borrower = accounts[3]; - receiver = accounts[4]; - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, accounts[2]); - } - - before(async () => { - [owner] = accounts; - }); - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - const internal_test_close_with_deposit = async ( - deposit_amount, - RBTC, - SUSD, - borrower, - collateral, - initial_loan, - initial_loan_interest, - loanToken, - loan_id, - priceFeeds, - principal, - receiver, - sovryn, - LoanClosingsEvents, - FeesEvents, - SOV - ) => { - await SUSD.mint(borrower, deposit_amount); - await SUSD.approve(sovryn.address, deposit_amount, { from: borrower }); - const { rate, precision } = await priceFeeds.queryRate(initial_loan["collateralToken"], initial_loan["loanToken"]); - - lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); - const sov_borrower_initial_balance = (await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower)); - - const tx = await sovryn.closeWithDeposit(loan_id, receiver, deposit_amount, { from: borrower }); - const receipt = tx.receipt; - - let loan_close_amount = deposit_amount.gt(principal) ? principal : deposit_amount; - - // Check that tiny position won't be created - // Comparison must be in wrbtc format because TINY_AMOUNT is assumed as WRBTC - const remainingAmount = principal.sub(loan_close_amount); - if (remainingAmount.gt(new BN(0))) { - const { rate, precision } = await priceFeeds.queryRate(initial_loan["loanToken"], WRBTC.address); - remainingAmountInWRBTC = remainingAmount.mul(rate).div(precision); - - if (remainingAmountInWRBTC.cmp(TINY_AMOUNT) <= 0) { - loan_close_amount = principal; - } - } - - const withdraw_amount = loan_close_amount.eq(principal) ? collateral : collateral.mul(loan_close_amount).div(principal); - const end_collateral = collateral.sub(withdraw_amount); - const end_principal = loan_close_amount.eq(principal) ? new BN(0) : principal.sub(loan_close_amount); - const collateral_to_loan_rate = new BN(rate).mul(oneEth).div(new BN(precision)); - const collateral_to_loan_amount = end_collateral.mul(collateral_to_loan_rate).div(oneEth); - const current_margin = - end_principal.lte(collateral_to_loan_amount) && !end_principal.eq(new BN(0)) - ? collateral_to_loan_amount.sub(end_principal).mul(hunEth).div(end_principal) - : new BN(0); - - const owed_per_day = new BN(initial_loan_interest["interestOwedPerDay"]); - const end_timestamp = initial_loan["endTimestamp"]; - const owed_per_day_refund = owed_per_day.mul(loan_close_amount).div(principal); - // (loan end timestamp - block timestamp) * owedPerDayRefund / 24*60*60 - const num = await blockNumber(); - let lastBlock = await web3.eth.getBlock(num); - const block_timestamp = lastBlock.timestamp; - const interest_refund_to_borrower_1 = new BN(end_timestamp - block_timestamp).mul(owed_per_day_refund).div(new BN(24 * 60 * 60)); - const interest_refund_to_borrower = interest_refund_to_borrower_1.lte(loan_close_amount) - ? new BN(0) - : interest_refund_to_borrower_1.sub(loan_close_amount); - - // Test CloseWithDeposit event parameters - // When all the tests are run, the event is not recognized so we have to decode it manually - const decode = decodeLogs(receipt.rawLogs, LoanClosingsEvents, "CloseWithDeposit"); - const close_event = decode[0].args; - - expect(close_event["user"] == borrower).to.be.true; - expect(close_event["lender"] == loanToken.address).to.be.true; - expect(close_event["loanId"] == loan_id).to.be.true; - expect(close_event["closer"] == borrower).to.be.true; - expect(close_event["loanToken"] == initial_loan["loanToken"]).to.be.true; - expect(close_event["collateralToken"] == initial_loan["collateralToken"]).to.be.true; - expect(close_event["repayAmount"] == loan_close_amount.toString()).to.be.true; - expect(close_event["collateralWithdrawAmount"] == withdraw_amount.toString()).to.be.true; - expect(close_event["collateralToLoanRate"] == collateral_to_loan_rate.toString()).to.be.true; - expect(close_event["currentMargin"] == current_margin.toString()).to.be.true; - - // Test refund collateral to receiver - // Test refund interest to receiver - expect((await RBTC.balanceOf(receiver)).eq(withdraw_amount)).to.be.true; - expect((await SUSD.balanceOf(receiver)).eq(interest_refund_to_borrower)).to.be.true; - - // Test loan update - const end_loan = await sovryn.getLoan(loan_id); - const new_principal = loan_close_amount.eq(principal) ? new BN(0) : principal.sub(loan_close_amount); - expect(end_loan["principal"] == new_principal.toString()).to.be.true; - if (loan_close_amount.eq(principal)) { - const last_block_timestamp = block_timestamp; - expect(end_loan["endTimestamp"] <= last_block_timestamp); - } - - // Test returning principal to lender with deposit - const loan_close_amount_less_interest = loan_close_amount.gte(interest_refund_to_borrower_1) - ? loan_close_amount.sub(interest_refund_to_borrower_1) - : new BN(0); - - const decode2 = decodeLogs(receipt.rawLogs, IERC20, "Transfer"); - let transfer_to_lender = decode2.filter((tx_event) => tx_event.args["from"] == borrower); - expect(transfer_to_lender.length == 1).to.be.true; - transfer_to_lender = transfer_to_lender[0].args; - expect(transfer_to_lender["to"] == loanToken.address).to.be.true; - expect(transfer_to_lender["value"]).eq(loan_close_amount_less_interest.toString()); - - await verify_sov_reward_payment( - receipt.rawLogs, - FeesEvents, - SOV, - borrower, - loan_id, - sov_borrower_initial_balance, - 1, - SUSD.address, - RBTC.address, - sovryn - ); - }; - - describe("Tests the close with deposit. ", () => { - /* + let owner; + let sovryn, SUSD, WRBTC, RBTC, BZRX, loanToken, loanTokenWRBTC, priceFeeds, SOV; + let borrower, receiver; + + async function deploymentAndInitFixture(_wallets, _provider) { + // Deploying sovrynProtocol w/ generic function from initializer.js + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + + loanToken = await getLoanToken(owner, sovryn, WRBTC, SUSD); + loanTokenWRBTC = await getLoanTokenWRBTC(owner, sovryn, WRBTC, SUSD); + await loan_pool_setup(sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC); + + /// @dev SOV test token deployment w/ initializer.js + SOV = await getSOV(sovryn, priceFeeds, SUSD, accounts); + + /// @dev Moved from some tests that require this initialization + /// and is not interfering w/ any others. + borrower = accounts[3]; + receiver = accounts[4]; + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, accounts[2]); + } + + before(async () => { + [owner] = accounts; + }); + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + const internal_test_close_with_deposit = async ( + deposit_amount, + RBTC, + SUSD, + borrower, + collateral, + initial_loan, + initial_loan_interest, + loanToken, + loan_id, + priceFeeds, + principal, + receiver, + sovryn, + LoanClosingsEvents, + FeesEvents, + SOV + ) => { + await SUSD.mint(borrower, deposit_amount); + await SUSD.approve(sovryn.address, deposit_amount, { from: borrower }); + const { rate, precision } = await priceFeeds.queryRate( + initial_loan["collateralToken"], + initial_loan["loanToken"] + ); + + lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); + const sov_borrower_initial_balance = (await SOV.balanceOf(borrower)).add( + await lockedSOV.getLockedBalance(borrower) + ); + + const tx = await sovryn.closeWithDeposit(loan_id, receiver, deposit_amount, { + from: borrower, + }); + const receipt = tx.receipt; + + let loan_close_amount = deposit_amount.gt(principal) ? principal : deposit_amount; + + // Check that tiny position won't be created + // Comparison must be in wrbtc format because TINY_AMOUNT is assumed as WRBTC + const remainingAmount = principal.sub(loan_close_amount); + if (remainingAmount.gt(new BN(0))) { + const { rate, precision } = await priceFeeds.queryRate( + initial_loan["loanToken"], + WRBTC.address + ); + remainingAmountInWRBTC = remainingAmount.mul(rate).div(precision); + + if (remainingAmountInWRBTC.cmp(TINY_AMOUNT) <= 0) { + loan_close_amount = principal; + } + } + + const withdraw_amount = loan_close_amount.eq(principal) + ? collateral + : collateral.mul(loan_close_amount).div(principal); + const end_collateral = collateral.sub(withdraw_amount); + const end_principal = loan_close_amount.eq(principal) + ? new BN(0) + : principal.sub(loan_close_amount); + const collateral_to_loan_rate = new BN(rate).mul(oneEth).div(new BN(precision)); + const collateral_to_loan_amount = end_collateral.mul(collateral_to_loan_rate).div(oneEth); + const current_margin = + end_principal.lte(collateral_to_loan_amount) && !end_principal.eq(new BN(0)) + ? collateral_to_loan_amount.sub(end_principal).mul(hunEth).div(end_principal) + : new BN(0); + + const owed_per_day = new BN(initial_loan_interest["interestOwedPerDay"]); + const end_timestamp = initial_loan["endTimestamp"]; + const owed_per_day_refund = owed_per_day.mul(loan_close_amount).div(principal); + // (loan end timestamp - block timestamp) * owedPerDayRefund / 24*60*60 + const num = await blockNumber(); + let lastBlock = await web3.eth.getBlock(num); + const block_timestamp = lastBlock.timestamp; + const interest_refund_to_borrower_1 = new BN(end_timestamp - block_timestamp) + .mul(owed_per_day_refund) + .div(new BN(24 * 60 * 60)); + const interest_refund_to_borrower = interest_refund_to_borrower_1.lte(loan_close_amount) + ? new BN(0) + : interest_refund_to_borrower_1.sub(loan_close_amount); + + // Test CloseWithDeposit event parameters + // When all the tests are run, the event is not recognized so we have to decode it manually + const decode = decodeLogs(receipt.rawLogs, LoanClosingsEvents, "CloseWithDeposit"); + const close_event = decode[0].args; + + expect(close_event["user"] == borrower).to.be.true; + expect(close_event["lender"] == loanToken.address).to.be.true; + expect(close_event["loanId"] == loan_id).to.be.true; + expect(close_event["closer"] == borrower).to.be.true; + expect(close_event["loanToken"] == initial_loan["loanToken"]).to.be.true; + expect(close_event["collateralToken"] == initial_loan["collateralToken"]).to.be.true; + expect(close_event["repayAmount"] == loan_close_amount.toString()).to.be.true; + expect(close_event["collateralWithdrawAmount"] == withdraw_amount.toString()).to.be.true; + expect(close_event["collateralToLoanRate"] == collateral_to_loan_rate.toString()).to.be + .true; + expect(close_event["currentMargin"] == current_margin.toString()).to.be.true; + + // Test refund collateral to receiver + // Test refund interest to receiver + expect((await RBTC.balanceOf(receiver)).eq(withdraw_amount)).to.be.true; + expect((await SUSD.balanceOf(receiver)).eq(interest_refund_to_borrower)).to.be.true; + + // Test loan update + const end_loan = await sovryn.getLoan(loan_id); + const new_principal = loan_close_amount.eq(principal) + ? new BN(0) + : principal.sub(loan_close_amount); + expect(end_loan["principal"] == new_principal.toString()).to.be.true; + if (loan_close_amount.eq(principal)) { + const last_block_timestamp = block_timestamp; + expect(end_loan["endTimestamp"] <= last_block_timestamp); + } + + // Test returning principal to lender with deposit + const loan_close_amount_less_interest = loan_close_amount.gte( + interest_refund_to_borrower_1 + ) + ? loan_close_amount.sub(interest_refund_to_borrower_1) + : new BN(0); + + const decode2 = decodeLogs(receipt.rawLogs, IERC20, "Transfer"); + let transfer_to_lender = decode2.filter((tx_event) => tx_event.args["from"] == borrower); + expect(transfer_to_lender.length == 1).to.be.true; + transfer_to_lender = transfer_to_lender[0].args; + expect(transfer_to_lender["to"] == loanToken.address).to.be.true; + expect(transfer_to_lender["value"]).eq(loan_close_amount_less_interest.toString()); + + await verify_sov_reward_payment( + receipt.rawLogs, + FeesEvents, + SOV, + borrower, + loan_id, + sov_borrower_initial_balance, + 1, + SUSD.address, + RBTC.address, + sovryn + ); + }; + + describe("Tests the close with deposit. ", () => { + /* Test CloseWithDeposit event parameters Test refund collateral to receiver Test refund interest to receiver Test loan update Test returning principal to lender with deposit */ - it("Test full close with deposit", async () => { - // Prepare the test - const borrower = accounts[3]; - const receiver = accounts[4]; - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, accounts[2]); - // prepare the test - const [loan_id] = await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, borrower); - - await increaseTime(10 * 24 * 60 * 60); - const initial_loan = await sovryn.getLoan(loan_id); - const principal = new BN(initial_loan["principal"]); - const collateral = new BN(initial_loan["collateral"]); - const initial_loan_interest = await sovryn.getLoanInterestData(loan_id); - - const deposit_amount = principal; - await internal_test_close_with_deposit( - deposit_amount, - RBTC, - SUSD, - borrower, - collateral, - initial_loan, - initial_loan_interest, - loanToken, - loan_id, - priceFeeds, - principal, - receiver, - sovryn, - LoanClosingsEvents, - FeesEvents, - SOV - ); - }); - - it("Test full close with deposit with special rebates", async () => { - // Prepare the test - const borrower = accounts[3]; - const receiver = accounts[4]; - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, accounts[2]); - - // prepare the test - await sovryn.setSpecialRebates(SUSD.address, RBTC.address, wei("300", "ether")); - const [loan_id] = await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, borrower); - - await increaseTime(10 * 24 * 60 * 60); - const initial_loan = await sovryn.getLoan(loan_id); - const principal = new BN(initial_loan["principal"]); - const collateral = new BN(initial_loan["collateral"]); - const initial_loan_interest = await sovryn.getLoanInterestData(loan_id); - - const deposit_amount = principal; - await internal_test_close_with_deposit( - deposit_amount, - RBTC, - SUSD, - borrower, - collateral, - initial_loan, - initial_loan_interest, - loanToken, - loan_id, - priceFeeds, - principal, - receiver, - sovryn, - LoanClosingsEvents, - FeesEvents, - SOV - ); - }); - - it("Test partial close with deposit", async () => { - // Prepare the test - const borrower = accounts[3]; - const receiver = accounts[4]; - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, accounts[2]); - // prepare the test - const [loan_id] = await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, borrower); - - await increaseTime(10 * 24 * 60 * 60); - const initial_loan = await sovryn.getLoan(loan_id); - const principal = new BN(initial_loan["principal"]); - const collateral = new BN(initial_loan["collateral"]); - const initial_loan_interest = await sovryn.getLoanInterestData(loan_id); - - const deposit_amount = principal.div(new BN(2)); - await internal_test_close_with_deposit( - deposit_amount, - RBTC, - SUSD, - borrower, - collateral, - initial_loan, - initial_loan_interest, - loanToken, - loan_id, - priceFeeds, - principal, - receiver, - sovryn, - LoanClosingsEvents, - FeesEvents, - SOV - ); - }); - - it("Test partial close w/ deposit tiny position", async () => { - // Prepare the test - const borrower = accounts[3]; - const receiver = accounts[4]; - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, accounts[2]); - const [loan_id] = await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, borrower); - - await increaseTime(10 * 24 * 60 * 60); - const initial_loan = await sovryn.getLoan(loan_id); - const principal = new BN(initial_loan["principal"]); - const collateral = new BN(initial_loan["collateral"]); - const initial_loan_interest = await sovryn.getLoanInterestData(loan_id); - const deposit_amount = principal.sub(TINY_AMOUNT); - await internal_test_close_with_deposit( - deposit_amount, - RBTC, - SUSD, - borrower, - collateral, - initial_loan, - initial_loan_interest, - loanToken, - loan_id, - priceFeeds, - principal, - receiver, - sovryn, - LoanClosingsEvents, - FeesEvents, - SOV - ); - }); - - it("Test partial close w/ deposit small not so tiny position is failing!", async () => { - // Prepare the test - const borrower = accounts[3]; - const receiver = accounts[4]; - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, accounts[2]); - const [loan_id] = await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, borrower); - - await increaseTime(10 * 24 * 60 * 60); - const initial_loan = await sovryn.getLoan(loan_id); - const principal = new BN(initial_loan["principal"]); - const collateral = new BN(initial_loan["collateral"]); - const initial_loan_interest = await sovryn.getLoanInterestData(loan_id); - const deposit_amount = principal.div(new BN(300)).mul(new BN(299)); - await internal_test_close_with_deposit( - deposit_amount, - RBTC, - SUSD, - borrower, - collateral, - initial_loan, - initial_loan_interest, - loanToken, - loan_id, - priceFeeds, - principal, - receiver, - sovryn, - LoanClosingsEvents, - FeesEvents, - SOV - ); - }); - - it("Test partial close w/ swap tiny position", async () => { - // Prepare the test - const borrower = accounts[3]; - const receiver = accounts[4]; - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, accounts[2]); - const [loan_id] = await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, borrower); - - await increaseTime(10 * 24 * 60 * 60); - const initial_loan = await sovryn.getLoan(loan_id); - const principal = new BN(initial_loan["principal"]); - const collateral = new BN(initial_loan["collateral"]); - const initial_loan_interest = await sovryn.getLoanInterestData(loan_id); - const swap_amount = principal.sub(TINY_AMOUNT); - const return_token_is_collateral = true; // true: withdraws collateralToken, false: withdraws loanToken - const { receipt } = await sovryn.closeWithSwap(loan_id, borrower, swap_amount, return_token_is_collateral, "0x", { - from: borrower, - }); - - // Test loan update - end_loan = await sovryn.getLoan.call(loan_id); - expect(end_loan["principal"]).to.be.bignumber.equal(new BN(0), "principal should be 0"); - expect(end_loan["collateral"]).to.be.bignumber.equal(new BN(0), "collateral should be 0"); - }); - - it("Test close with zero deposit should fail", async () => { - // Prepare the test - const borrower = accounts[3]; - const receiver = accounts[4]; - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, accounts[2]); - // prepare the test - const [loan_id] = await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, borrower); - - await increaseTime(10 * 24 * 60 * 60); - - await expectRevert(sovryn.closeWithDeposit(loan_id, receiver, 0, { from: borrower }), "depositAmount == 0"); - }); - }); + it("Test full close with deposit", async () => { + // Prepare the test + const borrower = accounts[3]; + const receiver = accounts[4]; + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, accounts[2]); + // prepare the test + const [loan_id] = await open_margin_trade_position( + loanToken, + RBTC, + WRBTC, + SUSD, + borrower + ); + + await increaseTime(10 * 24 * 60 * 60); + const initial_loan = await sovryn.getLoan(loan_id); + const principal = new BN(initial_loan["principal"]); + const collateral = new BN(initial_loan["collateral"]); + const initial_loan_interest = await sovryn.getLoanInterestData(loan_id); + + const deposit_amount = principal; + await internal_test_close_with_deposit( + deposit_amount, + RBTC, + SUSD, + borrower, + collateral, + initial_loan, + initial_loan_interest, + loanToken, + loan_id, + priceFeeds, + principal, + receiver, + sovryn, + LoanClosingsEvents, + FeesEvents, + SOV + ); + }); + + it("Test full close with deposit with special rebates", async () => { + // Prepare the test + const borrower = accounts[3]; + const receiver = accounts[4]; + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, accounts[2]); + + // prepare the test + await sovryn.setSpecialRebates(SUSD.address, RBTC.address, wei("300", "ether")); + const [loan_id] = await open_margin_trade_position( + loanToken, + RBTC, + WRBTC, + SUSD, + borrower + ); + + await increaseTime(10 * 24 * 60 * 60); + const initial_loan = await sovryn.getLoan(loan_id); + const principal = new BN(initial_loan["principal"]); + const collateral = new BN(initial_loan["collateral"]); + const initial_loan_interest = await sovryn.getLoanInterestData(loan_id); + + const deposit_amount = principal; + await internal_test_close_with_deposit( + deposit_amount, + RBTC, + SUSD, + borrower, + collateral, + initial_loan, + initial_loan_interest, + loanToken, + loan_id, + priceFeeds, + principal, + receiver, + sovryn, + LoanClosingsEvents, + FeesEvents, + SOV + ); + }); + + it("Test partial close with deposit", async () => { + // Prepare the test + const borrower = accounts[3]; + const receiver = accounts[4]; + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, accounts[2]); + // prepare the test + const [loan_id] = await open_margin_trade_position( + loanToken, + RBTC, + WRBTC, + SUSD, + borrower + ); + + await increaseTime(10 * 24 * 60 * 60); + const initial_loan = await sovryn.getLoan(loan_id); + const principal = new BN(initial_loan["principal"]); + const collateral = new BN(initial_loan["collateral"]); + const initial_loan_interest = await sovryn.getLoanInterestData(loan_id); + + const deposit_amount = principal.div(new BN(2)); + await internal_test_close_with_deposit( + deposit_amount, + RBTC, + SUSD, + borrower, + collateral, + initial_loan, + initial_loan_interest, + loanToken, + loan_id, + priceFeeds, + principal, + receiver, + sovryn, + LoanClosingsEvents, + FeesEvents, + SOV + ); + }); + + it("Test partial close w/ deposit tiny position", async () => { + // Prepare the test + const borrower = accounts[3]; + const receiver = accounts[4]; + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, accounts[2]); + const [loan_id] = await open_margin_trade_position( + loanToken, + RBTC, + WRBTC, + SUSD, + borrower + ); + + await increaseTime(10 * 24 * 60 * 60); + const initial_loan = await sovryn.getLoan(loan_id); + const principal = new BN(initial_loan["principal"]); + const collateral = new BN(initial_loan["collateral"]); + const initial_loan_interest = await sovryn.getLoanInterestData(loan_id); + const deposit_amount = principal.sub(TINY_AMOUNT); + await internal_test_close_with_deposit( + deposit_amount, + RBTC, + SUSD, + borrower, + collateral, + initial_loan, + initial_loan_interest, + loanToken, + loan_id, + priceFeeds, + principal, + receiver, + sovryn, + LoanClosingsEvents, + FeesEvents, + SOV + ); + }); + + it("Test partial close w/ deposit small not so tiny position is failing!", async () => { + // Prepare the test + const borrower = accounts[3]; + const receiver = accounts[4]; + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, accounts[2]); + const [loan_id] = await open_margin_trade_position( + loanToken, + RBTC, + WRBTC, + SUSD, + borrower + ); + + await increaseTime(10 * 24 * 60 * 60); + const initial_loan = await sovryn.getLoan(loan_id); + const principal = new BN(initial_loan["principal"]); + const collateral = new BN(initial_loan["collateral"]); + const initial_loan_interest = await sovryn.getLoanInterestData(loan_id); + const deposit_amount = principal.div(new BN(300)).mul(new BN(299)); + await internal_test_close_with_deposit( + deposit_amount, + RBTC, + SUSD, + borrower, + collateral, + initial_loan, + initial_loan_interest, + loanToken, + loan_id, + priceFeeds, + principal, + receiver, + sovryn, + LoanClosingsEvents, + FeesEvents, + SOV + ); + }); + + it("Test partial close w/ swap tiny position", async () => { + // Prepare the test + const borrower = accounts[3]; + const receiver = accounts[4]; + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, accounts[2]); + const [loan_id] = await open_margin_trade_position( + loanToken, + RBTC, + WRBTC, + SUSD, + borrower + ); + + await increaseTime(10 * 24 * 60 * 60); + const initial_loan = await sovryn.getLoan(loan_id); + const principal = new BN(initial_loan["principal"]); + const collateral = new BN(initial_loan["collateral"]); + const initial_loan_interest = await sovryn.getLoanInterestData(loan_id); + const swap_amount = principal.sub(TINY_AMOUNT); + const return_token_is_collateral = true; // true: withdraws collateralToken, false: withdraws loanToken + const { receipt } = await sovryn.closeWithSwap( + loan_id, + borrower, + swap_amount, + return_token_is_collateral, + "0x", + { + from: borrower, + } + ); + + // Test loan update + end_loan = await sovryn.getLoan.call(loan_id); + expect(end_loan["principal"]).to.be.bignumber.equal( + new BN(0), + "principal should be 0" + ); + expect(end_loan["collateral"]).to.be.bignumber.equal( + new BN(0), + "collateral should be 0" + ); + }); + + it("Test close with zero deposit should fail", async () => { + // Prepare the test + const borrower = accounts[3]; + const receiver = accounts[4]; + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, accounts[2]); + // prepare the test + const [loan_id] = await open_margin_trade_position( + loanToken, + RBTC, + WRBTC, + SUSD, + borrower + ); + + await increaseTime(10 * 24 * 60 * 60); + + await expectRevert( + sovryn.closeWithDeposit(loan_id, receiver, 0, { from: borrower }), + "depositAmount == 0" + ); + }); + }); }); diff --git a/tests/protocol/DepositCollateralTestToken.test.js b/tests/protocol/DepositCollateralTestToken.test.js index 9d40b0edb..421273383 100644 --- a/tests/protocol/DepositCollateralTestToken.test.js +++ b/tests/protocol/DepositCollateralTestToken.test.js @@ -24,20 +24,20 @@ const LoanMaintenance = artifacts.require("LoanMaintenance"); const LoanOpenings = artifacts.require("LoanOpenings"); const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getLoanToken, - getLoanTokenWRBTC, - loan_pool_setup, - set_demand_curve, - lend_to_pool, - getPriceFeeds, - getSovryn, - getSOV, - decodeLogs, - open_margin_trade_position, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getLoanToken, + getLoanTokenWRBTC, + loan_pool_setup, + set_demand_curve, + lend_to_pool, + getPriceFeeds, + getSovryn, + getSOV, + decodeLogs, + open_margin_trade_position, } = require("../Utils/initializer.js"); const wei = web3.utils.toWei; @@ -50,191 +50,266 @@ const hunEth = new BN(wei("100", "ether")); // the same form truffle-contract uses on its receipts contract("ProtocolAddingMargin", (accounts) => { - let owner; - let sovryn, SUSD, WRBTC, RBTC, BZRX, loanToken, loanTokenWRBTC, priceFeeds; - - async function deploymentAndInitFixture(_wallets, _provider) { - // Deploying sovrynProtocol w/ generic function from initializer.js - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - sov = await getSOV(sovryn, priceFeeds, SUSD, accounts); - - loanToken = await getLoanToken(owner, sovryn, WRBTC, SUSD); - loanTokenWRBTC = await getLoanTokenWRBTC(owner, sovryn, WRBTC, SUSD); - await loan_pool_setup(sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC); - - /// @dev Moved from some tests that require this initialization - /// and is not interfering w/ any others. - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, owner); - } - - before(async () => { - [owner, account1, ...accounts] = accounts; - }); - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - describe("Adding Margin", () => { - it("Test deposit collateral", async () => { - // prepare the test - const [loan_id] = await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, owner); - const loadData = await sovryn.getLoan(loan_id); - const startCollateral = new BN(loadData["collateral"]); - const deposit_amount = startCollateral.div(new BN(2)); - - // deposit collateral to add margin to the loan created above - await RBTC.approve(sovryn.address, deposit_amount); - const { receipt } = await sovryn.depositCollateral(loan_id, deposit_amount); - const { collateralToLoanRate } = await priceFeeds.getCurrentMargin( - loadData["loanToken"], - loadData["collateralToken"], - loadData["principal"], - loadData["collateral"] - ); - const decode = decodeLogs(receipt.rawLogs, LoanMaintenanceEvents, "DepositCollateral"); - const args = decode[0].args; - - // verify the deposit collateral event - - expect(args["loanId"] == loan_id).to.be.true; - expect(args["depositAmount"]).to.eq(deposit_amount.toString()); - expect(args["rate"]).to.eq(collateralToLoanRate.toString()); - - // make sure, collateral was increased - const endCollateral = (await sovryn.getLoan(loan_id))["collateral"]; - expect(new BN(endCollateral).sub(startCollateral).eq(deposit_amount)).to.be.true; - }); - - it("Test deposit collateral to non existent loan", async () => { - // try to deposit collateral to a loan with id 0 - await RBTC.approve(sovryn.address, new BN(10).pow(new BN(15))); - expectRevert(sovryn.depositCollateral("0x0", new BN(10).pow(new BN(15))), "loan is closed"); - }); - - it("Test deposit collateral 0 value", async () => { - // prepare the test - const [loan_id] = await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, owner); - expectRevert(sovryn.depositCollateral(loan_id, new BN(0)), "depositAmount is 0"); - }); - - it("Test deposit collateral sending Ether instead of RBTC", async () => { - // prepare the test - const [loan_id] = await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, owner); - expectRevert(sovryn.depositCollateral(loan_id, new BN(10).pow(new BN(15)), { value: new BN(1) }), "wrong asset sent"); - }); - - it("Test deposit collateral sending Ether as collateral", async () => { - /// @dev open_margin_trade_position cannot be used to perform this check - let trader = owner; - let loan_token_sent = hunEth.toString(); - let leverage_amount = new BN(2).mul(oneEth).toString(); - let collateralToken = zeroAddress; - await SUSD.mint(trader, loan_token_sent); - await SUSD.approve(loanToken.address, loan_token_sent, { from: trader }); - const { receipt } = await loanToken.marginTrade( - "0x0", // loanId (0 for new loans) - leverage_amount, // leverageAmount - loan_token_sent, // loanTokenSent - 0, // no collateral token sent - collateralToken, // collateralTokenAddress - trader, // trader, - 0, // slippage - [], // loanDataBytes (only required with ether) - { from: trader } - ); - const decode = decodeLogs(receipt.rawLogs, LoanOpenings, "Trade"); - let loan_id = decode[0].args["loanId"]; - - let depositAmount = new BN(10).pow(new BN(15)); - expectRevert(sovryn.depositCollateral(loan_id, depositAmount, { value: new BN(1) }), "ether deposit mismatch"); - await sovryn.depositCollateral(loan_id, depositAmount, { value: depositAmount }); - }); - - it("should fail LoanMaintenance fallback", async () => { - let newLoanMaintenanceAddr = await LoanMaintenance.new(); - await expectRevert( - newLoanMaintenanceAddr.send(wei("0.0000000000000001", "ether")), - "fallback function is not payable and was called with value 100" - ); - await expectRevert(newLoanMaintenanceAddr.sendTransaction({}), "fallback not allowed"); - }); - }); - - describe("Reducing Margin", () => { - it("should revert when withdrawAmount is 0", async () => { - // prepare the test - const [loan_id] = await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, owner); - - let withdrawAmount = new BN(0); - await expectRevert(sovryn.withdrawCollateral(loan_id, owner, withdrawAmount), "withdrawAmount is 0"); - }); - - it("should revert when sender is nor borrower neither delegated", async () => { - // prepare the test - const [loan_id] = await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, owner); - - let withdrawAmount = new BN(10).pow(new BN(15)); - await expectRevert(sovryn.withdrawCollateral(loan_id, owner, withdrawAmount, { from: account1 }), "unauthorized"); - }); - - it("should revert when loan is closed", async () => { - // prepare the test - const [loan_id] = await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, owner); - - // close the loan - const loadData = await sovryn.getLoan(loan_id); - const collateral = new BN(loadData["collateral"]); - await sovryn.closeWithSwap(loan_id, owner, collateral, false, "0x", { from: owner }); - - let withdrawAmount = new BN(10).pow(new BN(15)); - await expectRevert(sovryn.withdrawCollateral(loan_id, owner, withdrawAmount), "loan is closed"); - }); - - it("should reduce the margin when calling withdrawCollateral", async () => { - // prepare the test - const [loan_id] = await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, owner); - - // owner initial balance should be 10^50 - expect(await RBTC.balanceOf(owner)).bignumber.equal(new BN(10).pow(new BN(50))); - - let withdrawAmount = new BN(10).pow(new BN(15)); - await sovryn.withdrawCollateral(loan_id, owner, withdrawAmount); - - // owner final balance should be 10^50 + withdrawAmount(10^15) - expect(await RBTC.balanceOf(owner)).bignumber.equal(new BN(10).pow(new BN(50)).add(withdrawAmount)); - - // maxDrawdown = 5647468293983077 (aprox. 5.6*10^15) - // trying a withdrawal a bit higher (5.7*10^15) than available - let withdrawAmount2 = new BN(57).mul(new BN(10).pow(new BN(14))); - await sovryn.withdrawCollateral(loan_id, owner, withdrawAmount2); - - // owner final balance cannot be 10^50 + withdrawAmount(10^15) + withdrawAmount2(5.7*10^15), but a bit lower - expect(await RBTC.balanceOf(owner)).bignumber.lessThan(new BN(10).pow(new BN(50)).add(withdrawAmount).add(withdrawAmount2)); - }); - - it("should work when collateral is WRBTC", async () => { - // prepare the test - const [loan_id] = await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, owner, "WRBTC"); - - // send Ether collateral - let depositAmount = new BN(10).pow(new BN(15)); - await sovryn.depositCollateral(loan_id, depositAmount, { value: depositAmount }); - - // get collateral - let withdrawAmount = new BN(10).pow(new BN(15)); - await sovryn.withdrawCollateral(loan_id, owner, withdrawAmount); - - // should revert if collateral requested is higher than available - // Try to get collateral again - await expectRevert.unspecified(sovryn.withdrawCollateral(loan_id, owner, withdrawAmount)); - }); - }); + let owner; + let sovryn, SUSD, WRBTC, RBTC, BZRX, loanToken, loanTokenWRBTC, priceFeeds; + + async function deploymentAndInitFixture(_wallets, _provider) { + // Deploying sovrynProtocol w/ generic function from initializer.js + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + sov = await getSOV(sovryn, priceFeeds, SUSD, accounts); + + loanToken = await getLoanToken(owner, sovryn, WRBTC, SUSD); + loanTokenWRBTC = await getLoanTokenWRBTC(owner, sovryn, WRBTC, SUSD); + await loan_pool_setup(sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC); + + /// @dev Moved from some tests that require this initialization + /// and is not interfering w/ any others. + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, owner); + } + + before(async () => { + [owner, account1, ...accounts] = accounts; + }); + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + describe("Adding Margin", () => { + it("Test deposit collateral", async () => { + // prepare the test + const [loan_id] = await open_margin_trade_position( + loanToken, + RBTC, + WRBTC, + SUSD, + owner + ); + const loadData = await sovryn.getLoan(loan_id); + const startCollateral = new BN(loadData["collateral"]); + const deposit_amount = startCollateral.div(new BN(2)); + + // deposit collateral to add margin to the loan created above + await RBTC.approve(sovryn.address, deposit_amount); + const { receipt } = await sovryn.depositCollateral(loan_id, deposit_amount); + const { collateralToLoanRate } = await priceFeeds.getCurrentMargin( + loadData["loanToken"], + loadData["collateralToken"], + loadData["principal"], + loadData["collateral"] + ); + const decode = decodeLogs(receipt.rawLogs, LoanMaintenanceEvents, "DepositCollateral"); + const args = decode[0].args; + + // verify the deposit collateral event + + expect(args["loanId"] == loan_id).to.be.true; + expect(args["depositAmount"]).to.eq(deposit_amount.toString()); + expect(args["rate"]).to.eq(collateralToLoanRate.toString()); + + // make sure, collateral was increased + const endCollateral = (await sovryn.getLoan(loan_id))["collateral"]; + expect(new BN(endCollateral).sub(startCollateral).eq(deposit_amount)).to.be.true; + }); + + it("Test deposit collateral to non existent loan", async () => { + // try to deposit collateral to a loan with id 0 + await RBTC.approve(sovryn.address, new BN(10).pow(new BN(15))); + expectRevert( + sovryn.depositCollateral("0x0", new BN(10).pow(new BN(15))), + "loan is closed" + ); + }); + + it("Test deposit collateral 0 value", async () => { + // prepare the test + const [loan_id] = await open_margin_trade_position( + loanToken, + RBTC, + WRBTC, + SUSD, + owner + ); + expectRevert(sovryn.depositCollateral(loan_id, new BN(0)), "depositAmount is 0"); + }); + + it("Test deposit collateral sending Ether instead of RBTC", async () => { + // prepare the test + const [loan_id] = await open_margin_trade_position( + loanToken, + RBTC, + WRBTC, + SUSD, + owner + ); + expectRevert( + sovryn.depositCollateral(loan_id, new BN(10).pow(new BN(15)), { + value: new BN(1), + }), + "wrong asset sent" + ); + }); + + it("Test deposit collateral sending Ether as collateral", async () => { + /// @dev open_margin_trade_position cannot be used to perform this check + let trader = owner; + let loan_token_sent = hunEth.toString(); + let leverage_amount = new BN(2).mul(oneEth).toString(); + let collateralToken = zeroAddress; + await SUSD.mint(trader, loan_token_sent); + await SUSD.approve(loanToken.address, loan_token_sent, { from: trader }); + const { receipt } = await loanToken.marginTrade( + "0x0", // loanId (0 for new loans) + leverage_amount, // leverageAmount + loan_token_sent, // loanTokenSent + 0, // no collateral token sent + collateralToken, // collateralTokenAddress + trader, // trader, + 0, // slippage + [], // loanDataBytes (only required with ether) + { from: trader } + ); + const decode = decodeLogs(receipt.rawLogs, LoanOpenings, "Trade"); + let loan_id = decode[0].args["loanId"]; + + let depositAmount = new BN(10).pow(new BN(15)); + expectRevert( + sovryn.depositCollateral(loan_id, depositAmount, { value: new BN(1) }), + "ether deposit mismatch" + ); + await sovryn.depositCollateral(loan_id, depositAmount, { value: depositAmount }); + }); + + it("should fail LoanMaintenance fallback", async () => { + let newLoanMaintenanceAddr = await LoanMaintenance.new(); + await expectRevert( + newLoanMaintenanceAddr.send(wei("0.0000000000000001", "ether")), + "fallback function is not payable and was called with value 100" + ); + await expectRevert(newLoanMaintenanceAddr.sendTransaction({}), "fallback not allowed"); + }); + }); + + describe("Reducing Margin", () => { + it("should revert when withdrawAmount is 0", async () => { + // prepare the test + const [loan_id] = await open_margin_trade_position( + loanToken, + RBTC, + WRBTC, + SUSD, + owner + ); + + let withdrawAmount = new BN(0); + await expectRevert( + sovryn.withdrawCollateral(loan_id, owner, withdrawAmount), + "withdrawAmount is 0" + ); + }); + + it("should revert when sender is nor borrower neither delegated", async () => { + // prepare the test + const [loan_id] = await open_margin_trade_position( + loanToken, + RBTC, + WRBTC, + SUSD, + owner + ); + + let withdrawAmount = new BN(10).pow(new BN(15)); + await expectRevert( + sovryn.withdrawCollateral(loan_id, owner, withdrawAmount, { from: account1 }), + "unauthorized" + ); + }); + + it("should revert when loan is closed", async () => { + // prepare the test + const [loan_id] = await open_margin_trade_position( + loanToken, + RBTC, + WRBTC, + SUSD, + owner + ); + + // close the loan + const loadData = await sovryn.getLoan(loan_id); + const collateral = new BN(loadData["collateral"]); + await sovryn.closeWithSwap(loan_id, owner, collateral, false, "0x", { from: owner }); + + let withdrawAmount = new BN(10).pow(new BN(15)); + await expectRevert( + sovryn.withdrawCollateral(loan_id, owner, withdrawAmount), + "loan is closed" + ); + }); + + it("should reduce the margin when calling withdrawCollateral", async () => { + // prepare the test + const [loan_id] = await open_margin_trade_position( + loanToken, + RBTC, + WRBTC, + SUSD, + owner + ); + + // owner initial balance should be 10^50 + expect(await RBTC.balanceOf(owner)).bignumber.equal(new BN(10).pow(new BN(50))); + + let withdrawAmount = new BN(10).pow(new BN(15)); + await sovryn.withdrawCollateral(loan_id, owner, withdrawAmount); + + // owner final balance should be 10^50 + withdrawAmount(10^15) + expect(await RBTC.balanceOf(owner)).bignumber.equal( + new BN(10).pow(new BN(50)).add(withdrawAmount) + ); + + // maxDrawdown = 5647468293983077 (aprox. 5.6*10^15) + // trying a withdrawal a bit higher (5.7*10^15) than available + let withdrawAmount2 = new BN(57).mul(new BN(10).pow(new BN(14))); + await sovryn.withdrawCollateral(loan_id, owner, withdrawAmount2); + + // owner final balance cannot be 10^50 + withdrawAmount(10^15) + withdrawAmount2(5.7*10^15), but a bit lower + expect(await RBTC.balanceOf(owner)).bignumber.lessThan( + new BN(10).pow(new BN(50)).add(withdrawAmount).add(withdrawAmount2) + ); + }); + + it("should work when collateral is WRBTC", async () => { + // prepare the test + const [loan_id] = await open_margin_trade_position( + loanToken, + RBTC, + WRBTC, + SUSD, + owner, + "WRBTC" + ); + + // send Ether collateral + let depositAmount = new BN(10).pow(new BN(15)); + await sovryn.depositCollateral(loan_id, depositAmount, { value: depositAmount }); + + // get collateral + let withdrawAmount = new BN(10).pow(new BN(15)); + await sovryn.withdrawCollateral(loan_id, owner, withdrawAmount); + + // should revert if collateral requested is higher than available + // Try to get collateral again + await expectRevert.unspecified( + sovryn.withdrawCollateral(loan_id, owner, withdrawAmount) + ); + }); + }); }); diff --git a/tests/protocol/LiquidationTestToken.test.js b/tests/protocol/LiquidationTestToken.test.js index 129ad05f2..2c4db3cf5 100644 --- a/tests/protocol/LiquidationTestToken.test.js +++ b/tests/protocol/LiquidationTestToken.test.js @@ -16,21 +16,25 @@ const FeesEvents = artifacts.require("FeesEvents"); const LoanClosingsEvents = artifacts.require("LoanClosingsEvents"); const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getLoanToken, - getLoanTokenWRBTC, - loan_pool_setup, - set_demand_curve, - getPriceFeeds, - getSovryn, - getSOV, - decodeLogs, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getLoanToken, + getLoanTokenWRBTC, + loan_pool_setup, + set_demand_curve, + getPriceFeeds, + getSovryn, + getSOV, + decodeLogs, } = require("../Utils/initializer.js"); -const { liquidate, liquidate_healthy_position_should_fail, prepare_liquidation } = require("./liquidationFunctions"); +const { + liquidate, + liquidate_healthy_position_should_fail, + prepare_liquidation, +} = require("./liquidationFunctions"); const { increaseTime } = require("../Utils/Ethereum"); @@ -44,343 +48,423 @@ Should test the liquidation handling */ contract("ProtocolLiquidationTestToken", (accounts) => { - let owner; - let sovryn, SUSD, WRBTC, RBTC, BZRX, loanToken, loanTokenWRBTC, priceFeeds, SOV; + let owner; + let sovryn, SUSD, WRBTC, RBTC, BZRX, loanToken, loanTokenWRBTC, priceFeeds, SOV; - async function deploymentAndInitFixture(_wallets, _provider) { - // Deploying sovrynProtocol w/ generic function from initializer.js - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + async function deploymentAndInitFixture(_wallets, _provider) { + // Deploying sovrynProtocol w/ generic function from initializer.js + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - loanToken = await getLoanToken(owner, sovryn, WRBTC, SUSD); - loanTokenWRBTC = await getLoanTokenWRBTC(owner, sovryn, WRBTC, SUSD); - await loan_pool_setup(sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC); + loanToken = await getLoanToken(owner, sovryn, WRBTC, SUSD); + loanTokenWRBTC = await getLoanTokenWRBTC(owner, sovryn, WRBTC, SUSD); + await loan_pool_setup(sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC); - /// @dev SOV test token deployment w/ initializer.js - SOV = await getSOV(sovryn, priceFeeds, SUSD, accounts); - } + /// @dev SOV test token deployment w/ initializer.js + SOV = await getSOV(sovryn, priceFeeds, SUSD, accounts); + } - before(async () => { - [owner] = accounts; - }); + before(async () => { + [owner] = accounts; + }); - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); - describe("Tests liquidation handling ", () => { - /* + describe("Tests liquidation handling ", () => { + /* Test with different rates so the currentMargin is <= liquidationIncentivePercent or > liquidationIncentivePercent liquidationIncentivePercent = 5e18 by default */ - it("Test liquidate with rate 1e21", async () => { - const rate = new BN(10).pow(new BN(21)); - await liquidate(accounts, loanToken, SUSD, set_demand_curve, RBTC, sovryn, priceFeeds, rate, WRBTC, FeesEvents, SOV); - }); - - it("Test liquidate with rate 1e21 (special rebates)", async () => { - const rate = new BN(10).pow(new BN(21)); - await liquidate(accounts, loanToken, SUSD, set_demand_curve, RBTC, sovryn, priceFeeds, rate, WRBTC, FeesEvents, SOV, true); - }); - - it("Test liquidate with rate 6.7e21", async () => { - const rate = new BN(67).mul(new BN(10).pow(new BN(20))); - await liquidate(accounts, loanToken, SUSD, set_demand_curve, RBTC, sovryn, priceFeeds, rate, WRBTC, FeesEvents, SOV); - }); - - it("Test liquidate with rate 6.7e21 (special rebates)", async () => { - const rate = new BN(67).mul(new BN(10).pow(new BN(20))); - await liquidate(accounts, loanToken, SUSD, set_demand_curve, RBTC, sovryn, priceFeeds, rate, WRBTC, FeesEvents, SOV, true); - }); - - it("Test coverage: Trigger maxLiquidatable: ad hoc rate to be unhealthy and currentMargin > incentivePercent", async () => { - /// @dev Healthy when rate aprox. > 8*10^21 - /// @dev We need unhealthy to liquidate - /// @dev Not enough margin when rate aprox. < 7*10^21 - - /// @dev This rate triggers the maxLiquidatable computation in the contract - /// but the uncovered conditions: - /// if (maxLiquidatable > principal) { - /// and - /// if (maxSeizable > collateral) { - /// cannot ever be met inside the range (8*10^21 > rate > 7*10^21) - - const rate = new BN(10).pow(new BN(20)).mul(new BN(72)); - - /// @dev It should liquidate but not entirely: - /// principal = 20267418874797325811 - /// maxLiquidatable = 18355350998378606486 - /// Do not check RepayAmount => last parameter set to false - await liquidate( - accounts, - loanToken, - SUSD, - set_demand_curve, - RBTC, - sovryn, - priceFeeds, - rate, - WRBTC, - FeesEvents, - SOV, - false, - false - ); - }); - - /* + it("Test liquidate with rate 1e21", async () => { + const rate = new BN(10).pow(new BN(21)); + await liquidate( + accounts, + loanToken, + SUSD, + set_demand_curve, + RBTC, + sovryn, + priceFeeds, + rate, + WRBTC, + FeesEvents, + SOV + ); + }); + + it("Test liquidate with rate 1e21 (special rebates)", async () => { + const rate = new BN(10).pow(new BN(21)); + await liquidate( + accounts, + loanToken, + SUSD, + set_demand_curve, + RBTC, + sovryn, + priceFeeds, + rate, + WRBTC, + FeesEvents, + SOV, + true + ); + }); + + it("Test liquidate with rate 6.7e21", async () => { + const rate = new BN(67).mul(new BN(10).pow(new BN(20))); + await liquidate( + accounts, + loanToken, + SUSD, + set_demand_curve, + RBTC, + sovryn, + priceFeeds, + rate, + WRBTC, + FeesEvents, + SOV + ); + }); + + it("Test liquidate with rate 6.7e21 (special rebates)", async () => { + const rate = new BN(67).mul(new BN(10).pow(new BN(20))); + await liquidate( + accounts, + loanToken, + SUSD, + set_demand_curve, + RBTC, + sovryn, + priceFeeds, + rate, + WRBTC, + FeesEvents, + SOV, + true + ); + }); + + it("Test coverage: Trigger maxLiquidatable: ad hoc rate to be unhealthy and currentMargin > incentivePercent", async () => { + /// @dev Healthy when rate aprox. > 8*10^21 + /// @dev We need unhealthy to liquidate + /// @dev Not enough margin when rate aprox. < 7*10^21 + + /// @dev This rate triggers the maxLiquidatable computation in the contract + /// but the uncovered conditions: + /// if (maxLiquidatable > principal) { + /// and + /// if (maxSeizable > collateral) { + /// cannot ever be met inside the range (8*10^21 > rate > 7*10^21) + + const rate = new BN(10).pow(new BN(20)).mul(new BN(72)); + + /// @dev It should liquidate but not entirely: + /// principal = 20267418874797325811 + /// maxLiquidatable = 18355350998378606486 + /// Do not check RepayAmount => last parameter set to false + await liquidate( + accounts, + loanToken, + SUSD, + set_demand_curve, + RBTC, + sovryn, + priceFeeds, + rate, + WRBTC, + FeesEvents, + SOV, + false, + false + ); + }); + + /* Test if fails when the position is healthy currentMargin > maintenanceRate */ - it("Should fail liquidating a healthy position", async () => { - await liquidate_healthy_position_should_fail(accounts, loanToken, SUSD, set_demand_curve, RBTC, sovryn, priceFeeds, WRBTC); - }); - - it("Should fail liquidating a closed loan", async () => { - // Close the loan - await set_demand_curve(loanToken); - const lender = accounts[0]; - const borrower = accounts[1]; - const receiver = borrower; - const liquidator = accounts[2]; - const loan_token_sent = new BN(10).mul(oneEth); - const loan_id = await prepare_liquidation( - lender, - borrower, - liquidator, - loan_token_sent, - loanToken, - SUSD, // underlyingToken - RBTC, // collateralToken - sovryn, - WRBTC - ); - /// @dev Only way to close the loan into an inactive state is - /// by depositing the exact amount of the principal. - let deposit_amount = new BN("20267418874797325811"); // loanLocal.principal - await SUSD.mint(borrower, deposit_amount); - await SUSD.approve(sovryn.address, deposit_amount, { from: borrower }); - await sovryn.closeWithDeposit(loan_id, receiver, deposit_amount, { from: borrower }); - - // Try to liquidate an inactive loan - let value = 0; - await expectRevert( - sovryn.liquidate(loan_id, liquidator, loan_token_sent, { from: liquidator, value: value }), - "loan is closed" - ); - }); - - it("Should work liquidating an unhealthy loan", async () => { - // Close the loan - await set_demand_curve(loanToken); - const lender = accounts[0]; - const borrower = accounts[1]; - const receiver = borrower; - const liquidator = accounts[2]; - const loan_token_sent = new BN(10).mul(oneEth); - const loan_id = await prepare_liquidation( - lender, - borrower, - liquidator, - loan_token_sent, - loanToken, - SUSD, // underlyingToken - RBTC, // collateralToken - sovryn, - WRBTC - ); - - // Ad hoc rate for unhealthy loan - const rate = new BN(10).pow(new BN(20)).mul(new BN(72)); - await priceFeeds.setRates(RBTC.address, SUSD.address, rate); - - // Liquidate an unhealthy loan - let value = 0; - await sovryn.liquidate(loan_id, liquidator, loan_token_sent, { from: liquidator, value: value }); - }); - - it("Should revert liquidating w/ wrong asset", async () => { - // Close the loan - await set_demand_curve(loanToken); - const lender = accounts[0]; - const borrower = accounts[1]; - const receiver = borrower; - const liquidator = accounts[2]; - const loan_token_sent = new BN(10).mul(oneEth); - const loan_id = await prepare_liquidation( - lender, - borrower, - liquidator, - loan_token_sent, - loanToken, - SUSD, // underlyingToken - RBTC, // collateralToken - sovryn, - WRBTC - ); - - // Ad hoc rate for unhealthy loan - const rate = new BN(10).pow(new BN(20)).mul(new BN(72)); - await priceFeeds.setRates(RBTC.address, SUSD.address, rate); - - // Try to liquidate by sending Ether - let value = 10; - await expectRevert( - sovryn.liquidate(loan_id, liquidator, loan_token_sent, { from: liquidator, value: value }), - "wrong asset sent" - ); - }); - - it("Should fail rolling over a closed loan", async () => { - // Close the loan - await set_demand_curve(loanToken); - const lender = accounts[0]; - const borrower = accounts[1]; - const receiver = borrower; - const liquidator = accounts[2]; - const loan_token_sent = new BN(10).mul(oneEth); - const loan_id = await prepare_liquidation( - lender, - borrower, - liquidator, - loan_token_sent, - loanToken, - SUSD, // underlyingToken - RBTC, // collateralToken - sovryn, - WRBTC - ); - /// @dev Only way to close the loan into an inactive state is - /// by depositing the exact amount of the principal. - let deposit_amount = new BN("20267418874797325811"); // loanLocal.principal - await SUSD.mint(borrower, deposit_amount); - await SUSD.approve(sovryn.address, deposit_amount, { from: borrower }); - await sovryn.closeWithDeposit(loan_id, receiver, deposit_amount, { from: borrower }); - - // Try to liquidate an inactive loan - let value = 0; - await expectRevert(sovryn.rollover(loan_id, "0x", { from: liquidator, value: value }), "loan is closed"); - }); - - it("Should fail rolling over a healthy position", async () => { - // Prepare the loan - await set_demand_curve(loanToken); - const lender = accounts[0]; - const borrower = accounts[1]; - const receiver = borrower; - const liquidator = accounts[2]; - const loan_token_sent = new BN(10).mul(oneEth); - const loan_id = await prepare_liquidation( - lender, - borrower, - liquidator, - loan_token_sent, - loanToken, - SUSD, // underlyingToken - RBTC, // collateralToken - sovryn, - WRBTC - ); - - // Try to roll over a healthy loan - let value = 0; - await expectRevert(sovryn.rollover(loan_id, "0x", { from: liquidator, value: value }), "healthy position"); - }); - - it("Should work rolling over an unhealthy position", async () => { - // Prepare the loan - await set_demand_curve(loanToken); - const lender = accounts[0]; - const borrower = accounts[1]; - const receiver = borrower; - const liquidator = accounts[2]; - const loan_token_sent = new BN(10).mul(oneEth); - const loan_id = await prepare_liquidation( - lender, - borrower, - liquidator, - loan_token_sent, - loanToken, - SUSD, // underlyingToken - RBTC, // collateralToken - sovryn, - WRBTC - ); - - // time travel 100 days turns position into an unhealthy state - await increaseTime(8640000); - - // Roll over an unhealthy loan - let value = 0; - await sovryn.rollover(loan_id, "0x", { from: liquidator, value: value }); - }); - - it("should work when setting a delegated manager", async () => { - // Prepare the loan - await set_demand_curve(loanToken); - const lender = accounts[0]; - const borrower = accounts[1]; - const receiver = borrower; - const liquidator = accounts[2]; - const loan_token_sent = new BN(10).mul(oneEth); - const loan_id = await prepare_liquidation( - lender, - borrower, - liquidator, - loan_token_sent, - loanToken, - SUSD, // underlyingToken - RBTC, // collateralToken - sovryn, - WRBTC - ); - - // Set a delegated manager - await sovryn.setDelegatedManager(loan_id, borrower, true, { from: borrower }); - }); - - it("should revert when setting a delegated manager by other than borrower", async () => { - // Prepare the loan - await set_demand_curve(loanToken); - const lender = accounts[0]; - const borrower = accounts[1]; - const receiver = borrower; - const liquidator = accounts[2]; - const loan_token_sent = new BN(10).mul(oneEth); - const loan_id = await prepare_liquidation( - lender, - borrower, - liquidator, - loan_token_sent, - loanToken, - SUSD, // underlyingToken - RBTC, // collateralToken - sovryn, - WRBTC - ); - - // Try to set a delegated manager by other than borrower - await expectRevert(sovryn.setDelegatedManager("0x0", accounts[3], true, { from: accounts[3] }), "unauthorized"); - }); - - /// @dev the revert "loanParams not exists" is not achievable - /// because the previous check of loanLocal.active - /// is going to block it. - it("Should fail liquidating an inexistent loan", async () => { - // Try to liquidate an inexistent loan - const liquidator = accounts[2]; - const loan_token_sent = new BN(10).mul(oneEth); - let fakeLoan_id = "0x7af58ba7b104005f8e95f09abbbed011dab7e97dcfc9a353ce37948c7c320b45"; - let value = 0; - await expectRevert( - sovryn.liquidate(fakeLoan_id, liquidator, loan_token_sent, { from: liquidator, value: value }), - "loan is closed" - ); - }); - }); + it("Should fail liquidating a healthy position", async () => { + await liquidate_healthy_position_should_fail( + accounts, + loanToken, + SUSD, + set_demand_curve, + RBTC, + sovryn, + priceFeeds, + WRBTC + ); + }); + + it("Should fail liquidating a closed loan", async () => { + // Close the loan + await set_demand_curve(loanToken); + const lender = accounts[0]; + const borrower = accounts[1]; + const receiver = borrower; + const liquidator = accounts[2]; + const loan_token_sent = new BN(10).mul(oneEth); + const loan_id = await prepare_liquidation( + lender, + borrower, + liquidator, + loan_token_sent, + loanToken, + SUSD, // underlyingToken + RBTC, // collateralToken + sovryn, + WRBTC + ); + /// @dev Only way to close the loan into an inactive state is + /// by depositing the exact amount of the principal. + let deposit_amount = new BN("20267418874797325811"); // loanLocal.principal + await SUSD.mint(borrower, deposit_amount); + await SUSD.approve(sovryn.address, deposit_amount, { from: borrower }); + await sovryn.closeWithDeposit(loan_id, receiver, deposit_amount, { from: borrower }); + + // Try to liquidate an inactive loan + let value = 0; + await expectRevert( + sovryn.liquidate(loan_id, liquidator, loan_token_sent, { + from: liquidator, + value: value, + }), + "loan is closed" + ); + }); + + it("Should work liquidating an unhealthy loan", async () => { + // Close the loan + await set_demand_curve(loanToken); + const lender = accounts[0]; + const borrower = accounts[1]; + const receiver = borrower; + const liquidator = accounts[2]; + const loan_token_sent = new BN(10).mul(oneEth); + const loan_id = await prepare_liquidation( + lender, + borrower, + liquidator, + loan_token_sent, + loanToken, + SUSD, // underlyingToken + RBTC, // collateralToken + sovryn, + WRBTC + ); + + // Ad hoc rate for unhealthy loan + const rate = new BN(10).pow(new BN(20)).mul(new BN(72)); + await priceFeeds.setRates(RBTC.address, SUSD.address, rate); + + // Liquidate an unhealthy loan + let value = 0; + await sovryn.liquidate(loan_id, liquidator, loan_token_sent, { + from: liquidator, + value: value, + }); + }); + + it("Should revert liquidating w/ wrong asset", async () => { + // Close the loan + await set_demand_curve(loanToken); + const lender = accounts[0]; + const borrower = accounts[1]; + const receiver = borrower; + const liquidator = accounts[2]; + const loan_token_sent = new BN(10).mul(oneEth); + const loan_id = await prepare_liquidation( + lender, + borrower, + liquidator, + loan_token_sent, + loanToken, + SUSD, // underlyingToken + RBTC, // collateralToken + sovryn, + WRBTC + ); + + // Ad hoc rate for unhealthy loan + const rate = new BN(10).pow(new BN(20)).mul(new BN(72)); + await priceFeeds.setRates(RBTC.address, SUSD.address, rate); + + // Try to liquidate by sending Ether + let value = 10; + await expectRevert( + sovryn.liquidate(loan_id, liquidator, loan_token_sent, { + from: liquidator, + value: value, + }), + "wrong asset sent" + ); + }); + + it("Should fail rolling over a closed loan", async () => { + // Close the loan + await set_demand_curve(loanToken); + const lender = accounts[0]; + const borrower = accounts[1]; + const receiver = borrower; + const liquidator = accounts[2]; + const loan_token_sent = new BN(10).mul(oneEth); + const loan_id = await prepare_liquidation( + lender, + borrower, + liquidator, + loan_token_sent, + loanToken, + SUSD, // underlyingToken + RBTC, // collateralToken + sovryn, + WRBTC + ); + /// @dev Only way to close the loan into an inactive state is + /// by depositing the exact amount of the principal. + let deposit_amount = new BN("20267418874797325811"); // loanLocal.principal + await SUSD.mint(borrower, deposit_amount); + await SUSD.approve(sovryn.address, deposit_amount, { from: borrower }); + await sovryn.closeWithDeposit(loan_id, receiver, deposit_amount, { from: borrower }); + + // Try to liquidate an inactive loan + let value = 0; + await expectRevert( + sovryn.rollover(loan_id, "0x", { from: liquidator, value: value }), + "loan is closed" + ); + }); + + it("Should fail rolling over a healthy position", async () => { + // Prepare the loan + await set_demand_curve(loanToken); + const lender = accounts[0]; + const borrower = accounts[1]; + const receiver = borrower; + const liquidator = accounts[2]; + const loan_token_sent = new BN(10).mul(oneEth); + const loan_id = await prepare_liquidation( + lender, + borrower, + liquidator, + loan_token_sent, + loanToken, + SUSD, // underlyingToken + RBTC, // collateralToken + sovryn, + WRBTC + ); + + // Try to roll over a healthy loan + let value = 0; + await expectRevert( + sovryn.rollover(loan_id, "0x", { from: liquidator, value: value }), + "healthy position" + ); + }); + + it("Should work rolling over an unhealthy position", async () => { + // Prepare the loan + await set_demand_curve(loanToken); + const lender = accounts[0]; + const borrower = accounts[1]; + const receiver = borrower; + const liquidator = accounts[2]; + const loan_token_sent = new BN(10).mul(oneEth); + const loan_id = await prepare_liquidation( + lender, + borrower, + liquidator, + loan_token_sent, + loanToken, + SUSD, // underlyingToken + RBTC, // collateralToken + sovryn, + WRBTC + ); + + // time travel 100 days turns position into an unhealthy state + await increaseTime(8640000); + + // Roll over an unhealthy loan + let value = 0; + await sovryn.rollover(loan_id, "0x", { from: liquidator, value: value }); + }); + + it("should work when setting a delegated manager", async () => { + // Prepare the loan + await set_demand_curve(loanToken); + const lender = accounts[0]; + const borrower = accounts[1]; + const receiver = borrower; + const liquidator = accounts[2]; + const loan_token_sent = new BN(10).mul(oneEth); + const loan_id = await prepare_liquidation( + lender, + borrower, + liquidator, + loan_token_sent, + loanToken, + SUSD, // underlyingToken + RBTC, // collateralToken + sovryn, + WRBTC + ); + + // Set a delegated manager + await sovryn.setDelegatedManager(loan_id, borrower, true, { from: borrower }); + }); + + it("should revert when setting a delegated manager by other than borrower", async () => { + // Prepare the loan + await set_demand_curve(loanToken); + const lender = accounts[0]; + const borrower = accounts[1]; + const receiver = borrower; + const liquidator = accounts[2]; + const loan_token_sent = new BN(10).mul(oneEth); + const loan_id = await prepare_liquidation( + lender, + borrower, + liquidator, + loan_token_sent, + loanToken, + SUSD, // underlyingToken + RBTC, // collateralToken + sovryn, + WRBTC + ); + + // Try to set a delegated manager by other than borrower + await expectRevert( + sovryn.setDelegatedManager("0x0", accounts[3], true, { from: accounts[3] }), + "unauthorized" + ); + }); + + /// @dev the revert "loanParams not exists" is not achievable + /// because the previous check of loanLocal.active + /// is going to block it. + it("Should fail liquidating an inexistent loan", async () => { + // Try to liquidate an inexistent loan + const liquidator = accounts[2]; + const loan_token_sent = new BN(10).mul(oneEth); + let fakeLoan_id = "0x7af58ba7b104005f8e95f09abbbed011dab7e97dcfc9a353ce37948c7c320b45"; + let value = 0; + await expectRevert( + sovryn.liquidate(fakeLoan_id, liquidator, loan_token_sent, { + from: liquidator, + value: value, + }), + "loan is closed" + ); + }); + }); }); diff --git a/tests/protocol/LiquidationwRBTCAsLoanToken.test.js b/tests/protocol/LiquidationwRBTCAsLoanToken.test.js index b6c9654ce..3feff7a7e 100644 --- a/tests/protocol/LiquidationwRBTCAsLoanToken.test.js +++ b/tests/protocol/LiquidationwRBTCAsLoanToken.test.js @@ -15,17 +15,17 @@ const { loadFixture } = waffle; const FeesEvents = artifacts.require("FeesEvents"); const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getLoanToken, - getLoanTokenWRBTC, - loan_pool_setup, - set_demand_curve, - getPriceFeeds, - getSovryn, - getSOV, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getLoanToken, + getLoanTokenWRBTC, + loan_pool_setup, + set_demand_curve, + getPriceFeeds, + getSovryn, + getSOV, } = require("../Utils/initializer.js"); const { liquidate, liquidate_healthy_position_should_fail } = require("./liquidationFunctions"); @@ -37,65 +37,89 @@ Should test the liquidation handling */ contract("ProtocolLiquidationTestToken", (accounts) => { - let owner; - let sovryn, SUSD, WRBTC, RBTC, BZRX, loanToken, loanTokenWRBTC, priceFeeds, SOV; + let owner; + let sovryn, SUSD, WRBTC, RBTC, BZRX, loanToken, loanTokenWRBTC, priceFeeds, SOV; - async function deploymentAndInitFixture(_wallets, _provider) { - // Deploying sovrynProtocol w/ generic function from initializer.js - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + async function deploymentAndInitFixture(_wallets, _provider) { + // Deploying sovrynProtocol w/ generic function from initializer.js + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - loanToken = await getLoanToken(owner, sovryn, WRBTC, SUSD); - loanTokenWRBTC = await getLoanTokenWRBTC(owner, sovryn, WRBTC, SUSD); - await loan_pool_setup(sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC); + loanToken = await getLoanToken(owner, sovryn, WRBTC, SUSD); + loanTokenWRBTC = await getLoanTokenWRBTC(owner, sovryn, WRBTC, SUSD); + await loan_pool_setup(sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC); - /// @dev SOV test token deployment w/ initializer.js - SOV = await getSOV(sovryn, priceFeeds, SUSD, accounts); - } + /// @dev SOV test token deployment w/ initializer.js + SOV = await getSOV(sovryn, priceFeeds, SUSD, accounts); + } - before(async () => { - [owner] = accounts; - }); + before(async () => { + [owner] = accounts; + }); - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); - describe("Tests liquidation handling ", () => { - /* + describe("Tests liquidation handling ", () => { + /* Test with different rates so the currentMargin is <= liquidationIncentivePercent or > liquidationIncentivePercent liquidationIncentivePercent = 5e18 by default */ - it("Test liquidate with rate 1e23", async () => { - const rate = new BN(10).pow(new BN(23)); - await liquidate(accounts, loanTokenWRBTC, WRBTC, set_demand_curve, SUSD, sovryn, priceFeeds, rate, WRBTC, FeesEvents, SOV); - }); + it("Test liquidate with rate 1e23", async () => { + const rate = new BN(10).pow(new BN(23)); + await liquidate( + accounts, + loanTokenWRBTC, + WRBTC, + set_demand_curve, + SUSD, + sovryn, + priceFeeds, + rate, + WRBTC, + FeesEvents, + SOV + ); + }); - it("Test liquidate with rate 1.34e22", async () => { - const rate = new BN(134).mul(new BN(10).pow(new BN(20))); - await liquidate(accounts, loanTokenWRBTC, WRBTC, set_demand_curve, SUSD, sovryn, priceFeeds, rate, WRBTC, FeesEvents, SOV); - }); + it("Test liquidate with rate 1.34e22", async () => { + const rate = new BN(134).mul(new BN(10).pow(new BN(20))); + await liquidate( + accounts, + loanTokenWRBTC, + WRBTC, + set_demand_curve, + SUSD, + sovryn, + priceFeeds, + rate, + WRBTC, + FeesEvents, + SOV + ); + }); - /* + /* Test if fails when the position is healthy currentMargin > maintenanceRate */ - it("Test liquidate healthy position should fail", async () => { - await liquidate_healthy_position_should_fail( - accounts, - loanTokenWRBTC, - WRBTC, - set_demand_curve, - SUSD, - sovryn, - priceFeeds, - WRBTC - ); - }); - }); + it("Test liquidate healthy position should fail", async () => { + await liquidate_healthy_position_should_fail( + accounts, + loanTokenWRBTC, + WRBTC, + set_demand_curve, + SUSD, + sovryn, + priceFeeds, + WRBTC + ); + }); + }); }); diff --git a/tests/protocol/RolloverTestToken.test.js b/tests/protocol/RolloverTestToken.test.js index db7992620..86f4cd5b1 100644 --- a/tests/protocol/RolloverTestToken.test.js +++ b/tests/protocol/RolloverTestToken.test.js @@ -21,19 +21,19 @@ const { increaseTime, blockNumber } = require("../Utils/Ethereum"); const LoanClosingsEvents = artifacts.require("LoanClosingsEvents"); const SwapEvents = artifacts.require("SwapsEvents"); const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getLoanToken, - getLoanTokenWRBTC, - loan_pool_setup, - set_demand_curve, - getPriceFeeds, - getSovryn, - decodeLogs, - getSOV, - verify_sov_reward_payment, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getLoanToken, + getLoanTokenWRBTC, + loan_pool_setup, + set_demand_curve, + getPriceFeeds, + getSovryn, + decodeLogs, + getSOV, + verify_sov_reward_payment, } = require("../Utils/initializer.js"); const LockedSOVMockup = artifacts.require("LockedSOVMockup"); @@ -54,462 +54,592 @@ Note: close with swap is tested in loanToken/trading */ contract("ProtocolCloseDeposit", (accounts) => { - let owner; - let sovryn, SUSD, WRBTC, RBTC, BZRX, loanToken, loanTokenWRBTC, priceFeeds, SOV; - - async function deploymentAndInitFixture(_wallets, _provider) { - // Deploying sovrynProtocol w/ generic function from initializer.js - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - - loanToken = await getLoanToken(owner, sovryn, WRBTC, SUSD); - loanTokenWRBTC = await getLoanTokenWRBTC(owner, sovryn, WRBTC, SUSD); - await loan_pool_setup(sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC); - - /// @dev SOV test token deployment w/ initializer.js - SOV = await getSOV(sovryn, priceFeeds, SUSD, accounts); - } - - before(async () => { - [owner] = accounts; - }); - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - const setup_rollover_test = async (RBTC, SUSD, accounts, loanToken, loan_token_sent, set_demand_curve, sovryn) => { - await set_demand_curve(loanToken); - await SUSD.approve(loanToken.address, new BN(10).pow(new BN(40))); - const lender = accounts[0]; - const borrower = accounts[1]; - await loanToken.mint(lender, new BN(10).pow(new BN(30))); - await SUSD.mint(borrower, loan_token_sent); - await SUSD.approve(loanToken.address, loan_token_sent, { from: borrower }); - const { receipt } = await loanToken.marginTrade( - "0x0", // loanId (0 for new loans) - new BN(2).mul(oneEth), // leverageAmount - loan_token_sent, // loanTokenSent - 0, // no collateral token sent - RBTC.address, // collateralTokenAddress - borrower, // trader, - 0, - "0x", // loanDataBytes (only required with ether) - { from: borrower } - ); - - const decode = decodeLogs(receipt.rawLogs, LoanOpeningsEvents, "Trade"); - const loan_id = decode[0].args["loanId"]; - const loan = await sovryn.getLoan(loan_id); - const num = await blockNumber(); - let currentBlock = await web3.eth.getBlock(num); - const block_timestamp = currentBlock.timestamp; - const time_until_loan_end = loan["endTimestamp"] - block_timestamp; - await increaseTime(time_until_loan_end); - return [borrower, loan, loan_id, parseInt(loan["endTimestamp"])]; - }; - - describe("Tests the close with deposit. ", () => { - /* + let owner; + let sovryn, SUSD, WRBTC, RBTC, BZRX, loanToken, loanTokenWRBTC, priceFeeds, SOV; + + async function deploymentAndInitFixture(_wallets, _provider) { + // Deploying sovrynProtocol w/ generic function from initializer.js + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + + loanToken = await getLoanToken(owner, sovryn, WRBTC, SUSD); + loanTokenWRBTC = await getLoanTokenWRBTC(owner, sovryn, WRBTC, SUSD); + await loan_pool_setup(sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC); + + /// @dev SOV test token deployment w/ initializer.js + SOV = await getSOV(sovryn, priceFeeds, SUSD, accounts); + } + + before(async () => { + [owner] = accounts; + }); + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + const setup_rollover_test = async ( + RBTC, + SUSD, + accounts, + loanToken, + loan_token_sent, + set_demand_curve, + sovryn + ) => { + await set_demand_curve(loanToken); + await SUSD.approve(loanToken.address, new BN(10).pow(new BN(40))); + const lender = accounts[0]; + const borrower = accounts[1]; + await loanToken.mint(lender, new BN(10).pow(new BN(30))); + await SUSD.mint(borrower, loan_token_sent); + await SUSD.approve(loanToken.address, loan_token_sent, { from: borrower }); + const { receipt } = await loanToken.marginTrade( + "0x0", // loanId (0 for new loans) + new BN(2).mul(oneEth), // leverageAmount + loan_token_sent, // loanTokenSent + 0, // no collateral token sent + RBTC.address, // collateralTokenAddress + borrower, // trader, + 0, + "0x", // loanDataBytes (only required with ether) + { from: borrower } + ); + + const decode = decodeLogs(receipt.rawLogs, LoanOpeningsEvents, "Trade"); + const loan_id = decode[0].args["loanId"]; + const loan = await sovryn.getLoan(loan_id); + const num = await blockNumber(); + let currentBlock = await web3.eth.getBlock(num); + const block_timestamp = currentBlock.timestamp; + const time_until_loan_end = loan["endTimestamp"] - block_timestamp; + await increaseTime(time_until_loan_end); + return [borrower, loan, loan_id, parseInt(loan["endTimestamp"])]; + }; + + describe("Tests the close with deposit. ", () => { + /* Tests paid interests to lender Test that loan attributes are updated Test loan swap event */ - it("Test rollover", async () => { - // prepare the test - - const [borrower, loan, loan_id, endTimestamp] = await setup_rollover_test( - RBTC, - SUSD, - accounts, - loanToken, - hunEth, // loan_token_sent - set_demand_curve, - sovryn - ); - - const lender_interest_data = await sovryn.getLenderInterestData(loanToken.address, SUSD.address); - - const lender_pool_initial_balance = await SUSD.balanceOf(loanToken.address); - - lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); - const sov_borrower_initial_balance = (await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower)); - - const { receipt } = await sovryn.rollover(loan_id, "0x"); - const susd_bal_after_rollover = (await SUSD.balanceOf(loanToken.address)).toString(); - - const lender_interest_after = await sovryn.getLenderInterestData(loanToken.address, SUSD.address); - - const lending_fee_percent = await sovryn.lendingFeePercent(); - const interest_unpaid = new BN(lender_interest_data["interestUnPaid"]); - const lending_fee = interest_unpaid.mul(lending_fee_percent).div(hunEth); - let interest_owed_now = interest_unpaid.sub(lending_fee); - - num = await blockNumber(); - currentBlock = await web3.eth.getBlock(num); - block_timestamp = currentBlock.timestamp; - if (block_timestamp > endTimestamp) { - backInterestTime = new BN(block_timestamp - endTimestamp); - backInterestOwed = backInterestTime.mul(lender_interest_data["interestOwedPerDay"]).div(new BN(24 * 60 * 60)); - const lending_fee = backInterestOwed.mul(lending_fee_percent).div(hunEth); - backInterestOwed = backInterestOwed.sub(lending_fee); - interest_owed_now = interest_owed_now.add(backInterestOwed); - } - - expect(await SUSD.balanceOf(loanToken.address)).to.be.bignumber.equal(lender_pool_initial_balance.add(interest_owed_now)); - expect(lender_interest_after["interestPaid"] == interest_unpaid.toString()).to.be.true; - expect(lender_interest_after["interestUnPaid"] == "0").to.be.true; - - // Settles and pays borrowers based on fees generated by their interest payments - if ((await sovryn.protocolTokenHeld()) != 0) - await verify_sov_reward_payment( - receipt.rawLogs, - FeesEvents, - SOV, - borrower, - loan_id, - sov_borrower_initial_balance, - 2, - SUSD.address, - RBTC.address, - sovryn - ); - - const loan_after = await sovryn.getLoan(loan_id); - expect(loan_after["endTimestamp"] >= parseInt(loan["endTimestamp"]) + 28 * 24 * 60 * 60).to.be.true; - const { rate: trade_rate, precision } = await priceFeeds.queryRate(RBTC.address, SUSD.address); - const trading_fee_percent = await sovryn.tradingFeePercent(); - const trading_fee = interest_unpaid.mul(trading_fee_percent).div(hunEth); - - const decode = decodeLogs(receipt.rawLogs, SwapsEvents, "LoanSwap"); - const loan_swap_event = decode[0].args; - expect(loan_swap_event["loanId"] == loan_id).to.be.true; - expect(loan_swap_event["sourceToken"] == RBTC.address).to.be.true; - expect(loan_swap_event["destToken"] == SUSD.address).to.be.true; - expect(loan_swap_event["borrower"] == borrower).to.be.true; - // source buffer = 10000 in sovryn swap connector - expect( - new BN(loan_swap_event["sourceAmount"]) - .sub(interest_unpaid) - .add(trading_fee) - .mul(precision) - .div(trade_rate) - .lte(new BN(10000)) - ).to.be.true; - expect(new BN(loan_swap_event["destAmount"]).gte(interest_unpaid)).to.be.true; - - const decoded_rollover = decodeLogs(receipt.rawLogs, LoanClosingsEvents, "Rollover"); - const rollover_event = decoded_rollover[0].args; - expect(rollover_event["user"]).to.equal(borrower); - expect(rollover_event["lender"]).to.equal(loanToken.address); - expect(rollover_event["loanId"]).to.equal(loan_id); - expect(rollover_event["principal"]).to.equal(loan_after["principal"]); - expect(rollover_event["collateral"]).to.equal(loan_after["collateral"]); - expect(rollover_event["endTimestamp"]).to.equal(loan_after["endTimestamp"]); - expect(rollover_event["rewardReceiver"]).to.equal(accounts[0]); - expect(new BN(rollover_event["reward"]) > new BN(0)).to.be.true; - }); - - it("Test rollover tiny amount", async () => { - // prepare the test - let rollover_wallet = accounts[5]; - loan_token_sent = TINY_AMOUNT.add(new BN(1)).mul(new BN(10).pow(new BN(4))); - const [borrower, loan, loan_id, endTimestamp] = await setup_rollover_test( - RBTC, - SUSD, - accounts, - loanToken, - loan_token_sent, - set_demand_curve, - sovryn - ); - - const num = await blockNumber(); - let currentBlock = await web3.eth.getBlock(num); - const block_timestamp = currentBlock.timestamp; - const time_until_loan_end = loan["endTimestamp"] - block_timestamp; - await increaseTime(time_until_loan_end); - - loan_before_rolled_over = await sovryn.getLoan.call(loan_id); - - // // Set the wrbtc price become more expensive, so that it can create the dust - await priceFeeds.setRates(WRBTC.address, SUSD.address, new BN(10).pow(new BN(23)).toString()); - - const previousBorrowerBalanceSUSD = await SUSD.balanceOf(borrower); - const previousBorrowerBalanceRBTC = await RBTC.balanceOf(borrower); - const previousRolloverWalletBalanceSUSD = await SUSD.balanceOf(rollover_wallet); - const previousRolloverWalletBalanceRBTC = await RBTC.balanceOf(rollover_wallet); - const { receipt } = await sovryn.rollover(loan_id, "0x", { from: rollover_wallet }); - const latestBorrowerBalanceSUSD = await SUSD.balanceOf(borrower); - const latestBorrowerBalanceRBTC = await RBTC.balanceOf(borrower); - const latestRolloverWalletBalanceSUSD = await SUSD.balanceOf(rollover_wallet); - const latestRolloverWalletBalanceRBTC = await RBTC.balanceOf(rollover_wallet); - - const vaultWithdrawEvents = decodeLogs(receipt.rawLogs, VaultController, "VaultWithdraw"); - - // Make sure the borrower got the remaining collateral - const remainingCollateralAmount = vaultWithdrawEvents[vaultWithdrawEvents.length - 1].args["amount"]; - expect(latestBorrowerBalanceSUSD.toString()).to.equal( - previousBorrowerBalanceSUSD.add(new BN(remainingCollateralAmount)).toString() - ); - - expect(previousBorrowerBalanceRBTC.toString()).to.equal(new BN(0).toString()); - expect(latestBorrowerBalanceRBTC.toString()).to.equal(new BN(0).toString()); - - // Rollover wallet should get the reward - const rollover_reward = vaultWithdrawEvents[2].args["amount"]; - expect(previousRolloverWalletBalanceSUSD.toString()).to.equal(new BN(0).toString()); - expect(previousRolloverWalletBalanceRBTC.toString()).to.equal(new BN(0).toString()); - expect(latestRolloverWalletBalanceSUSD.toString()).to.equal(new BN(0).toString()); - expect(latestRolloverWalletBalanceRBTC.toString()).to.equal( - previousRolloverWalletBalanceRBTC.add(new BN(rollover_reward)).toString() - ); - - // Test loan update - end_loan = await sovryn.getLoan.call(loan_id); - // CHECK THE POSITION / LOAN IS COMPLETELY CLOSED - expect(end_loan["principal"]).to.be.bignumber.equal(new BN(0).toString(), "principal should be 0"); - expect(end_loan["collateral"]).to.be.bignumber.equal(new BN(0).toString(), "collateral should be 0"); - expect(end_loan["currentMargin"]).to.be.bignumber.equal(new BN(0).toString(), "current margin should be 0"); - expect(end_loan["maxLiquidatable"]).to.be.bignumber.equal(new BN(0).toString(), "current margin should be 0"); - expect(end_loan["maxSeizable"]).to.be.bignumber.equal(new BN(0).toString(), "current margin should be 0"); - expect(end_loan["interestOwedPerDay"]).to.be.bignumber.equal(new BN(0).toString(), "current margin should be 0"); - expect(end_loan["interestDepositRemaining"]).to.be.bignumber.equal(new BN(0).toString(), "current margin should be 0"); - - // CHECK THE CLOSE SWAP IS WORKING PROPERLY - const decode = decodeLogs(receipt.rawLogs, LoanClosingsEvents, "CloseWithSwap"); - const swapEvent = decode[0].args; - expect(swapEvent["user"]).to.equal(borrower); - expect(swapEvent["lender"]).to.equal(loanToken.address); // lender is the pool in this case - expect(swapEvent["loanId"]).to.equal(loan_id); - expect(swapEvent["closer"]).to.equal(rollover_wallet); // the one who called the rollover function - expect(swapEvent["loanToken"]).to.equal(SUSD.address); /// Don't get confused, the loanToken is not the pool token, it's the underlying token - expect(swapEvent["collateralToken"]).to.equal(RBTC.address); - expect(swapEvent["loanCloseAmount"]).to.equal(loan_before_rolled_over["principal"]); // the principal before rolled over - - const decoded_rollover = decodeLogs(receipt.rawLogs, LoanClosingsEvents, "Rollover"); - const rollover_event = decoded_rollover; - expect(rollover_event.length == 0).to.be.true; - }); - - it("Test rollover where the rollover reward greater than collateral itself", async () => { - // prepare the test - let rollover_wallet = accounts[5]; - loan_token_sent = TINY_AMOUNT.add(new BN(1)).mul(new BN(10).pow(new BN(4))); - const [borrower, loan, loan_id, endTimestamp] = await setup_rollover_test( - RBTC, - SUSD, - accounts, - loanToken, - loan_token_sent, - set_demand_curve, - sovryn - ); - - const num = await blockNumber(); - let currentBlock = await web3.eth.getBlock(num); - const block_timestamp = currentBlock.timestamp; - const time_until_loan_end = loan["endTimestamp"] - block_timestamp; - await increaseTime(time_until_loan_end); - - loan_before_rolled_over = await sovryn.getLoan.call(loan_id); - - // // Set the rbtc (collateral) price become more expensive, so that the reward will be greater than the collateral itself - await priceFeeds.setRates(WRBTC.address, RBTC.address, new BN(10).pow(new BN(19)).mul(new BN(3)).toString()); - - const previousRolloverWalletBalanceSUSD = await SUSD.balanceOf(rollover_wallet); - const previousRolloverWalletBalanceWRBTC = await WRBTC.balanceOf(rollover_wallet); - const { receipt } = await sovryn.rollover(loan_id, "0x", { from: rollover_wallet }); - const latestRolloverWalletBalanceSUSD = await SUSD.balanceOf(rollover_wallet); - const latestRolloverWalletBalanceWRBTC = await WRBTC.balanceOf(rollover_wallet); - - const vaultWithdrawEvents = decodeLogs(receipt.rawLogs, VaultController, "VaultWithdraw"); - - // Make sure the rollover wallet get the remaining collateral in form of underlying loan token - const remainingCollateralAmount = vaultWithdrawEvents[vaultWithdrawEvents.length - 1].args["amount"]; - expect(latestRolloverWalletBalanceSUSD.toString()).to.equal( - previousRolloverWalletBalanceSUSD.add(new BN(remainingCollateralAmount)).toString() - ); - - expect(previousRolloverWalletBalanceWRBTC.toString()).to.equal(new BN(0).toString()); - expect(latestRolloverWalletBalanceWRBTC.toString()).to.equal(new BN(0).toString()); - - // Test loan update - end_loan = await sovryn.getLoan.call(loan_id); - - // CHECK THE POSITION / LOAN IS COMPLETELY CLOSED - expect(end_loan["principal"]).to.be.bignumber.equal(new BN(0).toString(), "principal should be 0"); - expect(end_loan["collateral"]).to.be.bignumber.equal(new BN(0).toString(), "collateral should be 0"); - expect(end_loan["currentMargin"]).to.be.bignumber.equal(new BN(0).toString(), "current margin should be 0"); - expect(end_loan["maxLiquidatable"]).to.be.bignumber.equal(new BN(0).toString(), "current margin should be 0"); - expect(end_loan["maxSeizable"]).to.be.bignumber.equal(new BN(0).toString(), "current margin should be 0"); - expect(end_loan["interestOwedPerDay"]).to.be.bignumber.equal(new BN(0).toString(), "current margin should be 0"); - expect(end_loan["interestDepositRemaining"]).to.be.bignumber.equal(new BN(0).toString(), "current margin should be 0"); - - // CHECK THE CLOSE SWAP IS WORKING PROPERLY - const decode = decodeLogs(receipt.rawLogs, LoanClosingsEvents, "CloseWithSwap"); - const swapEvent = decode[0].args; - - const loanSwapEvent = decodeLogs(receipt.rawLogs, SwapEvents, "LoanSwap"); - const sourceTokenAmountUsed = new BN(loanSwapEvent[0].args["sourceAmount"]); - - expect(swapEvent["user"]).to.equal(borrower); - expect(swapEvent["lender"]).to.equal(loanToken.address); // lender is the pool in this case - expect(swapEvent["loanId"]).to.equal(loan_id); - expect(swapEvent["closer"]).to.equal(rollover_wallet); // the one who called the rollover function - expect(swapEvent["loanToken"]).to.equal(SUSD.address); /// Don't get confused, the loanToken is not the pool token, it's the underlying token - expect(swapEvent["collateralToken"]).to.equal(RBTC.address); - expect(swapEvent["loanCloseAmount"]).to.equal(loan_before_rolled_over["principal"]); // the principal before rolled over - expect(swapEvent["positionCloseSize"]).to.equal( - new BN(loan_before_rolled_over["collateral"]).sub(sourceTokenAmountUsed).toString() - ); // the principal before rolled over - }); - - it("Test rollover with special rebates", async () => { - // prepare the test - await sovryn.setSpecialRebates(SUSD.address, RBTC.address, wei("300", "ether")); - const [borrower, loan, loan_id, endTimestamp] = await setup_rollover_test( - RBTC, - SUSD, - accounts, - loanToken, - hunEth, // loan_token_sent - set_demand_curve, - sovryn - ); - - const lender_interest_data = await sovryn.getLenderInterestData(loanToken.address, SUSD.address); - - const lender_pool_initial_balance = await SUSD.balanceOf(loanToken.address); - - lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); - const sov_borrower_initial_balance = (await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower)); - - const { receipt } = await sovryn.rollover(loan_id, "0x"); - const susd_bal_after_rollover = (await SUSD.balanceOf(loanToken.address)).toString(); - - const lender_interest_after = await sovryn.getLenderInterestData(loanToken.address, SUSD.address); - - const lending_fee_percent = await sovryn.lendingFeePercent(); - const interest_unpaid = new BN(lender_interest_data["interestUnPaid"]); - const lending_fee = interest_unpaid.mul(lending_fee_percent).div(hunEth); - let interest_owed_now = interest_unpaid.sub(lending_fee); - - num = await blockNumber(); - currentBlock = await web3.eth.getBlock(num); - block_timestamp = currentBlock.timestamp; - if (block_timestamp > endTimestamp) { - backInterestTime = new BN(block_timestamp - endTimestamp); - backInterestOwed = backInterestTime.mul(lender_interest_data["interestOwedPerDay"]).div(new BN(24 * 60 * 60)); - const lending_fee = backInterestOwed.mul(lending_fee_percent).div(hunEth); - backInterestOwed = backInterestOwed.sub(lending_fee); - interest_owed_now = interest_owed_now.add(backInterestOwed); - } - - expect(await SUSD.balanceOf(loanToken.address)).to.be.bignumber.equal(lender_pool_initial_balance.add(interest_owed_now)); - expect(lender_interest_after["interestPaid"] == interest_unpaid.toString()).to.be.true; - expect(lender_interest_after["interestUnPaid"] == "0").to.be.true; - - // Settles and pays borrowers based on fees generated by their interest payments - if ((await sovryn.protocolTokenHeld()) != 0) - await verify_sov_reward_payment( - receipt.rawLogs, - FeesEvents, - SOV, - borrower, - loan_id, - sov_borrower_initial_balance, - 2, - SUSD.address, - RBTC.address, - sovryn - ); - - const loan_after = await sovryn.getLoan(loan_id); - expect(loan_after["endTimestamp"] >= parseInt(loan["endTimestamp"]) + 28 * 24 * 60 * 60).to.be.true; - const { rate: trade_rate, precision } = await priceFeeds.queryRate(RBTC.address, SUSD.address); - const trading_fee_percent = await sovryn.tradingFeePercent(); - const trading_fee = interest_unpaid.mul(trading_fee_percent).div(hunEth); - - const decode = decodeLogs(receipt.rawLogs, SwapsEvents, "LoanSwap"); - const loan_swap_event = decode[0].args; - expect(loan_swap_event["loanId"] == loan_id).to.be.true; - expect(loan_swap_event["sourceToken"] == RBTC.address).to.be.true; - expect(loan_swap_event["destToken"] == SUSD.address).to.be.true; - expect(loan_swap_event["borrower"] == borrower).to.be.true; - // source buffer = 10000 in sovryn swap connector - expect( - new BN(loan_swap_event["sourceAmount"]) - .sub(interest_unpaid) - .add(trading_fee) - .mul(precision) - .div(trade_rate) - .lte(new BN(10000)) - ).to.be.true; - expect(new BN(loan_swap_event["destAmount"]).gte(interest_unpaid)).to.be.true; - }); - - /* + it("Test rollover", async () => { + // prepare the test + + const [borrower, loan, loan_id, endTimestamp] = await setup_rollover_test( + RBTC, + SUSD, + accounts, + loanToken, + hunEth, // loan_token_sent + set_demand_curve, + sovryn + ); + + const lender_interest_data = await sovryn.getLenderInterestData( + loanToken.address, + SUSD.address + ); + + const lender_pool_initial_balance = await SUSD.balanceOf(loanToken.address); + + lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); + const sov_borrower_initial_balance = (await SOV.balanceOf(borrower)).add( + await lockedSOV.getLockedBalance(borrower) + ); + + const { receipt } = await sovryn.rollover(loan_id, "0x"); + const susd_bal_after_rollover = (await SUSD.balanceOf(loanToken.address)).toString(); + + const lender_interest_after = await sovryn.getLenderInterestData( + loanToken.address, + SUSD.address + ); + + const lending_fee_percent = await sovryn.lendingFeePercent(); + const interest_unpaid = new BN(lender_interest_data["interestUnPaid"]); + const lending_fee = interest_unpaid.mul(lending_fee_percent).div(hunEth); + let interest_owed_now = interest_unpaid.sub(lending_fee); + + num = await blockNumber(); + currentBlock = await web3.eth.getBlock(num); + block_timestamp = currentBlock.timestamp; + if (block_timestamp > endTimestamp) { + backInterestTime = new BN(block_timestamp - endTimestamp); + backInterestOwed = backInterestTime + .mul(lender_interest_data["interestOwedPerDay"]) + .div(new BN(24 * 60 * 60)); + const lending_fee = backInterestOwed.mul(lending_fee_percent).div(hunEth); + backInterestOwed = backInterestOwed.sub(lending_fee); + interest_owed_now = interest_owed_now.add(backInterestOwed); + } + + expect(await SUSD.balanceOf(loanToken.address)).to.be.bignumber.equal( + lender_pool_initial_balance.add(interest_owed_now) + ); + expect(lender_interest_after["interestPaid"] == interest_unpaid.toString()).to.be.true; + expect(lender_interest_after["interestUnPaid"] == "0").to.be.true; + + // Settles and pays borrowers based on fees generated by their interest payments + if ((await sovryn.protocolTokenHeld()) != 0) + await verify_sov_reward_payment( + receipt.rawLogs, + FeesEvents, + SOV, + borrower, + loan_id, + sov_borrower_initial_balance, + 2, + SUSD.address, + RBTC.address, + sovryn + ); + + const loan_after = await sovryn.getLoan(loan_id); + expect( + loan_after["endTimestamp"] >= parseInt(loan["endTimestamp"]) + 28 * 24 * 60 * 60 + ).to.be.true; + const { rate: trade_rate, precision } = await priceFeeds.queryRate( + RBTC.address, + SUSD.address + ); + const trading_fee_percent = await sovryn.tradingFeePercent(); + const trading_fee = interest_unpaid.mul(trading_fee_percent).div(hunEth); + + const decode = decodeLogs(receipt.rawLogs, SwapsEvents, "LoanSwap"); + const loan_swap_event = decode[0].args; + expect(loan_swap_event["loanId"] == loan_id).to.be.true; + expect(loan_swap_event["sourceToken"] == RBTC.address).to.be.true; + expect(loan_swap_event["destToken"] == SUSD.address).to.be.true; + expect(loan_swap_event["borrower"] == borrower).to.be.true; + // source buffer = 10000 in sovryn swap connector + expect( + new BN(loan_swap_event["sourceAmount"]) + .sub(interest_unpaid) + .add(trading_fee) + .mul(precision) + .div(trade_rate) + .lte(new BN(10000)) + ).to.be.true; + expect(new BN(loan_swap_event["destAmount"]).gte(interest_unpaid)).to.be.true; + + const decoded_rollover = decodeLogs(receipt.rawLogs, LoanClosingsEvents, "Rollover"); + const rollover_event = decoded_rollover[0].args; + expect(rollover_event["user"]).to.equal(borrower); + expect(rollover_event["lender"]).to.equal(loanToken.address); + expect(rollover_event["loanId"]).to.equal(loan_id); + expect(rollover_event["principal"]).to.equal(loan_after["principal"]); + expect(rollover_event["collateral"]).to.equal(loan_after["collateral"]); + expect(rollover_event["endTimestamp"]).to.equal(loan_after["endTimestamp"]); + expect(rollover_event["rewardReceiver"]).to.equal(accounts[0]); + expect(new BN(rollover_event["reward"]) > new BN(0)).to.be.true; + }); + + it("Test rollover tiny amount", async () => { + // prepare the test + let rollover_wallet = accounts[5]; + loan_token_sent = TINY_AMOUNT.add(new BN(1)).mul(new BN(10).pow(new BN(4))); + const [borrower, loan, loan_id, endTimestamp] = await setup_rollover_test( + RBTC, + SUSD, + accounts, + loanToken, + loan_token_sent, + set_demand_curve, + sovryn + ); + + const num = await blockNumber(); + let currentBlock = await web3.eth.getBlock(num); + const block_timestamp = currentBlock.timestamp; + const time_until_loan_end = loan["endTimestamp"] - block_timestamp; + await increaseTime(time_until_loan_end); + + loan_before_rolled_over = await sovryn.getLoan.call(loan_id); + + // // Set the wrbtc price become more expensive, so that it can create the dust + await priceFeeds.setRates( + WRBTC.address, + SUSD.address, + new BN(10).pow(new BN(23)).toString() + ); + + const previousBorrowerBalanceSUSD = await SUSD.balanceOf(borrower); + const previousBorrowerBalanceRBTC = await RBTC.balanceOf(borrower); + const previousRolloverWalletBalanceSUSD = await SUSD.balanceOf(rollover_wallet); + const previousRolloverWalletBalanceRBTC = await RBTC.balanceOf(rollover_wallet); + const { receipt } = await sovryn.rollover(loan_id, "0x", { from: rollover_wallet }); + const latestBorrowerBalanceSUSD = await SUSD.balanceOf(borrower); + const latestBorrowerBalanceRBTC = await RBTC.balanceOf(borrower); + const latestRolloverWalletBalanceSUSD = await SUSD.balanceOf(rollover_wallet); + const latestRolloverWalletBalanceRBTC = await RBTC.balanceOf(rollover_wallet); + + const vaultWithdrawEvents = decodeLogs( + receipt.rawLogs, + VaultController, + "VaultWithdraw" + ); + + // Make sure the borrower got the remaining collateral + const remainingCollateralAmount = + vaultWithdrawEvents[vaultWithdrawEvents.length - 1].args["amount"]; + expect(latestBorrowerBalanceSUSD.toString()).to.equal( + previousBorrowerBalanceSUSD.add(new BN(remainingCollateralAmount)).toString() + ); + + expect(previousBorrowerBalanceRBTC.toString()).to.equal(new BN(0).toString()); + expect(latestBorrowerBalanceRBTC.toString()).to.equal(new BN(0).toString()); + + // Rollover wallet should get the reward + const rollover_reward = vaultWithdrawEvents[2].args["amount"]; + expect(previousRolloverWalletBalanceSUSD.toString()).to.equal(new BN(0).toString()); + expect(previousRolloverWalletBalanceRBTC.toString()).to.equal(new BN(0).toString()); + expect(latestRolloverWalletBalanceSUSD.toString()).to.equal(new BN(0).toString()); + expect(latestRolloverWalletBalanceRBTC.toString()).to.equal( + previousRolloverWalletBalanceRBTC.add(new BN(rollover_reward)).toString() + ); + + // Test loan update + end_loan = await sovryn.getLoan.call(loan_id); + // CHECK THE POSITION / LOAN IS COMPLETELY CLOSED + expect(end_loan["principal"]).to.be.bignumber.equal( + new BN(0).toString(), + "principal should be 0" + ); + expect(end_loan["collateral"]).to.be.bignumber.equal( + new BN(0).toString(), + "collateral should be 0" + ); + expect(end_loan["currentMargin"]).to.be.bignumber.equal( + new BN(0).toString(), + "current margin should be 0" + ); + expect(end_loan["maxLiquidatable"]).to.be.bignumber.equal( + new BN(0).toString(), + "current margin should be 0" + ); + expect(end_loan["maxSeizable"]).to.be.bignumber.equal( + new BN(0).toString(), + "current margin should be 0" + ); + expect(end_loan["interestOwedPerDay"]).to.be.bignumber.equal( + new BN(0).toString(), + "current margin should be 0" + ); + expect(end_loan["interestDepositRemaining"]).to.be.bignumber.equal( + new BN(0).toString(), + "current margin should be 0" + ); + + // CHECK THE CLOSE SWAP IS WORKING PROPERLY + const decode = decodeLogs(receipt.rawLogs, LoanClosingsEvents, "CloseWithSwap"); + const swapEvent = decode[0].args; + expect(swapEvent["user"]).to.equal(borrower); + expect(swapEvent["lender"]).to.equal(loanToken.address); // lender is the pool in this case + expect(swapEvent["loanId"]).to.equal(loan_id); + expect(swapEvent["closer"]).to.equal(rollover_wallet); // the one who called the rollover function + expect(swapEvent["loanToken"]).to.equal(SUSD.address); /// Don't get confused, the loanToken is not the pool token, it's the underlying token + expect(swapEvent["collateralToken"]).to.equal(RBTC.address); + expect(swapEvent["loanCloseAmount"]).to.equal(loan_before_rolled_over["principal"]); // the principal before rolled over + + const decoded_rollover = decodeLogs(receipt.rawLogs, LoanClosingsEvents, "Rollover"); + const rollover_event = decoded_rollover; + expect(rollover_event.length == 0).to.be.true; + }); + + it("Test rollover where the rollover reward greater than collateral itself", async () => { + // prepare the test + let rollover_wallet = accounts[5]; + loan_token_sent = TINY_AMOUNT.add(new BN(1)).mul(new BN(10).pow(new BN(4))); + const [borrower, loan, loan_id, endTimestamp] = await setup_rollover_test( + RBTC, + SUSD, + accounts, + loanToken, + loan_token_sent, + set_demand_curve, + sovryn + ); + + const num = await blockNumber(); + let currentBlock = await web3.eth.getBlock(num); + const block_timestamp = currentBlock.timestamp; + const time_until_loan_end = loan["endTimestamp"] - block_timestamp; + await increaseTime(time_until_loan_end); + + loan_before_rolled_over = await sovryn.getLoan.call(loan_id); + + // // Set the rbtc (collateral) price become more expensive, so that the reward will be greater than the collateral itself + await priceFeeds.setRates( + WRBTC.address, + RBTC.address, + new BN(10).pow(new BN(19)).mul(new BN(3)).toString() + ); + + const previousRolloverWalletBalanceSUSD = await SUSD.balanceOf(rollover_wallet); + const previousRolloverWalletBalanceWRBTC = await WRBTC.balanceOf(rollover_wallet); + const { receipt } = await sovryn.rollover(loan_id, "0x", { from: rollover_wallet }); + const latestRolloverWalletBalanceSUSD = await SUSD.balanceOf(rollover_wallet); + const latestRolloverWalletBalanceWRBTC = await WRBTC.balanceOf(rollover_wallet); + + const vaultWithdrawEvents = decodeLogs( + receipt.rawLogs, + VaultController, + "VaultWithdraw" + ); + + // Make sure the rollover wallet get the remaining collateral in form of underlying loan token + const remainingCollateralAmount = + vaultWithdrawEvents[vaultWithdrawEvents.length - 1].args["amount"]; + expect(latestRolloverWalletBalanceSUSD.toString()).to.equal( + previousRolloverWalletBalanceSUSD.add(new BN(remainingCollateralAmount)).toString() + ); + + expect(previousRolloverWalletBalanceWRBTC.toString()).to.equal(new BN(0).toString()); + expect(latestRolloverWalletBalanceWRBTC.toString()).to.equal(new BN(0).toString()); + + // Test loan update + end_loan = await sovryn.getLoan.call(loan_id); + + // CHECK THE POSITION / LOAN IS COMPLETELY CLOSED + expect(end_loan["principal"]).to.be.bignumber.equal( + new BN(0).toString(), + "principal should be 0" + ); + expect(end_loan["collateral"]).to.be.bignumber.equal( + new BN(0).toString(), + "collateral should be 0" + ); + expect(end_loan["currentMargin"]).to.be.bignumber.equal( + new BN(0).toString(), + "current margin should be 0" + ); + expect(end_loan["maxLiquidatable"]).to.be.bignumber.equal( + new BN(0).toString(), + "current margin should be 0" + ); + expect(end_loan["maxSeizable"]).to.be.bignumber.equal( + new BN(0).toString(), + "current margin should be 0" + ); + expect(end_loan["interestOwedPerDay"]).to.be.bignumber.equal( + new BN(0).toString(), + "current margin should be 0" + ); + expect(end_loan["interestDepositRemaining"]).to.be.bignumber.equal( + new BN(0).toString(), + "current margin should be 0" + ); + + // CHECK THE CLOSE SWAP IS WORKING PROPERLY + const decode = decodeLogs(receipt.rawLogs, LoanClosingsEvents, "CloseWithSwap"); + const swapEvent = decode[0].args; + + const loanSwapEvent = decodeLogs(receipt.rawLogs, SwapEvents, "LoanSwap"); + const sourceTokenAmountUsed = new BN(loanSwapEvent[0].args["sourceAmount"]); + + expect(swapEvent["user"]).to.equal(borrower); + expect(swapEvent["lender"]).to.equal(loanToken.address); // lender is the pool in this case + expect(swapEvent["loanId"]).to.equal(loan_id); + expect(swapEvent["closer"]).to.equal(rollover_wallet); // the one who called the rollover function + expect(swapEvent["loanToken"]).to.equal(SUSD.address); /// Don't get confused, the loanToken is not the pool token, it's the underlying token + expect(swapEvent["collateralToken"]).to.equal(RBTC.address); + expect(swapEvent["loanCloseAmount"]).to.equal(loan_before_rolled_over["principal"]); // the principal before rolled over + expect(swapEvent["positionCloseSize"]).to.equal( + new BN(loan_before_rolled_over["collateral"]).sub(sourceTokenAmountUsed).toString() + ); // the principal before rolled over + }); + + it("Test rollover with special rebates", async () => { + // prepare the test + await sovryn.setSpecialRebates(SUSD.address, RBTC.address, wei("300", "ether")); + const [borrower, loan, loan_id, endTimestamp] = await setup_rollover_test( + RBTC, + SUSD, + accounts, + loanToken, + hunEth, // loan_token_sent + set_demand_curve, + sovryn + ); + + const lender_interest_data = await sovryn.getLenderInterestData( + loanToken.address, + SUSD.address + ); + + const lender_pool_initial_balance = await SUSD.balanceOf(loanToken.address); + + lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); + const sov_borrower_initial_balance = (await SOV.balanceOf(borrower)).add( + await lockedSOV.getLockedBalance(borrower) + ); + + const { receipt } = await sovryn.rollover(loan_id, "0x"); + const susd_bal_after_rollover = (await SUSD.balanceOf(loanToken.address)).toString(); + + const lender_interest_after = await sovryn.getLenderInterestData( + loanToken.address, + SUSD.address + ); + + const lending_fee_percent = await sovryn.lendingFeePercent(); + const interest_unpaid = new BN(lender_interest_data["interestUnPaid"]); + const lending_fee = interest_unpaid.mul(lending_fee_percent).div(hunEth); + let interest_owed_now = interest_unpaid.sub(lending_fee); + + num = await blockNumber(); + currentBlock = await web3.eth.getBlock(num); + block_timestamp = currentBlock.timestamp; + if (block_timestamp > endTimestamp) { + backInterestTime = new BN(block_timestamp - endTimestamp); + backInterestOwed = backInterestTime + .mul(lender_interest_data["interestOwedPerDay"]) + .div(new BN(24 * 60 * 60)); + const lending_fee = backInterestOwed.mul(lending_fee_percent).div(hunEth); + backInterestOwed = backInterestOwed.sub(lending_fee); + interest_owed_now = interest_owed_now.add(backInterestOwed); + } + + expect(await SUSD.balanceOf(loanToken.address)).to.be.bignumber.equal( + lender_pool_initial_balance.add(interest_owed_now) + ); + expect(lender_interest_after["interestPaid"] == interest_unpaid.toString()).to.be.true; + expect(lender_interest_after["interestUnPaid"] == "0").to.be.true; + + // Settles and pays borrowers based on fees generated by their interest payments + if ((await sovryn.protocolTokenHeld()) != 0) + await verify_sov_reward_payment( + receipt.rawLogs, + FeesEvents, + SOV, + borrower, + loan_id, + sov_borrower_initial_balance, + 2, + SUSD.address, + RBTC.address, + sovryn + ); + + const loan_after = await sovryn.getLoan(loan_id); + expect( + loan_after["endTimestamp"] >= parseInt(loan["endTimestamp"]) + 28 * 24 * 60 * 60 + ).to.be.true; + const { rate: trade_rate, precision } = await priceFeeds.queryRate( + RBTC.address, + SUSD.address + ); + const trading_fee_percent = await sovryn.tradingFeePercent(); + const trading_fee = interest_unpaid.mul(trading_fee_percent).div(hunEth); + + const decode = decodeLogs(receipt.rawLogs, SwapsEvents, "LoanSwap"); + const loan_swap_event = decode[0].args; + expect(loan_swap_event["loanId"] == loan_id).to.be.true; + expect(loan_swap_event["sourceToken"] == RBTC.address).to.be.true; + expect(loan_swap_event["destToken"] == SUSD.address).to.be.true; + expect(loan_swap_event["borrower"] == borrower).to.be.true; + // source buffer = 10000 in sovryn swap connector + expect( + new BN(loan_swap_event["sourceAmount"]) + .sub(interest_unpaid) + .add(trading_fee) + .mul(precision) + .div(trade_rate) + .lte(new BN(10000)) + ).to.be.true; + expect(new BN(loan_swap_event["destAmount"]).gte(interest_unpaid)).to.be.true; + }); + + /* Collateral should decrease Sender collateral balance should increase */ - it("Test rollover reward payment", async () => { - // prepare the test - const [, initial_loan, loan_id] = await setup_rollover_test(RBTC, SUSD, accounts, loanToken, hunEth, set_demand_curve, sovryn); - - const num = await blockNumber(); - let currentBlock = await web3.eth.getBlock(num); - const block_timestamp = currentBlock.timestamp; - const time_until_loan_end = initial_loan["endTimestamp"] - block_timestamp; - await increaseTime(time_until_loan_end); - - const receiver = accounts[3]; - expect((await RBTC.balanceOf(receiver)).toNumber() == 0).to.be.true; - - const previousRolloverWalletBalanceSUSD = await SUSD.balanceOf(receiver); - const previousRolloverWalletBalanceRBTC = await RBTC.balanceOf(receiver); - const { receipt } = await sovryn.rollover(loan_id, "0x", { from: receiver }); - const latestRolloverWalletBalanceSUSD = await SUSD.balanceOf(receiver); - const latestRolloverWalletBalanceRBTC = await RBTC.balanceOf(receiver); - - const vaultWithdrawEvents = decodeLogs(receipt.rawLogs, VaultController, "VaultWithdraw"); - - const end_loan = await sovryn.getLoan(loan_id); - const decode = decodeLogs(receipt.rawLogs, SwapsEvents, "LoanSwap"); - const loan_swap_event = decode[0].args; - const source_token_amount_used = new BN(loan_swap_event["sourceAmount"]); - - // Make sure the rollover wallet get the rollover reward - const rollover_reward = vaultWithdrawEvents[vaultWithdrawEvents.length - 1].args["amount"]; - expect(latestRolloverWalletBalanceRBTC.toString()).to.equal( - previousRolloverWalletBalanceRBTC.add(new BN(rollover_reward)).toString() - ); - - expect(previousRolloverWalletBalanceSUSD.toString()).to.equal(new BN(0).toString()); - expect(latestRolloverWalletBalanceSUSD.toString()).to.equal(new BN(0).toString()); - }); - - it("Test rollover reward payment with unhealthy position (margin <= 3%) should revert", async () => { - // prepare the test - const [, initial_loan, loan_id] = await setup_rollover_test(RBTC, SUSD, accounts, loanToken, hunEth, set_demand_curve, sovryn); - - const num = await blockNumber(); - let currentBlock = await web3.eth.getBlock(num); - const block_timestamp = currentBlock.timestamp; - const time_until_loan_end = initial_loan["endTimestamp"] - block_timestamp; - await increaseTime(time_until_loan_end); - - const receiver = accounts[3]; - expect((await RBTC.balanceOf(receiver)).toNumber() == 0).to.be.true; - - // Set the rbtc price become cheaper, so that it can create the unhealth position (0% margin) because the collateral value less than the loan amount - await priceFeeds.setRates(RBTC.address, SUSD.address, new BN(10).pow(new BN(21)).toString()); - await expectRevert(sovryn.rollover(loan_id, "0x", { from: receiver }), "unhealthy position"); - }); - }); + it("Test rollover reward payment", async () => { + // prepare the test + const [, initial_loan, loan_id] = await setup_rollover_test( + RBTC, + SUSD, + accounts, + loanToken, + hunEth, + set_demand_curve, + sovryn + ); + + const num = await blockNumber(); + let currentBlock = await web3.eth.getBlock(num); + const block_timestamp = currentBlock.timestamp; + const time_until_loan_end = initial_loan["endTimestamp"] - block_timestamp; + await increaseTime(time_until_loan_end); + + const receiver = accounts[3]; + expect((await RBTC.balanceOf(receiver)).toNumber() == 0).to.be.true; + + const previousRolloverWalletBalanceSUSD = await SUSD.balanceOf(receiver); + const previousRolloverWalletBalanceRBTC = await RBTC.balanceOf(receiver); + const { receipt } = await sovryn.rollover(loan_id, "0x", { from: receiver }); + const latestRolloverWalletBalanceSUSD = await SUSD.balanceOf(receiver); + const latestRolloverWalletBalanceRBTC = await RBTC.balanceOf(receiver); + + const vaultWithdrawEvents = decodeLogs( + receipt.rawLogs, + VaultController, + "VaultWithdraw" + ); + + const end_loan = await sovryn.getLoan(loan_id); + const decode = decodeLogs(receipt.rawLogs, SwapsEvents, "LoanSwap"); + const loan_swap_event = decode[0].args; + const source_token_amount_used = new BN(loan_swap_event["sourceAmount"]); + + // Make sure the rollover wallet get the rollover reward + const rollover_reward = + vaultWithdrawEvents[vaultWithdrawEvents.length - 1].args["amount"]; + expect(latestRolloverWalletBalanceRBTC.toString()).to.equal( + previousRolloverWalletBalanceRBTC.add(new BN(rollover_reward)).toString() + ); + + expect(previousRolloverWalletBalanceSUSD.toString()).to.equal(new BN(0).toString()); + expect(latestRolloverWalletBalanceSUSD.toString()).to.equal(new BN(0).toString()); + }); + + it("Test rollover reward payment with unhealthy position (margin <= 3%) should revert", async () => { + // prepare the test + const [, initial_loan, loan_id] = await setup_rollover_test( + RBTC, + SUSD, + accounts, + loanToken, + hunEth, + set_demand_curve, + sovryn + ); + + const num = await blockNumber(); + let currentBlock = await web3.eth.getBlock(num); + const block_timestamp = currentBlock.timestamp; + const time_until_loan_end = initial_loan["endTimestamp"] - block_timestamp; + await increaseTime(time_until_loan_end); + + const receiver = accounts[3]; + expect((await RBTC.balanceOf(receiver)).toNumber() == 0).to.be.true; + + // Set the rbtc price become cheaper, so that it can create the unhealth position (0% margin) because the collateral value less than the loan amount + await priceFeeds.setRates( + RBTC.address, + SUSD.address, + new BN(10).pow(new BN(21)).toString() + ); + await expectRevert( + sovryn.rollover(loan_id, "0x", { from: receiver }), + "unhealthy position" + ); + }); + }); }); diff --git a/tests/protocol/WithdrawFeesAndInterest.test.js b/tests/protocol/WithdrawFeesAndInterest.test.js index 8eee90009..b1ee6bead 100644 --- a/tests/protocol/WithdrawFeesAndInterest.test.js +++ b/tests/protocol/WithdrawFeesAndInterest.test.js @@ -16,117 +16,138 @@ const { BN, expectEvent, expectRevert } = require("@openzeppelin/test-helpers"); const { increaseTime, blockNumber } = require("../Utils/Ethereum.js"); const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getLoanToken, - getLoanTokenWRBTC, - loan_pool_setup, - set_demand_curve, - getPriceFeeds, - getSovryn, - getSOV, - lend_to_pool, - open_margin_trade_position, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getLoanToken, + getLoanTokenWRBTC, + loan_pool_setup, + set_demand_curve, + getPriceFeeds, + getSovryn, + getSOV, + lend_to_pool, + open_margin_trade_position, } = require("../Utils/initializer.js"); const InterestUser = artifacts.require("InterestUser"); contract("ProtocolWithdrawFeeAndInterest", (accounts) => { - let owner; - let sovryn, SUSD, WRBTC, RBTC, BZRX, loanToken, loanTokenWRBTC, priceFeeds; - - async function deploymentAndInitFixture(_wallets, _provider) { - // Deploying sovrynProtocol w/ generic function from initializer.js - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - - /// @dev SOV test token deployment w/ initializer.js - sov = await getSOV(sovryn, priceFeeds, SUSD, accounts); - - loanToken = await getLoanToken(owner, sovryn, WRBTC, SUSD); - loanTokenWRBTC = await getLoanTokenWRBTC(owner, sovryn, WRBTC, SUSD); - await loan_pool_setup(sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC); - - /// @dev Optimization: Moved from common init for specific test - await set_demand_curve(loanToken); - await lend_to_pool(loanToken, SUSD, owner); - } - - before(async () => { - [owner] = accounts; - }); - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - describe("Tests withdraw fees and interest ", () => { - it("Test withdraw accrued interest", async () => { - // prepare the test - const [loan_id] = await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, owner); - - let num = await blockNumber(); - let lastBlock = await web3.eth.getBlock(num); - const initial_block_timestamp = lastBlock.timestamp; - - const loan = await sovryn.getLoan(loan_id); - //console.log("loan = " + loan); - const lender = loanToken.address; - - // Time travel - const time_until_loan_end = loan["endTimestamp"] - initial_block_timestamp; - await increaseTime(time_until_loan_end); - - const end_interest_data_1 = await sovryn.getLenderInterestData(lender, SUSD.address); - expect(end_interest_data_1["interestPaid"] == "0").to.be.true; - // console.log("end_interest_data_1[interestUnPaid] = " + end_interest_data_1["interestUnPaid"]); - // console.log("end_interest_data_1[interestFeePercent] = " + end_interest_data_1["interestFeePercent"]); - - const feesApplied = new BN(end_interest_data_1["interestUnPaid"]) - .mul(end_interest_data_1["interestFeePercent"]) - .div(new BN(10).pow(new BN(20))); - - // lend to pool to call settle interest which calls withdrawAccruedInterest - // let tx = await lend_to_pool(loanToken, SUSD, owner); - // Instead of using lend_to_pool, use explicit transactions in order to capture - // the event PayInterestTransfer when loanToken.mint - - const lend_amount = new BN(10).pow(new BN(30)).toString(); - await SUSD.mint(lender, lend_amount); - await SUSD.approve(loanToken.address, lend_amount); - let tx = await loanToken.mint(lender, lend_amount); - - // Check the event PayInterestTransfer is reporting properly - await expectEvent.inTransaction(tx.receipt.rawLogs[0].transactionHash, InterestUser, "PayInterestTransfer", { - interestToken: loan["loanToken"], - lender: lender, - effectiveInterest: new BN(end_interest_data_1["interestUnPaid"]).sub(feesApplied), - }); - - const end_interest_data_2 = await sovryn.getLenderInterestData(lender, SUSD.address); - - num = await blockNumber(); - lastBlock = await web3.eth.getBlock(num); - const second_block_timestamp = lastBlock.timestamp; - - const interest_owed_now = new BN(loan["endTimestamp"] - initial_block_timestamp) - .mul(end_interest_data_1["interestOwedPerDay"]) - .div(new BN(24 * 60 * 60)); - - expect(end_interest_data_2["interestOwedPerDay"].toString() != "0").to.be.true; - expect(end_interest_data_2["interestPaid"].toString()).eq(interest_owed_now.toString()); - expect(end_interest_data_2["interestPaidDate"].toNumber() - second_block_timestamp <= 2).to.be.true; - expect(end_interest_data_2["interestUnPaid"].eq(end_interest_data_1["interestUnPaid"].sub(interest_owed_now))); - }); - - /* + let owner; + let sovryn, SUSD, WRBTC, RBTC, BZRX, loanToken, loanTokenWRBTC, priceFeeds; + + async function deploymentAndInitFixture(_wallets, _provider) { + // Deploying sovrynProtocol w/ generic function from initializer.js + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + + /// @dev SOV test token deployment w/ initializer.js + sov = await getSOV(sovryn, priceFeeds, SUSD, accounts); + + loanToken = await getLoanToken(owner, sovryn, WRBTC, SUSD); + loanTokenWRBTC = await getLoanTokenWRBTC(owner, sovryn, WRBTC, SUSD); + await loan_pool_setup(sovryn, owner, RBTC, WRBTC, SUSD, loanToken, loanTokenWRBTC); + + /// @dev Optimization: Moved from common init for specific test + await set_demand_curve(loanToken); + await lend_to_pool(loanToken, SUSD, owner); + } + + before(async () => { + [owner] = accounts; + }); + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + describe("Tests withdraw fees and interest ", () => { + it("Test withdraw accrued interest", async () => { + // prepare the test + const [loan_id] = await open_margin_trade_position( + loanToken, + RBTC, + WRBTC, + SUSD, + owner + ); + + let num = await blockNumber(); + let lastBlock = await web3.eth.getBlock(num); + const initial_block_timestamp = lastBlock.timestamp; + + const loan = await sovryn.getLoan(loan_id); + //console.log("loan = " + loan); + const lender = loanToken.address; + + // Time travel + const time_until_loan_end = loan["endTimestamp"] - initial_block_timestamp; + await increaseTime(time_until_loan_end); + + const end_interest_data_1 = await sovryn.getLenderInterestData(lender, SUSD.address); + expect(end_interest_data_1["interestPaid"] == "0").to.be.true; + // console.log("end_interest_data_1[interestUnPaid] = " + end_interest_data_1["interestUnPaid"]); + // console.log("end_interest_data_1[interestFeePercent] = " + end_interest_data_1["interestFeePercent"]); + + const feesApplied = new BN(end_interest_data_1["interestUnPaid"]) + .mul(end_interest_data_1["interestFeePercent"]) + .div(new BN(10).pow(new BN(20))); + + // lend to pool to call settle interest which calls withdrawAccruedInterest + // let tx = await lend_to_pool(loanToken, SUSD, owner); + // Instead of using lend_to_pool, use explicit transactions in order to capture + // the event PayInterestTransfer when loanToken.mint + + const lend_amount = new BN(10).pow(new BN(30)).toString(); + await SUSD.mint(lender, lend_amount); + await SUSD.approve(loanToken.address, lend_amount); + let tx = await loanToken.mint(lender, lend_amount); + + // Check the event PayInterestTransfer is reporting properly + await expectEvent.inTransaction( + tx.receipt.rawLogs[0].transactionHash, + InterestUser, + "PayInterestTransfer", + { + interestToken: loan["loanToken"], + lender: lender, + effectiveInterest: new BN(end_interest_data_1["interestUnPaid"]).sub( + feesApplied + ), + } + ); + + const end_interest_data_2 = await sovryn.getLenderInterestData(lender, SUSD.address); + + num = await blockNumber(); + lastBlock = await web3.eth.getBlock(num); + const second_block_timestamp = lastBlock.timestamp; + + const interest_owed_now = new BN(loan["endTimestamp"] - initial_block_timestamp) + .mul(end_interest_data_1["interestOwedPerDay"]) + .div(new BN(24 * 60 * 60)); + + expect(end_interest_data_2["interestOwedPerDay"].toString() != "0").to.be.true; + expect(end_interest_data_2["interestPaid"].toString()).eq( + interest_owed_now.toString() + ); + expect( + end_interest_data_2["interestPaidDate"].toNumber() - second_block_timestamp <= 2 + ).to.be.true; + expect( + end_interest_data_2["interestUnPaid"].eq( + end_interest_data_1["interestUnPaid"].sub(interest_owed_now) + ) + ); + }); + + /* Should successfully withdraw lending fees 1. Set demand curve (fixture) 2. Lend to the pool (fixture) @@ -136,68 +157,79 @@ contract("ProtocolWithdrawFeeAndInterest", (accounts) => { 6. Call withdraw lending fees 7. Verify the right amount was paid out and the lending fees reduced on the smart contract */ - it("Test withdraw lending fees", async () => { - // prepare the test - await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, owner); - - await sovryn.setFeesController(accounts[0]); - await increaseTime(100); - - await lend_to_pool(loanToken, SUSD, owner); - - // withdraw fees and verify - const fees = await sovryn.lendingFeeTokensHeld(SUSD.address); - await sovryn.withdrawLendingFees(SUSD.address, accounts[1], fees); - const paid = await sovryn.lendingFeeTokensPaid(SUSD.address); - - expect(paid.eq(fees)).to.be.true; - expect(await sovryn.lendingFeeTokensHeld(SUSD.address)).to.be.a.bignumber.eq(new BN(0)); - expect(await SUSD.balanceOf(accounts[1])).to.be.a.bignumber.eq(fees); - }); - - it("should revert when withdrawing lending fees by no feesController", async () => { - // Prepare the test - await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, owner); - await sovryn.setFeesController(accounts[0]); - await increaseTime(100); - await lend_to_pool(loanToken, SUSD, owner); - - // Try to withdraw fees - const fees = await sovryn.lendingFeeTokensHeld(SUSD.address); - await expectRevert(sovryn.withdrawLendingFees(SUSD.address, accounts[1], fees, { from: accounts[1] }), "unauthorized"); - }); - - it("should ignore withdrawAmounts bigger than balance when withdrawing lending fees", async () => { - // Prepare the test - await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, owner); - await sovryn.setFeesController(accounts[0]); - await increaseTime(100); - await lend_to_pool(loanToken, SUSD, owner); - - // Withdraw fees and verify - const fees = await sovryn.lendingFeeTokensHeld(SUSD.address); - await sovryn.withdrawLendingFees(SUSD.address, accounts[1], fees.mul(new BN(2))); - const paid = await sovryn.lendingFeeTokensPaid(SUSD.address); - - expect(paid.eq(fees)).to.be.true; - expect(await sovryn.lendingFeeTokensHeld(SUSD.address)).to.be.a.bignumber.eq(new BN(0)); - expect(await SUSD.balanceOf(accounts[1])).to.be.a.bignumber.eq(fees); - }); - - it("should return false when withdrawing amount 0 of lending fees", async () => { - // Prepare the test - await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, owner); - await sovryn.setFeesController(accounts[0]); - await increaseTime(100); - await lend_to_pool(loanToken, SUSD, owner); - - // Withdraw fees and verify - const fees = await sovryn.lendingFeeTokensHeld(SUSD.address); - let result = await sovryn.withdrawLendingFees.call(SUSD.address, accounts[1], new BN(0)); - expect(result).to.be.false; - }); - - /* + it("Test withdraw lending fees", async () => { + // prepare the test + await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, owner); + + await sovryn.setFeesController(accounts[0]); + await increaseTime(100); + + await lend_to_pool(loanToken, SUSD, owner); + + // withdraw fees and verify + const fees = await sovryn.lendingFeeTokensHeld(SUSD.address); + await sovryn.withdrawLendingFees(SUSD.address, accounts[1], fees); + const paid = await sovryn.lendingFeeTokensPaid(SUSD.address); + + expect(paid.eq(fees)).to.be.true; + expect(await sovryn.lendingFeeTokensHeld(SUSD.address)).to.be.a.bignumber.eq( + new BN(0) + ); + expect(await SUSD.balanceOf(accounts[1])).to.be.a.bignumber.eq(fees); + }); + + it("should revert when withdrawing lending fees by no feesController", async () => { + // Prepare the test + await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, owner); + await sovryn.setFeesController(accounts[0]); + await increaseTime(100); + await lend_to_pool(loanToken, SUSD, owner); + + // Try to withdraw fees + const fees = await sovryn.lendingFeeTokensHeld(SUSD.address); + await expectRevert( + sovryn.withdrawLendingFees(SUSD.address, accounts[1], fees, { from: accounts[1] }), + "unauthorized" + ); + }); + + it("should ignore withdrawAmounts bigger than balance when withdrawing lending fees", async () => { + // Prepare the test + await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, owner); + await sovryn.setFeesController(accounts[0]); + await increaseTime(100); + await lend_to_pool(loanToken, SUSD, owner); + + // Withdraw fees and verify + const fees = await sovryn.lendingFeeTokensHeld(SUSD.address); + await sovryn.withdrawLendingFees(SUSD.address, accounts[1], fees.mul(new BN(2))); + const paid = await sovryn.lendingFeeTokensPaid(SUSD.address); + + expect(paid.eq(fees)).to.be.true; + expect(await sovryn.lendingFeeTokensHeld(SUSD.address)).to.be.a.bignumber.eq( + new BN(0) + ); + expect(await SUSD.balanceOf(accounts[1])).to.be.a.bignumber.eq(fees); + }); + + it("should return false when withdrawing amount 0 of lending fees", async () => { + // Prepare the test + await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, owner); + await sovryn.setFeesController(accounts[0]); + await increaseTime(100); + await lend_to_pool(loanToken, SUSD, owner); + + // Withdraw fees and verify + const fees = await sovryn.lendingFeeTokensHeld(SUSD.address); + let result = await sovryn.withdrawLendingFees.call( + SUSD.address, + accounts[1], + new BN(0) + ); + expect(result).to.be.false; + }); + + /* Should successfully withdraw trading fees 1. Set demand curve (fixture) 2. Lend to the pool (fixture) @@ -207,68 +239,79 @@ contract("ProtocolWithdrawFeeAndInterest", (accounts) => { 6. Call withdraw trading fees 7. Verify the right amount was paid out and the trading fees reduced on the smart contract */ - it("Test withdraw trading fees", async () => { - // prepare the test - await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, owner); - - await sovryn.setFeesController(accounts[0]); - await increaseTime(100); - - await lend_to_pool(loanToken, SUSD, owner); - - // withdraw fees and verify - const fees = await sovryn.tradingFeeTokensHeld(SUSD.address); - await sovryn.withdrawTradingFees(SUSD.address, accounts[1], fees); - const paid = await sovryn.tradingFeeTokensPaid(SUSD.address); - - expect(paid.eq(fees)).to.be.true; - expect(await sovryn.tradingFeeTokensHeld(SUSD.address)).to.be.a.bignumber.eq(new BN(0)); - expect(await SUSD.balanceOf(accounts[1])).to.be.a.bignumber.eq(fees); - }); - - it("should revert when withdrawing trading fees by no feesController", async () => { - // Prepare the test - await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, owner); - await sovryn.setFeesController(accounts[0]); - await increaseTime(100); - await lend_to_pool(loanToken, SUSD, owner); - - // Try to withdraw fees - const fees = await sovryn.lendingFeeTokensHeld(SUSD.address); - await expectRevert(sovryn.withdrawTradingFees(SUSD.address, accounts[1], fees, { from: accounts[1] }), "unauthorized"); - }); - - it("should ignore withdrawAmounts bigger than balance when withdrawing trading fees", async () => { - // Prepare the test - await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, owner); - await sovryn.setFeesController(accounts[0]); - await increaseTime(100); - await lend_to_pool(loanToken, SUSD, owner); - - // Withdraw fees and verify - const fees = await sovryn.tradingFeeTokensHeld(SUSD.address); - await sovryn.withdrawTradingFees(SUSD.address, accounts[1], fees.mul(new BN(2))); - const paid = await sovryn.tradingFeeTokensPaid(SUSD.address); - - expect(paid.eq(fees)).to.be.true; - expect(await sovryn.tradingFeeTokensHeld(SUSD.address)).to.be.a.bignumber.eq(new BN(0)); - expect(await SUSD.balanceOf(accounts[1])).to.be.a.bignumber.eq(fees); - }); - - it("should return false when withdrawing amount 0 of trading fees", async () => { - // Prepare the test - await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, owner); - await sovryn.setFeesController(accounts[0]); - await increaseTime(100); - await lend_to_pool(loanToken, SUSD, owner); - - // Withdraw fees and verify - const fees = await sovryn.tradingFeeTokensHeld(SUSD.address); - let result = await sovryn.withdrawTradingFees.call(SUSD.address, accounts[1], new BN(0)); - expect(result).to.be.false; - }); - - /* + it("Test withdraw trading fees", async () => { + // prepare the test + await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, owner); + + await sovryn.setFeesController(accounts[0]); + await increaseTime(100); + + await lend_to_pool(loanToken, SUSD, owner); + + // withdraw fees and verify + const fees = await sovryn.tradingFeeTokensHeld(SUSD.address); + await sovryn.withdrawTradingFees(SUSD.address, accounts[1], fees); + const paid = await sovryn.tradingFeeTokensPaid(SUSD.address); + + expect(paid.eq(fees)).to.be.true; + expect(await sovryn.tradingFeeTokensHeld(SUSD.address)).to.be.a.bignumber.eq( + new BN(0) + ); + expect(await SUSD.balanceOf(accounts[1])).to.be.a.bignumber.eq(fees); + }); + + it("should revert when withdrawing trading fees by no feesController", async () => { + // Prepare the test + await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, owner); + await sovryn.setFeesController(accounts[0]); + await increaseTime(100); + await lend_to_pool(loanToken, SUSD, owner); + + // Try to withdraw fees + const fees = await sovryn.lendingFeeTokensHeld(SUSD.address); + await expectRevert( + sovryn.withdrawTradingFees(SUSD.address, accounts[1], fees, { from: accounts[1] }), + "unauthorized" + ); + }); + + it("should ignore withdrawAmounts bigger than balance when withdrawing trading fees", async () => { + // Prepare the test + await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, owner); + await sovryn.setFeesController(accounts[0]); + await increaseTime(100); + await lend_to_pool(loanToken, SUSD, owner); + + // Withdraw fees and verify + const fees = await sovryn.tradingFeeTokensHeld(SUSD.address); + await sovryn.withdrawTradingFees(SUSD.address, accounts[1], fees.mul(new BN(2))); + const paid = await sovryn.tradingFeeTokensPaid(SUSD.address); + + expect(paid.eq(fees)).to.be.true; + expect(await sovryn.tradingFeeTokensHeld(SUSD.address)).to.be.a.bignumber.eq( + new BN(0) + ); + expect(await SUSD.balanceOf(accounts[1])).to.be.a.bignumber.eq(fees); + }); + + it("should return false when withdrawing amount 0 of trading fees", async () => { + // Prepare the test + await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, owner); + await sovryn.setFeesController(accounts[0]); + await increaseTime(100); + await lend_to_pool(loanToken, SUSD, owner); + + // Withdraw fees and verify + const fees = await sovryn.tradingFeeTokensHeld(SUSD.address); + let result = await sovryn.withdrawTradingFees.call( + SUSD.address, + accounts[1], + new BN(0) + ); + expect(result).to.be.false; + }); + + /* Should successfully withdraw borrowing fees 1. Set demand curve (fixture) 2. Lend to the pool (fixture) @@ -278,30 +321,30 @@ contract("ProtocolWithdrawFeeAndInterest", (accounts) => { 6. Call withdraw borrowing fees 7. Verify the right amount was paid out and the borrowing fees reduced on the smart contract */ - /// @dev In this test borrowing fees are always 0 - /// Borrowing fees are only accrued from torque loans - /// Added a new test on tests/other/LoanOpeningsBorrowOrTradeFromPool.test.js - /// checking withdrawBorrowingFees by using a torque loan. - /// So, this test has been commented out because it seems to be incomplete. - // it("Test withdraw borrowing fees", async () => { - // // prepare the test - // await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, owner); - - // await sovryn.setFeesController(accounts[0]); - // await increaseTime(100); - - // await lend_to_pool(loanToken, SUSD, owner); - - // // withdraw fees and verify - // const fees = await sovryn.borrowingFeeTokensHeld(SUSD.address); - // console.log("fees: ", fees.toString()); - // await sovryn.withdrawBorrowingFees(SUSD.address, accounts[1], fees); - // const paid = await sovryn.borrowingFeeTokensPaid(SUSD.address); - // console.log("paid: ", paid.toString()); - - // expect(paid.eq(fees)).to.be.true; - // expect(await sovryn.borrowingFeeTokensHeld(SUSD.address)).to.be.a.bignumber.eq(new BN(0)); - // expect(await SUSD.balanceOf(accounts[1])).to.be.a.bignumber.eq(fees); - // }); - }); + /// @dev In this test borrowing fees are always 0 + /// Borrowing fees are only accrued from torque loans + /// Added a new test on tests/other/LoanOpeningsBorrowOrTradeFromPool.test.js + /// checking withdrawBorrowingFees by using a torque loan. + /// So, this test has been commented out because it seems to be incomplete. + // it("Test withdraw borrowing fees", async () => { + // // prepare the test + // await open_margin_trade_position(loanToken, RBTC, WRBTC, SUSD, owner); + + // await sovryn.setFeesController(accounts[0]); + // await increaseTime(100); + + // await lend_to_pool(loanToken, SUSD, owner); + + // // withdraw fees and verify + // const fees = await sovryn.borrowingFeeTokensHeld(SUSD.address); + // console.log("fees: ", fees.toString()); + // await sovryn.withdrawBorrowingFees(SUSD.address, accounts[1], fees); + // const paid = await sovryn.borrowingFeeTokensPaid(SUSD.address); + // console.log("paid: ", paid.toString()); + + // expect(paid.eq(fees)).to.be.true; + // expect(await sovryn.borrowingFeeTokensHeld(SUSD.address)).to.be.a.bignumber.eq(new BN(0)); + // expect(await SUSD.balanceOf(accounts[1])).to.be.a.bignumber.eq(fees); + // }); + }); }); diff --git a/tests/protocol/liquidationFunctions.js b/tests/protocol/liquidationFunctions.js index 4e1d5ae43..f7dc006aa 100644 --- a/tests/protocol/liquidationFunctions.js +++ b/tests/protocol/liquidationFunctions.js @@ -11,171 +11,183 @@ const oneEth = new BN(wei("1", "ether")); const hunEth = new BN(wei("100", "ether")); const liquidate = async ( - accounts, - loanToken, - underlyingToken, - set_demand_curve, - collateralToken, - sovryn, - priceFeeds, - rate, - WRBTC, - FeesEvents, - SOV, - isSpecialRebates = false, - checkRepayAmount = true + accounts, + loanToken, + underlyingToken, + set_demand_curve, + collateralToken, + sovryn, + priceFeeds, + rate, + WRBTC, + FeesEvents, + SOV, + isSpecialRebates = false, + checkRepayAmount = true ) => { - if (isSpecialRebates) { - await sovryn.setSpecialRebates(underlyingToken.address, collateralToken.address, wei("70", "ether")); - } - - // set the demand curve to set interest rates - await set_demand_curve(loanToken); - const lender = accounts[0]; - const borrower = accounts[1]; - const liquidator = accounts[2]; - const loan_token_sent = new BN(10).mul(oneEth); - // lend to the pool, mint tokens if required, open a margin trade position - const loan_id = await prepare_liquidation( - lender, - borrower, - liquidator, - loan_token_sent, - loanToken, - underlyingToken, - collateralToken, - sovryn, - WRBTC - ); - - const loan = await sovryn.getLoan(loan_id); - // set the rates so we're able to liquidate - let value; - if (underlyingToken.address == WRBTC.address) { - await priceFeeds.setRates(underlyingToken.address, collateralToken.address, rate); - value = loan_token_sent; - } else { - await priceFeeds.setRates(collateralToken.address, underlyingToken.address, rate); - value = 0; - } - - lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); - const sov_borrower_initial_balance = (await SOV.balanceOf(borrower)).add(await lockedSOV.getLockedBalance(borrower)); - await increaseTime(10 * 24 * 60 * 60); // time travel 10 days - - // liquidate - const { receipt } = await sovryn.liquidate(loan_id, liquidator, loan_token_sent, { from: liquidator, value: value }); - await verify_liquidation_event( - loan, - receipt.rawLogs, - lender, - borrower, - liquidator, - loan_token_sent, - loanToken, - underlyingToken, - collateralToken, - sovryn, - priceFeeds, - checkRepayAmount - ); - if (underlyingToken.address != WRBTC.address) - await verify_sov_reward_payment( - receipt.rawLogs, - FeesEvents, - SOV, - borrower, - loan_id, - sov_borrower_initial_balance, - 1, - underlyingToken.address, - collateralToken.address, - sovryn - ); + if (isSpecialRebates) { + await sovryn.setSpecialRebates( + underlyingToken.address, + collateralToken.address, + wei("70", "ether") + ); + } + + // set the demand curve to set interest rates + await set_demand_curve(loanToken); + const lender = accounts[0]; + const borrower = accounts[1]; + const liquidator = accounts[2]; + const loan_token_sent = new BN(10).mul(oneEth); + // lend to the pool, mint tokens if required, open a margin trade position + const loan_id = await prepare_liquidation( + lender, + borrower, + liquidator, + loan_token_sent, + loanToken, + underlyingToken, + collateralToken, + sovryn, + WRBTC + ); + + const loan = await sovryn.getLoan(loan_id); + // set the rates so we're able to liquidate + let value; + if (underlyingToken.address == WRBTC.address) { + await priceFeeds.setRates(underlyingToken.address, collateralToken.address, rate); + value = loan_token_sent; + } else { + await priceFeeds.setRates(collateralToken.address, underlyingToken.address, rate); + value = 0; + } + + lockedSOV = await LockedSOVMockup.at(await sovryn.lockedSOVAddress()); + const sov_borrower_initial_balance = (await SOV.balanceOf(borrower)).add( + await lockedSOV.getLockedBalance(borrower) + ); + await increaseTime(10 * 24 * 60 * 60); // time travel 10 days + + // liquidate + const { receipt } = await sovryn.liquidate(loan_id, liquidator, loan_token_sent, { + from: liquidator, + value: value, + }); + await verify_liquidation_event( + loan, + receipt.rawLogs, + lender, + borrower, + liquidator, + loan_token_sent, + loanToken, + underlyingToken, + collateralToken, + sovryn, + priceFeeds, + checkRepayAmount + ); + if (underlyingToken.address != WRBTC.address) + await verify_sov_reward_payment( + receipt.rawLogs, + FeesEvents, + SOV, + borrower, + loan_id, + sov_borrower_initial_balance, + 1, + underlyingToken.address, + collateralToken.address, + sovryn + ); }; /* should fail to liquidate a healthy position */ const liquidate_healthy_position_should_fail = async ( - accounts, - loanToken, - underlyingToken, - set_demand_curve, - collateralToken, - sovryn, - priceFeeds, - WRBTC + accounts, + loanToken, + underlyingToken, + set_demand_curve, + collateralToken, + sovryn, + priceFeeds, + WRBTC ) => { - // set the demand curve to set interest rates - set_demand_curve(loanToken); - const lender = accounts[0]; - const borrower = accounts[1]; - const liquidator = accounts[2]; - const loan_token_sent = new BN(10).mul(oneEth); - // lend to the pool, mint tokens if required, open a margin trade position - const loan_id = await prepare_liquidation( - lender, - borrower, - liquidator, - loan_token_sent, - loanToken, - underlyingToken, - collateralToken, - sovryn, - WRBTC - ); - - // const loan = await sovryn.getLoan(loan_id); - // const principal = new BN(loan["principal"]); - // const collateral = new BN(loan["collateral"]); - // const loan_interest = await sovryn.getLoanInterestData(loan_id); - // console.log("principal: ", principal.toString()); - // console.log("collateral: ", collateral.toString()); - // console.log("loan_interest: ", loan_interest); - - // try to liquidate the still healthy position - await expectRevert(sovryn.liquidate(loan_id, lender, loan_token_sent, { from: liquidator }), "healthy position"); + // set the demand curve to set interest rates + set_demand_curve(loanToken); + const lender = accounts[0]; + const borrower = accounts[1]; + const liquidator = accounts[2]; + const loan_token_sent = new BN(10).mul(oneEth); + // lend to the pool, mint tokens if required, open a margin trade position + const loan_id = await prepare_liquidation( + lender, + borrower, + liquidator, + loan_token_sent, + loanToken, + underlyingToken, + collateralToken, + sovryn, + WRBTC + ); + + // const loan = await sovryn.getLoan(loan_id); + // const principal = new BN(loan["principal"]); + // const collateral = new BN(loan["collateral"]); + // const loan_interest = await sovryn.getLoanInterestData(loan_id); + // console.log("principal: ", principal.toString()); + // console.log("collateral: ", collateral.toString()); + // console.log("loan_interest: ", loan_interest); + + // try to liquidate the still healthy position + await expectRevert( + sovryn.liquidate(loan_id, lender, loan_token_sent, { from: liquidator }), + "healthy position" + ); }; // lend to the pool, mint tokens if required, open a margin trade position const prepare_liquidation = async ( - lender, - borrower, - liquidator, - loan_token_sent, - loanToken, - underlyingToken, - collateralToken, - sovryn, - WRBTC + lender, + borrower, + liquidator, + loan_token_sent, + loanToken, + underlyingToken, + collateralToken, + sovryn, + WRBTC ) => { - await underlyingToken.approve(loanToken.address, new BN(10).pow(new BN(40))); - let value; - if (WRBTC.address == underlyingToken.address) { - await loanToken.mintWithBTC(lender, false, { value: new BN(10).pow(new BN(21)) }); - value = loan_token_sent; - } else { - await loanToken.mint(lender, new BN(10).pow(new BN(21))); - await underlyingToken.mint(borrower, loan_token_sent); - await underlyingToken.mint(liquidator, loan_token_sent); - await underlyingToken.approve(loanToken.address, loan_token_sent, { from: borrower }); - await underlyingToken.approve(sovryn.address, loan_token_sent, { from: liquidator }); - value = 0; - } - const { receipt } = await loanToken.marginTrade( - "0x0", // loanId (0 for new loans) - new BN(2).mul(oneEth), // leverageAmount - loan_token_sent, // loanTokenSent - 0, // no collateral token sent - collateralToken.address, // collateralTokenAddress - borrower, // trader, - 0, - "0x", // loanDataBytes (only required with ether) - { from: borrower, value: value } - ); - const decode = decodeLogs(receipt.rawLogs, LoanOpenings, "Trade"); - return decode[0].args["loanId"]; + await underlyingToken.approve(loanToken.address, new BN(10).pow(new BN(40))); + let value; + if (WRBTC.address == underlyingToken.address) { + await loanToken.mintWithBTC(lender, false, { value: new BN(10).pow(new BN(21)) }); + value = loan_token_sent; + } else { + await loanToken.mint(lender, new BN(10).pow(new BN(21))); + await underlyingToken.mint(borrower, loan_token_sent); + await underlyingToken.mint(liquidator, loan_token_sent); + await underlyingToken.approve(loanToken.address, loan_token_sent, { from: borrower }); + await underlyingToken.approve(sovryn.address, loan_token_sent, { from: liquidator }); + value = 0; + } + const { receipt } = await loanToken.marginTrade( + "0x0", // loanId (0 for new loans) + new BN(2).mul(oneEth), // leverageAmount + loan_token_sent, // loanTokenSent + 0, // no collateral token sent + collateralToken.address, // collateralTokenAddress + borrower, // trader, + 0, + "0x", // loanDataBytes (only required with ether) + { from: borrower, value: value } + ); + const decode = decodeLogs(receipt.rawLogs, LoanOpenings, "Trade"); + return decode[0].args["loanId"]; }; /// @dev Compute the expected values and make sure the event contains them @@ -183,60 +195,71 @@ const prepare_liquidation = async ( /// the repayAmount to be equal to loan_token_sent when partially liquidating /// a position not completely unhealthy. const verify_liquidation_event = async ( - loan, - logs, - lender, - borrower, - liquidator, - loan_token_sent, - loanToken, - underlyingToken, - collateralToken, - sovryn, - priceFeeds, - checkRepayAmount = true + loan, + logs, + lender, + borrower, + liquidator, + loan_token_sent, + loanToken, + underlyingToken, + collateralToken, + sovryn, + priceFeeds, + checkRepayAmount = true ) => { - const loan_id = loan["loanId"]; - const collateral_ = new BN(loan["collateral"]); - const principal_ = new BN(loan["principal"]); - const res = await priceFeeds.getCurrentMargin(underlyingToken.address, collateralToken.address, principal_, collateral_); - const current_margin = res.currentMargin; - const collateral_to_loan_rate = res.collateralToLoanRate; - const liquidation_incentive_percent = new BN(await sovryn.liquidationIncentivePercent()); - const maintenance_margin = new BN(loan["maintenanceMargin"]); - const desired_margin = maintenance_margin.add(new BN(5).mul(oneEth)); - - let max_liquidatable = desired_margin.add(hunEth).mul(principal_).div(hunEth); - max_liquidatable = max_liquidatable.sub(collateral_.mul(collateral_to_loan_rate).div(oneEth)); - max_liquidatable = max_liquidatable.mul(hunEth).div(desired_margin.sub(liquidation_incentive_percent)); - max_liquidatable = max_liquidatable.gt(principal_) ? principal_ : max_liquidatable; - - let max_seizable = max_liquidatable.mul(liquidation_incentive_percent.add(hunEth)); - max_seizable = max_seizable.div(collateral_to_loan_rate).div(new BN(100)); - max_seizable = max_seizable.gt(collateral_) ? collateral_ : max_seizable; - const loan_close_amount = loan_token_sent.gt(max_liquidatable) ? max_liquidatable : loan_token_sent; - const collateral_withdraw_amount = max_seizable.mul(loan_close_amount).div(max_liquidatable); - - const decode = decodeLogs(logs, LoanClosingsEvents, "Liquidate"); - const liquidate_event = decode[0].args; - - expect(liquidate_event["user"] == borrower).to.be.true; - expect(liquidate_event["liquidator"] == liquidator).to.be.true; - expect(liquidate_event["loanId"] == loan_id).to.be.true; - expect(liquidate_event["lender"] == loanToken.address).to.be.true; - expect(liquidate_event["loanToken"] == underlyingToken.address).to.be.true; - expect(liquidate_event["collateralToken"] == collateralToken.address).to.be.true; - if (checkRepayAmount) { - /// @dev This check holds true just when the position is completely liquidated - expect(liquidate_event["repayAmount"] == loan_token_sent.toString()).to.be.true; - } - expect(liquidate_event["collateralWithdrawAmount"] == collateral_withdraw_amount.toString()).to.be.true; - expect(liquidate_event["collateralToLoanRate"] == collateral_to_loan_rate.toString()).to.be.true; - expect(liquidate_event["currentMargin"] == current_margin.toString()).to.be.true; + const loan_id = loan["loanId"]; + const collateral_ = new BN(loan["collateral"]); + const principal_ = new BN(loan["principal"]); + const res = await priceFeeds.getCurrentMargin( + underlyingToken.address, + collateralToken.address, + principal_, + collateral_ + ); + const current_margin = res.currentMargin; + const collateral_to_loan_rate = res.collateralToLoanRate; + const liquidation_incentive_percent = new BN(await sovryn.liquidationIncentivePercent()); + const maintenance_margin = new BN(loan["maintenanceMargin"]); + const desired_margin = maintenance_margin.add(new BN(5).mul(oneEth)); + + let max_liquidatable = desired_margin.add(hunEth).mul(principal_).div(hunEth); + max_liquidatable = max_liquidatable.sub(collateral_.mul(collateral_to_loan_rate).div(oneEth)); + max_liquidatable = max_liquidatable + .mul(hunEth) + .div(desired_margin.sub(liquidation_incentive_percent)); + max_liquidatable = max_liquidatable.gt(principal_) ? principal_ : max_liquidatable; + + let max_seizable = max_liquidatable.mul(liquidation_incentive_percent.add(hunEth)); + max_seizable = max_seizable.div(collateral_to_loan_rate).div(new BN(100)); + max_seizable = max_seizable.gt(collateral_) ? collateral_ : max_seizable; + const loan_close_amount = loan_token_sent.gt(max_liquidatable) + ? max_liquidatable + : loan_token_sent; + const collateral_withdraw_amount = max_seizable.mul(loan_close_amount).div(max_liquidatable); + + const decode = decodeLogs(logs, LoanClosingsEvents, "Liquidate"); + const liquidate_event = decode[0].args; + + expect(liquidate_event["user"] == borrower).to.be.true; + expect(liquidate_event["liquidator"] == liquidator).to.be.true; + expect(liquidate_event["loanId"] == loan_id).to.be.true; + expect(liquidate_event["lender"] == loanToken.address).to.be.true; + expect(liquidate_event["loanToken"] == underlyingToken.address).to.be.true; + expect(liquidate_event["collateralToken"] == collateralToken.address).to.be.true; + if (checkRepayAmount) { + /// @dev This check holds true just when the position is completely liquidated + expect(liquidate_event["repayAmount"] == loan_token_sent.toString()).to.be.true; + } + expect(liquidate_event["collateralWithdrawAmount"] == collateral_withdraw_amount.toString()).to + .be.true; + expect(liquidate_event["collateralToLoanRate"] == collateral_to_loan_rate.toString()).to.be + .true; + expect(liquidate_event["currentMargin"] == current_margin.toString()).to.be.true; }; module.exports = { - liquidate, - liquidate_healthy_position_should_fail, - prepare_liquidation, + liquidate, + liquidate_healthy_position_should_fail, + prepare_liquidation, }; diff --git a/tests/protocol_settings.js b/tests/protocol_settings.js index af0a8a986..cfe1565d8 100644 --- a/tests/protocol_settings.js +++ b/tests/protocol_settings.js @@ -17,21 +17,21 @@ const { loadFixture } = waffle; const { BN, constants, expectRevert } = require("@openzeppelin/test-helpers"); const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getSOV, - getLoanTokenLogic, - getLoanTokenLogicWrbtc, - getLoanToken, - getLoanTokenWRBTC, - loan_pool_setup, - getPriceFeeds, - getSovryn, - lend_to_pool, - set_demand_curve, - open_margin_trade_position, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getSOV, + getLoanTokenLogic, + getLoanTokenLogicWrbtc, + getLoanToken, + getLoanTokenWRBTC, + loan_pool_setup, + getPriceFeeds, + getSovryn, + lend_to_pool, + set_demand_curve, + open_margin_trade_position, } = require("./Utils/initializer.js"); const sovrynProtocol = artifacts.require("sovrynProtocol"); @@ -45,127 +45,163 @@ const TOTAL_SUPPLY = etherMantissa(1000000000); const wei = web3.utils.toWei; contract("Affliates", (accounts) => { - let sovryn; - - async function deploymentAndInitFixture(_wallets, _provider) { - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - - await sovryn.setSovrynProtocolAddress(sovryn.address); - } - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - it("Saves Sovryn Proxy Address correctly", async () => { - assert.equal(await sovryn.getProtocolAddress(), sovryn.address); - // assert.equal(loanTokenAddress, doc.address, "Doc address not set yet"); - }); - - // Should successfully set the sov token address - it("Test set sov token address", async () => { - const sov = await TestToken.new("Sovryn", "SOV", 18, new BN(10).pow(new BN(50))); - - // Should revert if set with non owner - await expectRevert(sovryn.setSOVTokenAddress(constants.ZERO_ADDRESS, { from: accounts[1] }), "unauthorized"); - await expectRevert(sovryn.setSOVTokenAddress(constants.ZERO_ADDRESS), "newSovTokenAddress not a contract"); - - await sovryn.setSOVTokenAddress(sov.address); - expect((await sovryn.getSovTokenAddress()) == sov.address).to.be.true; - }); - - // Should successfully set the locked SOV address - it("Test set sov token address", async () => { - const sov = await TestToken.new("Sovryn", "SOV", 18, new BN(10).pow(new BN(50))); - const lockedSOV = await LockedSOV.new(sov.address, [accounts[0]]); - - // Should revert if set with non owner - await expectRevert(sovryn.setLockedSOVAddress(constants.ZERO_ADDRESS, { from: accounts[1] }), "unauthorized"); - await expectRevert(sovryn.setLockedSOVAddress(constants.ZERO_ADDRESS), "newLockSOVAddress not a contract"); - - await sovryn.setLockedSOVAddress(lockedSOV.address); - expect((await sovryn.getLockedSOVAddress()) == lockedSOV.address).to.be.true; - }); - - // Should successfully set the Affiliate Fee Percent - it("Test set affiliate fee percent", async () => { - const affiliateFeePercent = web3.utils.toWei("20", "ether"); - const invalidAffiliateFeePercent = web3.utils.toWei("101", "ether"); - // Should revert if set with non owner - await expectRevert(sovryn.setAffiliateFeePercent(affiliateFeePercent, { from: accounts[1] }), "unauthorized"); - // Should revert if value too high - await expectRevert(sovryn.setAffiliateFeePercent(invalidAffiliateFeePercent), "value too high"); - - await sovryn.setAffiliateFeePercent(affiliateFeePercent); - expect((await sovryn.affiliateFeePercent()).toString() == affiliateFeePercent).to.be.true; - }); - - // Should successfully set the Affiliate Trading Token Fee Percent - it("Test set affiliate fee percent", async () => { - // Should revert if set with non owner - const affiliateTradingTokenFeePercent = web3.utils.toWei("20", "ether"); - const invalidAffiliateTradingTokenFeePercent = web3.utils.toWei("101", "ether"); - // Should revert if set with non owner - await expectRevert( - sovryn.setAffiliateTradingTokenFeePercent(affiliateTradingTokenFeePercent, { from: accounts[1] }), - "unauthorized" - ); - // Should revert if value too high - await expectRevert(sovryn.setAffiliateTradingTokenFeePercent(invalidAffiliateTradingTokenFeePercent), "value too high"); - - await sovryn.setAffiliateTradingTokenFeePercent(affiliateTradingTokenFeePercent); - expect((await sovryn.affiliateTradingTokenFeePercent()).toString() == affiliateTradingTokenFeePercent).to.be.true; - }); - - it("Test set trading rebate rewards basis point with invalid value", async () => { - // Should revert if set with invalid value (more than the max basis point value 9999) - await expectRevert(sovryn.setTradingRebateRewardsBasisPoint(10000), "value too high"); - }); - - it("Test set trading rebate rewards basis point with max value", async () => { - const maxBasisPoint = 9999; - await sovryn.setTradingRebateRewardsBasisPoint(maxBasisPoint); - expect((await sovryn.getTradingRebateRewardsBasisPoint()).toString()).to.be.equal(new BN(maxBasisPoint).toString()); - }); - - it("Check dedicated SOV calculation", async () => { - let protocol = await sovrynProtocol.new(); - const protocolSettings = await ProtocolSettings.new(); - const dedicatedSOVAmount = new BN(wei("1", "wei")); - const SOVToken = await TestToken.new("SOV", "SOV", 18, TOTAL_SUPPLY); - - await protocol.replaceContract(protocolSettings.address); - protocol = await ProtocolSettings.at(protocol.address); - await protocol.setSOVTokenAddress(SOVToken.address); - - await set_fee_tokens_held(protocol, SOVToken, new BN(100), new BN(200), new BN(300)); - - expect((await protocol.getDedicatedSOVRebate()).toString()).to.equal(new BN(0).toString()); - - await SOVToken.transfer(protocol.address, new BN(0)); - - expect((await protocol.getDedicatedSOVRebate()).toString()).to.equal(new BN(0).toString()); - - await SOVToken.transfer(protocol.address, dedicatedSOVAmount); - - expect((await protocol.getDedicatedSOVRebate()).toString()).to.equal(new BN(1).toString()); - }); - - // Should successfully set the RolloverFlex Fee Percent - it("Test set RolloverFlexFeePercent", async () => { - const newRolloverFlexFeePercent = web3.utils.toWei("1", "ether"); - const invalidRolloverFlexFeePercent = web3.utils.toWei("2", "ether"); - // Should revert if set with non owner - await expectRevert(sovryn.setRolloverFlexFeePercent(newRolloverFlexFeePercent, { from: accounts[1] }), "unauthorized"); - // Should revert if value too high - await expectRevert(sovryn.setRolloverFlexFeePercent(invalidRolloverFlexFeePercent), "value too high"); - - await sovryn.setRolloverFlexFeePercent(newRolloverFlexFeePercent); - expect((await sovryn.rolloverFlexFeePercent()).toString()).to.equal(newRolloverFlexFeePercent); - }); + let sovryn; + + async function deploymentAndInitFixture(_wallets, _provider) { + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + + await sovryn.setSovrynProtocolAddress(sovryn.address); + } + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + it("Saves Sovryn Proxy Address correctly", async () => { + assert.equal(await sovryn.getProtocolAddress(), sovryn.address); + // assert.equal(loanTokenAddress, doc.address, "Doc address not set yet"); + }); + + // Should successfully set the sov token address + it("Test set sov token address", async () => { + const sov = await TestToken.new("Sovryn", "SOV", 18, new BN(10).pow(new BN(50))); + + // Should revert if set with non owner + await expectRevert( + sovryn.setSOVTokenAddress(constants.ZERO_ADDRESS, { from: accounts[1] }), + "unauthorized" + ); + await expectRevert( + sovryn.setSOVTokenAddress(constants.ZERO_ADDRESS), + "newSovTokenAddress not a contract" + ); + + await sovryn.setSOVTokenAddress(sov.address); + expect((await sovryn.getSovTokenAddress()) == sov.address).to.be.true; + }); + + // Should successfully set the locked SOV address + it("Test set sov token address", async () => { + const sov = await TestToken.new("Sovryn", "SOV", 18, new BN(10).pow(new BN(50))); + const lockedSOV = await LockedSOV.new(sov.address, [accounts[0]]); + + // Should revert if set with non owner + await expectRevert( + sovryn.setLockedSOVAddress(constants.ZERO_ADDRESS, { from: accounts[1] }), + "unauthorized" + ); + await expectRevert( + sovryn.setLockedSOVAddress(constants.ZERO_ADDRESS), + "newLockSOVAddress not a contract" + ); + + await sovryn.setLockedSOVAddress(lockedSOV.address); + expect((await sovryn.getLockedSOVAddress()) == lockedSOV.address).to.be.true; + }); + + // Should successfully set the Affiliate Fee Percent + it("Test set affiliate fee percent", async () => { + const affiliateFeePercent = web3.utils.toWei("20", "ether"); + const invalidAffiliateFeePercent = web3.utils.toWei("101", "ether"); + // Should revert if set with non owner + await expectRevert( + sovryn.setAffiliateFeePercent(affiliateFeePercent, { from: accounts[1] }), + "unauthorized" + ); + // Should revert if value too high + await expectRevert( + sovryn.setAffiliateFeePercent(invalidAffiliateFeePercent), + "value too high" + ); + + await sovryn.setAffiliateFeePercent(affiliateFeePercent); + expect((await sovryn.affiliateFeePercent()).toString() == affiliateFeePercent).to.be.true; + }); + + // Should successfully set the Affiliate Trading Token Fee Percent + it("Test set affiliate fee percent", async () => { + // Should revert if set with non owner + const affiliateTradingTokenFeePercent = web3.utils.toWei("20", "ether"); + const invalidAffiliateTradingTokenFeePercent = web3.utils.toWei("101", "ether"); + // Should revert if set with non owner + await expectRevert( + sovryn.setAffiliateTradingTokenFeePercent(affiliateTradingTokenFeePercent, { + from: accounts[1], + }), + "unauthorized" + ); + // Should revert if value too high + await expectRevert( + sovryn.setAffiliateTradingTokenFeePercent(invalidAffiliateTradingTokenFeePercent), + "value too high" + ); + + await sovryn.setAffiliateTradingTokenFeePercent(affiliateTradingTokenFeePercent); + expect( + (await sovryn.affiliateTradingTokenFeePercent()).toString() == + affiliateTradingTokenFeePercent + ).to.be.true; + }); + + it("Test set trading rebate rewards basis point with invalid value", async () => { + // Should revert if set with invalid value (more than the max basis point value 9999) + await expectRevert(sovryn.setTradingRebateRewardsBasisPoint(10000), "value too high"); + }); + + it("Test set trading rebate rewards basis point with max value", async () => { + const maxBasisPoint = 9999; + await sovryn.setTradingRebateRewardsBasisPoint(maxBasisPoint); + expect((await sovryn.getTradingRebateRewardsBasisPoint()).toString()).to.be.equal( + new BN(maxBasisPoint).toString() + ); + }); + + it("Check dedicated SOV calculation", async () => { + let protocol = await sovrynProtocol.new(); + const protocolSettings = await ProtocolSettings.new(); + const dedicatedSOVAmount = new BN(wei("1", "wei")); + const SOVToken = await TestToken.new("SOV", "SOV", 18, TOTAL_SUPPLY); + + await protocol.replaceContract(protocolSettings.address); + protocol = await ProtocolSettings.at(protocol.address); + await protocol.setSOVTokenAddress(SOVToken.address); + + await set_fee_tokens_held(protocol, SOVToken, new BN(100), new BN(200), new BN(300)); + + expect((await protocol.getDedicatedSOVRebate()).toString()).to.equal(new BN(0).toString()); + + await SOVToken.transfer(protocol.address, new BN(0)); + + expect((await protocol.getDedicatedSOVRebate()).toString()).to.equal(new BN(0).toString()); + + await SOVToken.transfer(protocol.address, dedicatedSOVAmount); + + expect((await protocol.getDedicatedSOVRebate()).toString()).to.equal(new BN(1).toString()); + }); + + // Should successfully set the RolloverFlex Fee Percent + it("Test set RolloverFlexFeePercent", async () => { + const newRolloverFlexFeePercent = web3.utils.toWei("1", "ether"); + const invalidRolloverFlexFeePercent = web3.utils.toWei("2", "ether"); + // Should revert if set with non owner + await expectRevert( + sovryn.setRolloverFlexFeePercent(newRolloverFlexFeePercent, { from: accounts[1] }), + "unauthorized" + ); + // Should revert if value too high + await expectRevert( + sovryn.setRolloverFlexFeePercent(invalidRolloverFlexFeePercent), + "value too high" + ); + + await sovryn.setRolloverFlexFeePercent(newRolloverFlexFeePercent); + expect((await sovryn.rolloverFlexFeePercent()).toString()).to.equal( + newRolloverFlexFeePercent + ); + }); }); diff --git a/tests/proxy/Proxy.js b/tests/proxy/Proxy.js index b44c2590a..08e1ba1f5 100644 --- a/tests/proxy/Proxy.js +++ b/tests/proxy/Proxy.js @@ -11,7 +11,14 @@ const { expect } = require("chai"); const { waffle } = require("hardhat"); const { loadFixture } = waffle; -const { expectRevert, expectEvent, constants, BN, balance, time } = require("@openzeppelin/test-helpers"); +const { + expectRevert, + expectEvent, + constants, + BN, + balance, + time, +} = require("@openzeppelin/test-helpers"); const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; @@ -19,112 +26,127 @@ const Proxy = artifacts.require("ProxyMockup"); const Implementation = artifacts.require("ImplementationMockup"); contract("Proxy", (accounts) => { - let account1; - let accountOwner; - - let proxy; - let implementation; - - async function deploymentAndInitFixture(_wallets, _provider) { - proxy = await Proxy.new({ from: accountOwner }); - implementation = await Implementation.new({ from: accountOwner }); - } - - before(async () => { - account1 = accounts[0]; - accountOwner = accounts[9]; - }); - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - describe("setProxyOwner", async () => { - it("Should be able to transfer ownership", async () => { - let tx = await proxy.setProxyOwner(account1, { from: accountOwner }); - expectEvent(tx, "OwnershipTransferred", { - _oldOwner: accountOwner, - _newOwner: account1, - }); - - let owner = await proxy.getProxyOwner.call(); - expect(owner).to.be.equal(account1); - }); - - it("Only owner should be able to transfer ownership", async () => { - await expectRevert(proxy.setProxyOwner(account1, { from: account1 }), "Proxy:: access denied"); - }); - - it("Should not be able to set proxy owner to zero address", async () => { - await expectRevert(proxy.setProxyOwner(ZERO_ADDRESS, { from: accountOwner }), "Proxy::setProxyOwner: invalid address"); - }); - }); - - describe("setImplementation", async () => { - it("Should be able to set implementation", async () => { - let tx = await proxy.setImplementation(implementation.address, { from: accountOwner }); - expectEvent(tx, "ImplementationChanged", { - _oldImplementation: ZERO_ADDRESS, - _newImplementation: implementation.address, - }); - - let returnedImplementation = await proxy.getImplementation.call(); - expect(returnedImplementation).to.be.equal(implementation.address); - }); - - it("Only owner should be able to set implementation", async () => { - await expectRevert(proxy.setImplementation(implementation.address, { from: account1 }), "Proxy:: access denied"); - }); - - it("Should not be able to set implementation to zero address", async () => { - await expectRevert(proxy.setImplementation(ZERO_ADDRESS, { from: accountOwner }), "Proxy::setImplementation: invalid address"); - }); - }); - - describe("invoke an implementation", async () => { - let value = "12345"; - - it("Should be able invoke method of the implementation", async () => { - await proxy.setImplementation(implementation.address, { from: accountOwner }); - proxy = await Implementation.at(proxy.address); - - let tx = await proxy.setValue(value, { from: accountOwner }); - expectEvent(tx, "ValueChanged", { - value: value, - }); - - let savedValue = await proxy.getValue.call(); - expect(savedValue.toString()).to.be.equal(value); - }); - - it("Storage data should be the same after an upgrade", async () => { - /// @dev For some reason (probably proxy conflict w/ waffle fixtures) - /// resuming the fixture snapshot is not enough to run this test successfully - /// It is additionaly required a proxy redeployment - proxy = await Proxy.new({ from: accountOwner }); - await proxy.setImplementation(implementation.address, { from: accountOwner }); - proxy = await Implementation.at(proxy.address); - - await proxy.setValue(value, { from: accountOwner }); - - let implementationNew = await Implementation.new({ from: accountOwner }); - proxy = await Proxy.at(proxy.address); - await proxy.setImplementation(implementationNew.address, { from: accountOwner }); - proxy = await Implementation.at(proxy.address); - - let savedValue = await proxy.getValue.call(); - expect(savedValue.toString()).to.be.equal(value); - }); - - it("Should not be able to invoke not set implementation", async () => { - /// @dev For some reason (probably proxy conflict w/ waffle fixtures) - /// resuming the fixture snapshot is not enough to run this test successfully - /// It is additionaly required a proxy redeployment - proxy = await Proxy.new({ from: accountOwner }); - await Implementation.new({ from: accountOwner }); - proxy = await Implementation.at(proxy.address); - - await expectRevert(proxy.setValue(456, { from: accountOwner }), "Proxy::(): implementation not found"); - }); - }); + let account1; + let accountOwner; + + let proxy; + let implementation; + + async function deploymentAndInitFixture(_wallets, _provider) { + proxy = await Proxy.new({ from: accountOwner }); + implementation = await Implementation.new({ from: accountOwner }); + } + + before(async () => { + account1 = accounts[0]; + accountOwner = accounts[9]; + }); + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + describe("setProxyOwner", async () => { + it("Should be able to transfer ownership", async () => { + let tx = await proxy.setProxyOwner(account1, { from: accountOwner }); + expectEvent(tx, "OwnershipTransferred", { + _oldOwner: accountOwner, + _newOwner: account1, + }); + + let owner = await proxy.getProxyOwner.call(); + expect(owner).to.be.equal(account1); + }); + + it("Only owner should be able to transfer ownership", async () => { + await expectRevert( + proxy.setProxyOwner(account1, { from: account1 }), + "Proxy:: access denied" + ); + }); + + it("Should not be able to set proxy owner to zero address", async () => { + await expectRevert( + proxy.setProxyOwner(ZERO_ADDRESS, { from: accountOwner }), + "Proxy::setProxyOwner: invalid address" + ); + }); + }); + + describe("setImplementation", async () => { + it("Should be able to set implementation", async () => { + let tx = await proxy.setImplementation(implementation.address, { from: accountOwner }); + expectEvent(tx, "ImplementationChanged", { + _oldImplementation: ZERO_ADDRESS, + _newImplementation: implementation.address, + }); + + let returnedImplementation = await proxy.getImplementation.call(); + expect(returnedImplementation).to.be.equal(implementation.address); + }); + + it("Only owner should be able to set implementation", async () => { + await expectRevert( + proxy.setImplementation(implementation.address, { from: account1 }), + "Proxy:: access denied" + ); + }); + + it("Should not be able to set implementation to zero address", async () => { + await expectRevert( + proxy.setImplementation(ZERO_ADDRESS, { from: accountOwner }), + "Proxy::setImplementation: invalid address" + ); + }); + }); + + describe("invoke an implementation", async () => { + let value = "12345"; + + it("Should be able invoke method of the implementation", async () => { + await proxy.setImplementation(implementation.address, { from: accountOwner }); + proxy = await Implementation.at(proxy.address); + + let tx = await proxy.setValue(value, { from: accountOwner }); + expectEvent(tx, "ValueChanged", { + value: value, + }); + + let savedValue = await proxy.getValue.call(); + expect(savedValue.toString()).to.be.equal(value); + }); + + it("Storage data should be the same after an upgrade", async () => { + /// @dev For some reason (probably proxy conflict w/ waffle fixtures) + /// resuming the fixture snapshot is not enough to run this test successfully + /// It is additionaly required a proxy redeployment + proxy = await Proxy.new({ from: accountOwner }); + await proxy.setImplementation(implementation.address, { from: accountOwner }); + proxy = await Implementation.at(proxy.address); + + await proxy.setValue(value, { from: accountOwner }); + + let implementationNew = await Implementation.new({ from: accountOwner }); + proxy = await Proxy.at(proxy.address); + await proxy.setImplementation(implementationNew.address, { from: accountOwner }); + proxy = await Implementation.at(proxy.address); + + let savedValue = await proxy.getValue.call(); + expect(savedValue.toString()).to.be.equal(value); + }); + + it("Should not be able to invoke not set implementation", async () => { + /// @dev For some reason (probably proxy conflict w/ waffle fixtures) + /// resuming the fixture snapshot is not enough to run this test successfully + /// It is additionaly required a proxy redeployment + proxy = await Proxy.new({ from: accountOwner }); + await Implementation.new({ from: accountOwner }); + proxy = await Implementation.at(proxy.address); + + await expectRevert( + proxy.setValue(456, { from: accountOwner }), + "Proxy::(): implementation not found" + ); + }); + }); }); diff --git a/tests/staking/EnumerableBytes32Set.test.js b/tests/staking/EnumerableBytes32Set.test.js index 36baf9c46..2dca688ed 100644 --- a/tests/staking/EnumerableBytes32Set.test.js +++ b/tests/staking/EnumerableBytes32Set.test.js @@ -5,75 +5,92 @@ const { ZERO_ADDRESS } = constants; const TestCoverage = artifacts.require("TestCoverage"); contract("EnumerableBytes32Set", (accounts) => { - before(async () => { - testCoverage = await TestCoverage.new(); - }); + before(async () => { + testCoverage = await TestCoverage.new(); + }); - describe("EnumerableBytes32Set edge cases", () => { - it("Add and remove bytes32 w/ same value", async () => { - let result = await testCoverage.testEnum_AddRemove.call( - "0x7465737400000000000000000000000000000000000000000000000000000000", - "0x7465737400000000000000000000000000000000000000000000000000000000" - ); - // console.log("result", result); - assert(result == true); - }); + describe("EnumerableBytes32Set edge cases", () => { + it("Add and remove bytes32 w/ same value", async () => { + let result = await testCoverage.testEnum_AddRemove.call( + "0x7465737400000000000000000000000000000000000000000000000000000000", + "0x7465737400000000000000000000000000000000000000000000000000000000" + ); + // console.log("result", result); + assert(result == true); + }); - it("Add and remove bytes32 w/ different value", async () => { - let result = await testCoverage.testEnum_AddRemove.call( - "0x7465737400000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000" - ); - // console.log("result", result); - assert(result == false); - }); + it("Add and remove bytes32 w/ different value", async () => { + let result = await testCoverage.testEnum_AddRemove.call( + "0x7465737400000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000" + ); + // console.log("result", result); + assert(result == false); + }); - it("Add and check address", async () => { - let result = await testCoverage.testEnum_AddAddress.call(testCoverage.address, testCoverage.address); - // console.log("result", result); - assert(result == true); - }); + it("Add and check address", async () => { + let result = await testCoverage.testEnum_AddAddress.call( + testCoverage.address, + testCoverage.address + ); + // console.log("result", result); + assert(result == true); + }); - it("Add and check different address", async () => { - let result = await testCoverage.testEnum_AddAddress.call(testCoverage.address, ZERO_ADDRESS); - // console.log("result", result); - assert(result == false); - }); + it("Add and check different address", async () => { + let result = await testCoverage.testEnum_AddAddress.call( + testCoverage.address, + ZERO_ADDRESS + ); + // console.log("result", result); + assert(result == false); + }); - it("should revert enumerate when end < start", async () => { - let MAX_INT = new BN("115792089237316195423570985008687907853269984665640564039457584007913129639935"); - await expectRevert( - testCoverage.testEnum_AddAddressesAndEnumerate(testCoverage.address, ZERO_ADDRESS, MAX_INT, new BN(1)), - "addition overflow" - ); - }); + it("should revert enumerate when end < start", async () => { + let MAX_INT = new BN( + "115792089237316195423570985008687907853269984665640564039457584007913129639935" + ); + await expectRevert( + testCoverage.testEnum_AddAddressesAndEnumerate( + testCoverage.address, + ZERO_ADDRESS, + MAX_INT, + new BN(1) + ), + "addition overflow" + ); + }); - it("enumerate should return void when end == 0", async () => { - let result = await testCoverage.testEnum_AddAddressesAndEnumerate.call( - testCoverage.address, - ZERO_ADDRESS, - new BN(0), - new BN(0) - ); - // console.log("result", result); - assert.equal(result.length, 0, "Result is not void"); - }); + it("enumerate should return void when end == 0", async () => { + let result = await testCoverage.testEnum_AddAddressesAndEnumerate.call( + testCoverage.address, + ZERO_ADDRESS, + new BN(0), + new BN(0) + ); + // console.log("result", result); + assert.equal(result.length, 0, "Result is not void"); + }); - it("Add several addresses and enumerate them", async () => { - let result = await testCoverage.testEnum_AddAddressesAndEnumerate.call( - testCoverage.address, - ZERO_ADDRESS, - new BN(0), - new BN(10) - ); - // console.log("result", result); - /// @dev the output from contract has to be sliced to be compared as a String. Besides, it comes lowercased. - assert.equal( - "0x" + result[0].toString().match(/.{40}$/)[0], - testCoverage.address.toLowerCase(), - "The 1st Address does not match." - ); - assert.equal("0x" + result[1].toString().match(/.{40}$/)[0], ZERO_ADDRESS.toLowerCase(), "The 2nd Address does not match."); - }); - }); + it("Add several addresses and enumerate them", async () => { + let result = await testCoverage.testEnum_AddAddressesAndEnumerate.call( + testCoverage.address, + ZERO_ADDRESS, + new BN(0), + new BN(10) + ); + // console.log("result", result); + /// @dev the output from contract has to be sliced to be compared as a String. Besides, it comes lowercased. + assert.equal( + "0x" + result[0].toString().match(/.{40}$/)[0], + testCoverage.address.toLowerCase(), + "The 1st Address does not match." + ); + assert.equal( + "0x" + result[1].toString().match(/.{40}$/)[0], + ZERO_ADDRESS.toLowerCase(), + "The 2nd Address does not match." + ); + }); + }); }); diff --git a/tests/staking/ExtendedStakingTest.js b/tests/staking/ExtendedStakingTest.js index 91cb5b199..ba1c6f2c3 100644 --- a/tests/staking/ExtendedStakingTest.js +++ b/tests/staking/ExtendedStakingTest.js @@ -20,33 +20,33 @@ const { waffle } = require("hardhat"); const { loadFixture } = waffle; const { expectRevert, expectEvent, BN, time } = require("@openzeppelin/test-helpers"); const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getLoanTokenLogic, - getLoanToken, - getLoanTokenLogicWrbtc, - getLoanTokenWRBTC, - loan_pool_setup, - set_demand_curve, - getPriceFeeds, - getSovryn, - decodeLogs, - getSOV, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getLoanTokenLogic, + getLoanToken, + getLoanTokenLogicWrbtc, + getLoanTokenWRBTC, + loan_pool_setup, + set_demand_curve, + getPriceFeeds, + getSovryn, + decodeLogs, + getSOV, } = require("../Utils/initializer.js"); const { - address, - minerStart, - minerStop, - unlockedAccount, - mineBlock, - etherMantissa, - etherUnsigned, - setTime, - increaseTime, - setNextBlockTimestamp, - advanceBlocks, + address, + minerStart, + minerStop, + unlockedAccount, + mineBlock, + etherMantissa, + etherUnsigned, + setTime, + increaseTime, + setNextBlockTimestamp, + advanceBlocks, } = require("../Utils/Ethereum"); const StakingProxy = artifacts.require("StakingProxy"); @@ -78,1159 +78,1315 @@ const DELAY = 86400 * 14; const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; contract("Staking", (accounts) => { - let root, account1, account2; - let token, SUSD, WRBTC, staking; - let sovryn; - let loanTokenLogic, loanToken; - let feeSharingProxy; - let kickoffTS, inOneWeek; - - async function deploymentAndInitFixture(_wallets, _provider) { - // Deploying sovrynProtocol w/ generic function from initializer.js - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - await sovryn.setSovrynProtocolAddress(sovryn.address); - - // Custom tokens - /// @dev This SOV token is not a SOV test token - /// but a full-fledged SOV token including functionality - /// like the approveAndCall method. - token = await SOV.new(TOTAL_SUPPLY); - - // Staking - let stakingLogic = await StakingMockup.new(token.address); - staking = await StakingProxy.new(token.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingMockup.at(staking.address); - - // Upgradable Vesting Registry - vestingRegistryLogic = await VestingRegistryLogic.new(); - vesting = await VestingRegistryProxy.new(); - await vesting.setImplementation(vestingRegistryLogic.address); - vesting = await VestingRegistryLogic.at(vesting.address); - - await staking.setVestingRegistry(vesting.address); - - // Loan token - loanTokenSettings = await LoanTokenSettings.new(); - loanTokenLogic = await LoanTokenLogic.new(); - loanToken = await LoanToken.new(root, loanTokenLogic.address, sovryn.address, WRBTC.address); - // await loanToken.initialize(SUSD.address, "iSUSD", "iSUSD"); - loanToken = await LoanTokenLogic.at(loanToken.address); - - await sovryn.setLoanPool([loanToken.address], [SUSD.address]); - - //FeeSharingProxy - let feeSharingLogic = await FeeSharingLogic.new(); - feeSharingProxyObj = await FeeSharingProxy.new(sovryn.address, staking.address); - await feeSharingProxyObj.setImplementation(feeSharingLogic.address); - feeSharingProxy = await FeeSharingLogic.at(feeSharingProxyObj.address); - await sovryn.setFeesController(feeSharingProxy.address); - await staking.setFeeSharing(feeSharingProxy.address); - - await token.transfer(account1, 1000); - await token.approve(staking.address, TOTAL_SUPPLY); - kickoffTS = await staking.kickoffTS.call(); - inOneWeek = kickoffTS.add(new BN(DELAY)); - } - - before(async () => { - [root, account1, account2, ...accounts] = accounts; - }); - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - describe("stake", () => { - it("Amount should be positive", async () => { - await expectRevert(staking.stake(0, inOneWeek, root, root), "amount needs to be bigger than 0"); - }); - - it("Amount should be approved", async () => { - await expectRevert(staking.stake(100, inOneWeek, root, root, { from: account1 }), "ERC20: transfer amount exceeds allowance"); - }); - - it("Staking period too short", async () => { - await expectRevert( - staking.stake(100, await getTimeFromKickoff(DAY), root, root), - "Staking::timestampToLockDate: staking period too short" - ); - }); - - it("Shouldn't be able to stake longer than max duration", async () => { - let amount = "100"; - let lockedTS = await getTimeFromKickoff(MAX_DURATION); - let tx = await staking.stake(amount, lockedTS, account1, account1); - - expectEvent(tx, "TokensStaked", { - staker: account1, - amount: amount, - lockedUntil: lockedTS, - totalStaked: amount, - }); - - expectEvent(tx, "DelegateChanged", { - delegator: account1, - fromDelegate: ZERO_ADDRESS, - toDelegate: account1, - }); - }); - - it("Sender should be used if zero addresses passed", async () => { - let amount = "100"; - let lockedTS = await getTimeFromKickoff(MAX_DURATION); - let tx = await staking.stake(amount, lockedTS, ZERO_ADDRESS, ZERO_ADDRESS); - - expectEvent(tx, "TokensStaked", { - staker: root, - amount: amount, - lockedUntil: lockedTS, - totalStaked: amount, - }); - - expectEvent(tx, "DelegateChanged", { - delegator: root, - fromDelegate: ZERO_ADDRESS, - toDelegate: root, - }); - }); - - it("Should be able to stake and delegate for yourself", async () => { - let amount = "100"; - let duration = TWO_WEEKS; - let lockedTS = await getTimeFromKickoff(duration); - - let stakingBalance = await token.balanceOf.call(staking.address); - expect(stakingBalance.toNumber()).to.be.equal(0); - let beforeBalance = await token.balanceOf.call(root); - - let tx = await staking.stake(amount, lockedTS, root, root); - - stakingBalance = await token.balanceOf.call(staking.address); - expect(stakingBalance.toString()).to.be.equal(amount); - let afterBalance = await token.balanceOf.call(root); - expect(beforeBalance.sub(afterBalance).toString()).to.be.equal(amount); - - // _writeUserCheckpoint - let numUserCheckpoints = await staking.numUserStakingCheckpoints.call(root, lockedTS); - expect(numUserCheckpoints.toNumber()).to.be.equal(1); - let checkpoint = await staking.userStakingCheckpoints.call(root, lockedTS, 0); - expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx.receipt.blockNumber); - expect(checkpoint.stake.toString()).to.be.equal(amount); - - // _increaseDailyStake - let numTotalStakingCheckpoints = await staking.numTotalStakingCheckpoints.call(lockedTS); - expect(numTotalStakingCheckpoints.toNumber()).to.be.equal(1); - checkpoint = await staking.totalStakingCheckpoints.call(lockedTS, 0); - expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx.receipt.blockNumber); - expect(checkpoint.stake.toString()).to.be.equal(amount); - - // _delegate - let delegator = await staking.delegates.call(root, lockedTS); - expect(delegator).to.be.equal(root); - - let numDelegateStakingCheckpoints = await staking.numDelegateStakingCheckpoints.call(root, lockedTS); - expect(numDelegateStakingCheckpoints.toNumber()).to.be.equal(1); - checkpoint = await staking.delegateStakingCheckpoints.call(root, lockedTS, 0); - expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx.receipt.blockNumber); - expect(checkpoint.stake.toString()).to.be.equal(amount); - - expectEvent(tx, "TokensStaked", { - staker: root, - amount: amount, - lockedUntil: lockedTS, - totalStaked: amount, - }); - - expectEvent(tx, "DelegateChanged", { - delegator: root, - fromDelegate: ZERO_ADDRESS, - toDelegate: root, - }); - }); - - it("Should be able to stake and delegate for another person", async () => { - let amount = "1000"; - let duration = new BN(TWO_WEEKS).mul(new BN(2)); - let lockedTS = await getTimeFromKickoff(duration); - - let tx = await staking.stake(amount, lockedTS, account1, account1); - - // _writeUserCheckpoint - let numUserCheckpoints = await staking.numUserStakingCheckpoints.call(account1, lockedTS); - expect(numUserCheckpoints.toNumber()).to.be.equal(1); - let checkpoint = await staking.userStakingCheckpoints.call(account1, lockedTS, 0); - expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx.receipt.blockNumber); - expect(checkpoint.stake.toString()).to.be.equal(amount); - - // _increaseDailyStake - let numTotalStakingCheckpoints = await staking.numTotalStakingCheckpoints.call(lockedTS); - expect(numTotalStakingCheckpoints.toNumber()).to.be.equal(1); - checkpoint = await staking.totalStakingCheckpoints.call(lockedTS, 0); - expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx.receipt.blockNumber); - expect(checkpoint.stake.toString()).to.be.equal(amount); - - // _delegate - let delegator = await staking.delegates.call(account1, lockedTS); - expect(delegator).to.be.equal(account1); - - let numDelegateStakingCheckpoints = await staking.numDelegateStakingCheckpoints.call(account1, lockedTS); - expect(numDelegateStakingCheckpoints.toNumber()).to.be.equal(1); - checkpoint = await staking.delegateStakingCheckpoints.call(account1, lockedTS, 0); - expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx.receipt.blockNumber); - expect(checkpoint.stake.toString()).to.be.equal(amount); - - expectEvent(tx, "TokensStaked", { - staker: account1, - amount: amount, - lockedUntil: lockedTS, - totalStaked: amount, - }); - - expectEvent(tx, "DelegateChanged", { - delegator: account1, - fromDelegate: ZERO_ADDRESS, - toDelegate: account1, - }); - }); - - it("Should be able to stake after withdrawing whole amount", async () => { - let amount = "1000"; - let duration = new BN(TWO_WEEKS).mul(new BN(2)); - let lockedTS = await getTimeFromKickoff(duration); - await staking.stake(amount, lockedTS, root, root); - - // await setTime(lockedTS); - setNextBlockTimestamp(lockedTS.toNumber()); - - let stakingBalance = await token.balanceOf.call(staking.address); - expect(stakingBalance.toString()).to.be.equal(amount); - - await staking.withdraw(amount, lockedTS, root); - - stakingBalance = await token.balanceOf.call(staking.address); - expect(stakingBalance.toNumber()).to.be.equal(0); - - // stake second time - lockedTS = await getTimeFromKickoff(duration * 2); - let tx = await staking.stake(amount * 2, lockedTS, root, root); - - stakingBalance = await token.balanceOf.call(staking.address); - expect(stakingBalance.toNumber()).to.be.equal(amount * 2); - - // _writeUserCheckpoint - let numUserCheckpoints = await staking.numUserStakingCheckpoints.call(root, lockedTS); - expect(numUserCheckpoints.toNumber()).to.be.equal(1); - let checkpoint = await staking.userStakingCheckpoints.call(root, lockedTS, 0); - expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx.receipt.blockNumber); - expect(checkpoint.stake.toNumber()).to.be.equal(amount * 2); - }); - - it("Should be able to stake after withdrawing amount partially", async () => { - let amount = "1000"; - let duration = new BN(TWO_WEEKS).mul(new BN(2)); - let lockedTS = await getTimeFromKickoff(duration); - - let bb = await token.balanceOf.call(root); - - await staking.stake(amount, lockedTS, root, root); - - // await setTime(lockedTS); - setNextBlockTimestamp(lockedTS.toNumber()); - blockTimestamp = (await ethers.provider.getBlock("latest")).timestamp; - - let stakingBalance = await token.balanceOf.call(staking.address); - expect(stakingBalance.toString()).to.be.equal(amount); - let beforeBalance = await token.balanceOf.call(root); - - await staking.withdraw(amount / 2, lockedTS, root); - - stakingBalance = await token.balanceOf.call(staking.address); - expect(stakingBalance.toNumber()).to.be.equal(amount / 2); - let afterBalance = await token.balanceOf.call(root); - - expect(afterBalance.sub(beforeBalance).toNumber()).to.be.equal(amount / 2); - - // increase stake - lockedTS = await getTimeFromKickoff(duration * 2); - let tx = await staking.stake(amount * 2.5, lockedTS, root, root); - - stakingBalance = await token.balanceOf.call(staking.address); - expect(stakingBalance.toNumber()).to.be.equal(amount * 3); - - // _writeUserCheckpoint - let numUserCheckpoints = await staking.numUserStakingCheckpoints.call(root, lockedTS); - expect(numUserCheckpoints.toNumber()).to.be.equal(1); - let checkpoint = await staking.userStakingCheckpoints.call(root, lockedTS, 0); - expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx.receipt.blockNumber); - expect(checkpoint.stake.toNumber()).to.be.equal(amount * 2.5); - }); - }); - - describe("stakeWithApproval", () => { - it("Should be able to stake and delegate for yourself", async () => { - let amount = "100"; - let duration = TWO_WEEKS; - let lockedTS = await getTimeFromKickoff(duration); - - let stakingBalance = await token.balanceOf.call(staking.address); - expect(stakingBalance.toNumber()).to.be.equal(0); - let beforeBalance = await token.balanceOf.call(root); - - await token.approve(staking.address, 0); - - // TODO - await token.approve(staking.address, amount * 2, { from: account1 }); - - let contract = new web3.eth.Contract(staking.abi, staking.address); - let sender = root; - let data = contract.methods.stakeWithApproval(sender, amount, lockedTS, root, root).encodeABI(); - // let data = contract.methods.stakeWithApproval(account1, amount * 2, lockedTS, root, root).encodeABI(); - let tx = await token.approveAndCall(staking.address, amount, data, { from: sender }); - - stakingBalance = await token.balanceOf.call(staking.address); - expect(stakingBalance.toString()).to.be.equal(amount); - let afterBalance = await token.balanceOf.call(root); - expect(beforeBalance.sub(afterBalance).toString()).to.be.equal(amount); - - // _writeUserCheckpoint - let numUserCheckpoints = await staking.numUserStakingCheckpoints.call(root, lockedTS); - expect(numUserCheckpoints.toNumber()).to.be.equal(1); - let checkpoint = await staking.userStakingCheckpoints.call(root, lockedTS, 0); - expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx.receipt.blockNumber); - expect(checkpoint.stake.toString()).to.be.equal(amount); - - // _increaseDailyStake - let numTotalStakingCheckpoints = await staking.numTotalStakingCheckpoints.call(lockedTS); - expect(numTotalStakingCheckpoints.toNumber()).to.be.equal(1); - checkpoint = await staking.totalStakingCheckpoints.call(lockedTS, 0); - expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx.receipt.blockNumber); - expect(checkpoint.stake.toString()).to.be.equal(amount); - - // _delegate - let delegator = await staking.delegates.call(root, lockedTS); - expect(delegator).to.be.equal(root); - - let numDelegateStakingCheckpoints = await staking.numDelegateStakingCheckpoints.call(root, lockedTS); - expect(numDelegateStakingCheckpoints.toNumber()).to.be.equal(1); - checkpoint = await staking.delegateStakingCheckpoints.call(root, lockedTS, 0); - expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx.receipt.blockNumber); - expect(checkpoint.stake.toString()).to.be.equal(amount); - }); - - it("fails if invoked directly", async () => { - let lockedTS = await getTimeFromKickoff(TWO_WEEKS); - await expectRevert(staking.stakeWithApproval(root, "100", lockedTS, root, root), "unauthorized"); - }); - - it("fails if wrong method passed in data", async () => { - let amount = "100"; - let lockedTS = await getTimeFromKickoff(TWO_WEEKS); - let contract = new web3.eth.Contract(staking.abi, staking.address); - let data = contract.methods.stake(amount, lockedTS, root, root).encodeABI(); - - await expectRevert(token.approveAndCall(staking.address, amount, data), "method is not allowed"); - }); - - it("fails if wrong sender passed in data", async () => { - let amount = "100"; - let lockedTS = await getTimeFromKickoff(TWO_WEEKS); - let contract = new web3.eth.Contract(staking.abi, staking.address); - - await token.approve(staking.address, amount * 2, { from: account1 }); - let sender = root; - let data = contract.methods.stakeWithApproval(account1, amount, lockedTS, root, root).encodeABI(); - - await expectRevert(token.approveAndCall(staking.address, amount, data, { from: sender }), "sender mismatch"); - }); - - it("fails if wrong amount passed in data", async () => { - let amount = "100"; - let lockedTS = await getTimeFromKickoff(TWO_WEEKS); - let contract = new web3.eth.Contract(staking.abi, staking.address); - - await token.approve(staking.address, amount * 2, { from: account1 }); - let sender = root; - let data = contract.methods.stakeWithApproval(sender, amount, lockedTS, root, root).encodeABI(); - - await expectRevert(token.approveAndCall(staking.address, amount * 2, data, { from: sender }), "amount mismatch"); - }); - }); - - describe("WeightedStaking", () => { - /// @dev On governance/Staking/WeightedStaking.sol the conditional: - /// if (userStakingCheckpoints[account][date][nCheckpoints - 1].fromBlock <= blockNumber) - /// is always met, because when a checkpoint is created it is always set the blocknumber - /// of the transaction ocurring. So current blockNumber is to be equal to the - /// blockNumber of the checkpoint if it has been created on the same block, - /// or bigger if it was created on a previous block. Only exception to this - /// would be to request the prior stake for a blockNumber lower than - /// the current one, i.e. an historical query. - it("Coverage for WeightedStaking::_getPriorUserStakeByDate", async () => { - let amount = new BN(1000); - let duration = new BN(TWO_WEEKS).mul(new BN(2)); - let lockedTS = await getTimeFromKickoff(duration); - - // 1st Stake - await staking.stake(amount, lockedTS, root, account1); - - // 2nd Stake - await staking.stake(amount, lockedTS, root, account1); - - // Remember the blocknumber of the second staking - let block = await web3.eth.getBlock("latest"); - - // Time travel, just enough to jump 1 block - await time.increase(1); - - // Check stake is there for the block when 2nd staking took place - let priorStake = await staking.getPriorUserStakeByDate.call(root, lockedTS, new BN(block.number)); - expect(priorStake).to.be.bignumber.equal(amount.mul(new BN(2))); - - // Check there is still stake for the block when 1st staking took place - priorStake = await staking.getPriorUserStakeByDate.call(root, lockedTS, new BN(block.number).sub(new BN(1))); - expect(priorStake).to.be.bignumber.equal(amount); - - // Check there is no stake for previous block to the block when staking took place - priorStake = await staking.getPriorUserStakeByDate.call(root, lockedTS, new BN(block.number).sub(new BN(2))); - expect(priorStake).to.be.bignumber.equal(new BN(0)); - }); - }); - - describe("extendStakingDuration", () => { - it("shouldn't extendStakingDuration when _getPriorUserStakeByDate == 0", async () => { - let duration = new BN(0); - let lockedTS = await getTimeFromKickoff(duration); - // console.log("lockedTS: ", lockedTS.toString()); - let newTime = await getTimeFromKickoff(TWO_WEEKS); - // console.log("newTime: ", newTime.toString()); - - // Trying to extend the stake when previous stake is 0 - await expectRevert(staking.extendStakingDuration(lockedTS, newTime), "nothing staked until the previous lock date"); - }); - - it("extend to a date inside the next 2 weeks granularity bucket", async () => { - let amount = "1000"; - let duration = new BN(TWO_WEEKS).mul(new BN(2)); - let lockedTS = await getTimeFromKickoff(duration); - // console.log("lockedTS: ", lockedTS.toString()); - - // Extending for 13 days - let newDuration = duration.add(new BN(DAY).mul(new BN(13))); - let newTime = await getTimeFromKickoff(newDuration); - console.log("newTime: ", newTime.toString()); - let newTimeLockDate = await staking.timestampToLockDate(newTime); - console.log("newTimeLockDate: ", newTimeLockDate.toString()); - - // Set delegate as account1 - await staking.stake(amount, lockedTS, root, account1); - - // Check the delegate of the stake - let delegate = await staking.delegates(root, lockedTS); - expect(delegate).equal(account1); - - // Extending the stake - await staking.extendStakingDuration(lockedTS, newTime); - - // Check the delegate of the extended stake - delegate = await staking.delegates(root, newTimeLockDate); - /// @dev A 13 days extension is setting delegate to address(0) - /// TODO: Should be fixed soon by contract upgrade. - /// When fixed, uncomment next line and test should be working ok. - // expect(delegate).equal(account1); - }); - - it("Cannot reduce the staking duration", async () => { - let amount = "1000"; - let duration = new BN(TWO_WEEKS).mul(new BN(2)); - let lockedTS = await getTimeFromKickoff(duration); - await staking.stake(amount, lockedTS, root, root); - - let newTime = await getTimeFromKickoff(TWO_WEEKS); - await expectRevert(staking.extendStakingDuration(lockedTS, newTime), "cannot reduce the staking duration"); - }); - - it("Do not exceed the max duration", async () => { - let amount = "1000"; - let duration = new BN(TWO_WEEKS).mul(new BN(2)); - let lockedTS = await getTimeFromKickoff(duration); - await staking.stake(amount, lockedTS, root, root); - - let newTime = await getTimeFromKickoff(MAX_DURATION.mul(new BN(2))); - let tx = await staking.extendStakingDuration(lockedTS, newTime); - - expectEvent(tx, "ExtendedStakingDuration", { - staker: root, - previousDate: lockedTS, - newDate: await getTimeFromKickoff(MAX_DURATION), - amountStaked: amount, - }); - }); - - it("Should be able to extend staking duration", async () => { - let amount = "1000"; - let lockedTS = await getTimeFromKickoff(TWO_WEEKS); - let tx1 = await staking.stake(amount, lockedTS, root, root); - - let stakingBalance = await token.balanceOf.call(staking.address); - expect(stakingBalance.toString()).to.be.equal(amount); - let beforeBalance = await token.balanceOf.call(root); - - expect(tx1.logs[2].args.lockedUntil.toNumber()).to.be.equal(lockedTS.toNumber()); - - let newLockedTS = await getTimeFromKickoff(TWO_WEEKS * 2); - let tx2 = await staking.extendStakingDuration(lockedTS, newLockedTS); - - stakingBalance = await token.balanceOf.call(staking.address); - expect(stakingBalance.toString()).to.be.equal(amount); - let afterBalance = await token.balanceOf.call(root); - expect(beforeBalance.sub(afterBalance).toNumber()).to.be.equal(0); - - // _decreaseDailyStake - let numTotalStakingCheckpoints = await staking.numTotalStakingCheckpoints.call(lockedTS); - expect(numTotalStakingCheckpoints.toNumber()).to.be.equal(2); - let checkpoint = await staking.totalStakingCheckpoints.call(lockedTS, 0); - expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx1.receipt.blockNumber); - expect(checkpoint.stake.toString()).to.be.equal(amount); - checkpoint = await staking.totalStakingCheckpoints.call(lockedTS, 1); - expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx2.receipt.blockNumber); - expect(checkpoint.stake.toString()).to.be.equal("0"); - - // _increaseDailyStake - numTotalStakingCheckpoints = await staking.numTotalStakingCheckpoints.call(newLockedTS); - expect(numTotalStakingCheckpoints.toNumber()).to.be.equal(1); - checkpoint = await staking.totalStakingCheckpoints.call(newLockedTS, 0); - expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx2.receipt.blockNumber); - expect(checkpoint.stake.toString()).to.be.equal(amount); - - // _writeUserCheckpoint - let numUserCheckpoints = await staking.numUserStakingCheckpoints.call(root, lockedTS); - expect(numUserCheckpoints.toNumber()).to.be.equal(2); - checkpoint = await staking.userStakingCheckpoints.call(root, lockedTS, 0); - expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx1.receipt.blockNumber); - expect(checkpoint.stake.toString()).to.be.equal(amount); - checkpoint = await staking.userStakingCheckpoints.call(root, newLockedTS, 0); - expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx2.receipt.blockNumber); - expect(checkpoint.stake.toString()).to.be.equal(amount); - - expectEvent(tx2, "ExtendedStakingDuration", { - staker: root, - previousDate: lockedTS, - newDate: newLockedTS, - amountStaked: amount, - }); - }); - - it("should update the vesting checkpoints if the stake is extended with a vesting contract", async () => { - //TODO if vesting contracts should ever support this function. - //currently, they don't and they are not upgradable. - }); - }); - - describe("increaseStake", () => { - it("stakesBySchedule w/ duration < = > MAX_DURATION", async () => { - let amount = "1000"; - let duration = new BN(MAX_DURATION).div(new BN(2)); - let cliff = new BN(TWO_WEEKS).mul(new BN(2)); - let intervalLength = new BN(10000000); - let lockTS = await getTimeFromKickoff(duration); - await staking.stakesBySchedule(amount, cliff, duration, intervalLength, root, root); - - // Check staking status for this staker - let rootStaked = await staking.getStakes(root); - // console.log("rootStaked['stakes']", rootStaked["stakes"].toString()); - let stakedDurationLowerThanMax = rootStaked["stakes"][0]; - - // Reset & duration = MAX - await loadFixture(deploymentAndInitFixture); - duration = new BN(MAX_DURATION); - await staking.stakesBySchedule(amount, cliff, duration, intervalLength, root, root); - - // Check staking status for this staker - rootStaked = await staking.getStakes(root); - // console.log("rootStaked['stakes']", rootStaked["stakes"].toString()); - let stakedDurationEqualToMax = rootStaked["stakes"][0]; - - // Reset & duration > MAX - await loadFixture(deploymentAndInitFixture); - duration = new BN(MAX_DURATION).mul(new BN(4)); - await staking.stakesBySchedule(amount, cliff, duration, intervalLength, root, root); - - // Check staking status for this staker - rootStaked = await staking.getStakes(root); - // console.log("rootStaked['stakes']", rootStaked["stakes"].toString()); - let stakedDurationHigherThanMax = rootStaked["stakes"][0]; - - /// @dev When duration = MAX or duration > MAX, contract deals w/ it as MAX - /// so the staked amount is higher when duration < MAX and equal when duration >= MAX - expect(stakedDurationLowerThanMax).to.be.bignumber.greaterThan(stakedDurationEqualToMax); - expect(stakedDurationEqualToMax).to.be.bignumber.equal(stakedDurationHigherThanMax); - }); - - it("Check getCurrentStakedUntil", async () => { - let amount = "1000"; - let duration = new BN(TWO_WEEKS).mul(new BN(2)); - let lockTS = await getTimeFromKickoff(duration); - - // Check staking status before staking - let totalStaked = await staking.getCurrentStakedUntil(lockTS); - // console.log("totalStaked", totalStaked.toString()); - expect(totalStaked).to.be.bignumber.equal(new BN(0)); - - await staking.stake(amount, lockTS, root, root); - - // Check staking status after staking - totalStaked = await staking.getCurrentStakedUntil(lockTS); - // console.log("totalStaked", totalStaked.toString()); - expect(totalStaked).to.be.bignumber.equal(amount); - }); - - it("Amount of tokens to stake needs to be bigger than 0", async () => { - let amount = "1000"; - let duration = new BN(TWO_WEEKS).mul(new BN(2)); - let lockTS = await getTimeFromKickoff(duration); - await staking.stake(amount, lockTS, root, root); - - await expectRevert(staking.stake("0", lockTS, root, root), "amount needs to be bigger than 0"); - }); - - it("Amount of tokens to stake needs to be bigger than 0", async () => { - let amount = "1000"; - let duration = new BN(TWO_WEEKS).mul(new BN(2)); - let lockTS = await getTimeFromKickoff(duration); - await staking.stake(amount, lockTS, root, root); - - await token.approve(staking.address, 0); - await expectRevert(staking.stake(amount, lockTS, root, root), "ERC20: transfer amount exceeds allowance"); - }); - - it("Shouldn't be able to overflow balance", async () => { - let amount = "1000"; - let duration = new BN(TWO_WEEKS).mul(new BN(2)); - let lockTS = await getTimeFromKickoff(duration); - await staking.stake(amount, lockTS, root, root); - - let maxValue = new BN(2).pow(new BN(96)).sub(new BN(1)); - await expectRevert(staking.stake(maxValue.sub(new BN(100)), lockTS, root, root), "overflow"); - }); - - it("Should be able to increase stake", async () => { - let amount = "1000"; - let duration = new BN(TWO_WEEKS).mul(new BN(2)); - let lockedTS = await getTimeFromKickoff(duration); - let tx1 = await staking.stake(amount, lockedTS, root, root); - - // check delegatee - let delegatee = await staking.delegates(root, lockedTS); - expect(delegatee).equal(root); - - let stakingBalance = await token.balanceOf.call(staking.address); - expect(stakingBalance.toString()).to.be.equal(amount); - let beforeBalance = await token.balanceOf.call(root); - - let tx2 = await staking.stake(amount * 2, lockedTS, root, account1); - - // check delegatee - delegatee = await staking.delegates(root, lockedTS); - expect(delegatee).equal(account1); - - stakingBalance = await token.balanceOf.call(staking.address); - expect(stakingBalance.toNumber()).to.be.equal(amount * 3); - let afterBalance = await token.balanceOf.call(root); - expect(beforeBalance.sub(afterBalance).toNumber()).to.be.equal(amount * 2); - - // _increaseDailyStake - let numTotalStakingCheckpoints = await staking.numTotalStakingCheckpoints.call(lockedTS); - expect(numTotalStakingCheckpoints.toNumber()).to.be.equal(2); - let checkpoint = await staking.totalStakingCheckpoints.call(lockedTS, 0); - expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx1.receipt.blockNumber); - expect(checkpoint.stake.toString()).to.be.equal(amount); - checkpoint = await staking.totalStakingCheckpoints.call(lockedTS, 1); - expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx2.receipt.blockNumber); - expect(checkpoint.stake.toNumber()).to.be.equal(amount * 3); - - // _writeUserCheckpoint - let numUserCheckpoints = await staking.numUserStakingCheckpoints.call(root, lockedTS); - expect(numUserCheckpoints.toNumber()).to.be.equal(2); - checkpoint = await staking.userStakingCheckpoints.call(root, lockedTS, 0); - expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx1.receipt.blockNumber); - expect(checkpoint.stake.toString()).to.be.equal(amount); - checkpoint = await staking.userStakingCheckpoints.call(root, lockedTS, 1); - expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx2.receipt.blockNumber); - expect(checkpoint.stake.toNumber()).to.be.equal(amount * 3); - - // delegateStakingCheckpoints - root - let numDelegateStakingCheckpoints = await staking.numDelegateStakingCheckpoints.call(root, lockedTS); - expect(numDelegateStakingCheckpoints.toNumber()).to.be.equal(2); - checkpoint = await staking.delegateStakingCheckpoints.call(root, lockedTS, 0); - expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx1.receipt.blockNumber); - expect(checkpoint.stake.toString()).to.be.equal(amount); - checkpoint = await staking.delegateStakingCheckpoints.call(root, lockedTS, 1); - expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx2.receipt.blockNumber); - expect(checkpoint.stake.toNumber()).to.be.equal(0); - - // delegateStakingCheckpoints - account1 - numDelegateStakingCheckpoints = await staking.numDelegateStakingCheckpoints.call(account1, lockedTS); - expect(numDelegateStakingCheckpoints.toNumber()).to.be.equal(1); - checkpoint = await staking.delegateStakingCheckpoints.call(account1, lockedTS, 0); - expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx2.receipt.blockNumber); - expect(checkpoint.stake.toNumber()).to.be.equal(amount * 3); - - expectEvent(tx2, "TokensStaked", { - staker: root, - amount: new BN(amount * 2), - lockedUntil: lockedTS, - totalStaked: new BN(amount * 3), - }); - }); - }); - - describe("getStakes", () => { - it("Should be able to increase stake", async () => { - let amount1 = "1000"; - let lockedTS1 = await getTimeFromKickoff(new BN(TWO_WEEKS)); - await staking.stake(amount1, lockedTS1, root, root); - - // time travel - await time.increase(TWO_WEEKS * 10); - - let amount2 = "5000"; - let lockedTS2 = await getTimeFromKickoff(new BN(MAX_DURATION)); - await staking.stake(amount2, lockedTS2, root, root); - - let data = await staking.getStakes.call(root); - - // for (let i = 0; i < data.dates.length; i++) { - - // } - - expect(data.dates[0]).to.be.bignumber.equal(new BN(lockedTS1)); - expect(data.stakes[0]).to.be.bignumber.equal(new BN(amount1)); - - expect(data.dates[1]).to.be.bignumber.equal(new BN(lockedTS2)); - expect(data.stakes[1]).to.be.bignumber.equal(new BN(amount2)); - }); - }); - - describe("setWeightScaling", () => { - it("Shouldn't be able to weight scaling less than min value", async () => { - await expectRevert(staking.setWeightScaling(0), "weight scaling doesn't belong to range [1, 9]"); - }); - - it("Shouldn't be able to weight scaling more than max value", async () => { - await expectRevert(staking.setWeightScaling(10), "weight scaling doesn't belong to range [1, 9]"); - }); - - it("Only owner should be able to weight scaling", async () => { - await expectRevert(staking.setWeightScaling(5, { from: account1 }), "unauthorized"); - }); - - it("Should be able to weight scaling", async () => { - await staking.setWeightScaling(7); - - expect(await staking.weightScaling.call()).to.be.bignumber.equal(new BN(7)); - }); - }); - - describe("withdraw", () => { - it("Amount of tokens to be withdrawn needs to be bigger than 0", async () => { - let amount = "1000"; - let duration = new BN(TWO_WEEKS).mul(new BN(2)); - let lockedTS = await getTimeFromKickoff(duration); - await staking.stake(amount, lockedTS, root, root); - - await expectRevert( - staking.withdraw("0", lockedTS, root), - "Staking::withdraw: amount of tokens to be withdrawn needs to be bigger than 0" - ); - }); - - it("Shouldn't be able to withdraw amount greater than balance", async () => { - let amount = 1000; - let duration = new BN(TWO_WEEKS).mul(new BN(2)); - let lockedTS = await getTimeFromKickoff(duration); - await staking.stake(amount, lockedTS, root, root); - - // await setTime(lockedTS); - setNextBlockTimestamp(lockedTS.toNumber()); - await expectRevert(staking.withdraw(amount * 2, lockedTS, root), "Staking::withdraw: not enough balance"); - }); - - it("Should be able to withdraw", async () => { - let amount = "1000"; - let duration = new BN(TWO_WEEKS).mul(new BN(2)); - let lockedTS = await getTimeFromKickoff(duration); - let tx1 = await staking.stake(amount, lockedTS, root, root); - - // await setTime(lockedTS); - setNextBlockTimestamp(lockedTS.toNumber()); - mineBlock(); - - let stakingBalance = await token.balanceOf.call(staking.address); - expect(stakingBalance.toString()).to.be.equal(amount); - let beforeBalance = await token.balanceOf.call(root); - - let tx2 = await staking.withdraw(amount / 2, lockedTS, root); - - stakingBalance = await token.balanceOf.call(staking.address); - expect(stakingBalance.toNumber()).to.be.equal(amount / 2); - let afterBalance = await token.balanceOf.call(root); - expect(afterBalance.sub(beforeBalance).toNumber()).to.be.equal(amount / 2); - - // _increaseDailyStake - let numTotalStakingCheckpoints = await staking.numTotalStakingCheckpoints.call(lockedTS); - expect(numTotalStakingCheckpoints.toNumber()).to.be.equal(2); - let checkpoint = await staking.totalStakingCheckpoints.call(lockedTS, 0); - expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx1.receipt.blockNumber); - expect(checkpoint.stake.toString()).to.be.equal(amount); - checkpoint = await staking.totalStakingCheckpoints.call(lockedTS, 1); - expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx2.receipt.blockNumber); - expect(checkpoint.stake.toNumber()).to.be.equal(amount / 2); - - // _writeUserCheckpoint - let numUserCheckpoints = await staking.numUserStakingCheckpoints.call(root, lockedTS); - expect(numUserCheckpoints.toNumber()).to.be.equal(2); - checkpoint = await staking.userStakingCheckpoints.call(root, lockedTS, 0); - expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx1.receipt.blockNumber); - expect(checkpoint.stake.toString()).to.be.equal(amount); - checkpoint = await staking.userStakingCheckpoints.call(root, lockedTS, 1); - expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx2.receipt.blockNumber); - expect(checkpoint.stake.toNumber()).to.be.equal(amount / 2); - - // _decreaseDelegateStake - let numDelegateStakingCheckpoints = await staking.numDelegateStakingCheckpoints.call(root, lockedTS); - checkpoint = await staking.delegateStakingCheckpoints.call(root, lockedTS, numDelegateStakingCheckpoints - 1); - expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx2.receipt.blockNumber); - expect(checkpoint.stake.toNumber()).to.be.equal(amount / 2); - expect(numDelegateStakingCheckpoints.toNumber()).to.be.equal(2); - - expectEvent(tx2, "StakingWithdrawn", { - staker: root, - amount: new BN(amount / 2), - }); - }); - - it("Should be able to withdraw second time", async () => { - let amount = "1000"; - let duration = new BN(TWO_WEEKS).mul(new BN(2)); - let lockedTS = await getTimeFromKickoff(duration); - await staking.stake(amount, lockedTS, root, root); - - let stakingBalance = await token.balanceOf.call(staking.address); - expect(stakingBalance.toString()).to.be.equal(amount); - - await staking.withdraw(amount / 2, lockedTS, account2); - - stakingBalance = await token.balanceOf.call(staking.address); - expect(stakingBalance.toNumber()).to.be.equal(amount / 2); - - // _decreaseDelegateStake - let numDelegateStakingCheckpoints = await staking.numDelegateStakingCheckpoints.call(root, lockedTS); - let checkpoint = await staking.delegateStakingCheckpoints.call(root, lockedTS, numDelegateStakingCheckpoints - 1); - expect(checkpoint.stake.toNumber()).to.be.equal(amount / 2); - expect(numDelegateStakingCheckpoints.toNumber()).to.be.equal(2); - - await staking.withdraw(amount / 2, lockedTS, account2); - - stakingBalance = await token.balanceOf.call(staking.address); - expect(stakingBalance.toNumber()).to.be.equal(0); - - // _decreaseDelegateStake - numDelegateStakingCheckpoints = await staking.numDelegateStakingCheckpoints.call(root, lockedTS); - checkpoint = await staking.delegateStakingCheckpoints.call(root, lockedTS, numDelegateStakingCheckpoints - 1); - expect(checkpoint.stake.toNumber()).to.be.equal(0); - expect(numDelegateStakingCheckpoints.toNumber()).to.be.equal(3); - - let feeSharingBalance = await token.balanceOf.call(feeSharingProxy.address); - let userBalance = await token.balanceOf.call(account2); - - let maxVotingWeight = await staking.MAX_VOTING_WEIGHT.call(); - let maxDuration = await staking.MAX_DURATION.call(); - let weightFactor = await staking.WEIGHT_FACTOR.call(); - let weight = weightingFunction(amount, duration, maxDuration, maxVotingWeight, weightFactor.toNumber()) / 100; - let weightScaling = await staking.weightScaling.call(); - weight = weight * weightScaling; - let punishedAmount = weight; - - await expect(feeSharingBalance).to.be.bignumber.equal(new BN(punishedAmount)); - await expect(userBalance).to.be.bignumber.equal(new BN(amount - punishedAmount)); - }); - - it("Should be able to withdraw second time", async () => { - let amount = "1000"; - let duration = new BN(TWO_WEEKS).mul(new BN(2)); - let lockedTS = await getTimeFromKickoff(duration); - await staking.stake(amount, lockedTS, root, root); - - await staking.withdraw(amount, lockedTS, root); - - await staking.stake(amount, lockedTS, root, root); - - await staking.withdraw(amount, lockedTS, root); - }); - - it("Should be able to withdraw second time after partial withdraw", async () => { - let amount = new BN(1000); - let duration = new BN(TWO_WEEKS).mul(new BN(2)); - let lockedTS = await getTimeFromKickoff(duration); - await staking.stake(amount, lockedTS, root, root); - - await staking.withdraw(amount.sub(new BN(1)), lockedTS, root); - - await staking.stake(amount, lockedTS, root, root); - - await staking.withdraw(amount.add(new BN(1)), lockedTS, root); - }); - - it("Should be able to withdraw second time (emulate issue with delegate checkpoint)", async () => { - let amount = "1000"; - let duration = new BN(TWO_WEEKS).mul(new BN(2)); - let lockedTS = await getTimeFromKickoff(duration); - await staking.stake(amount, lockedTS, root, root); - - await staking.withdraw(amount, lockedTS, root); - - await staking.stake(amount, lockedTS, root, root); - await staking.setDelegateStake(root, lockedTS, 0); - - await staking.withdraw(amount, lockedTS, root); - }); - - it("Should be able to extend stake after second stake (emulate issue with delegate checkpoint)", async () => { - let amount = "1000"; - let duration = new BN(TWO_WEEKS).mul(new BN(2)); - let lockedTS = await getTimeFromKickoff(duration); - await staking.stake(amount, lockedTS, root, root); - - await staking.withdraw(amount, lockedTS, root); - - await staking.stake(amount, lockedTS, root, root); - await staking.setDelegateStake(root, lockedTS, 0); - - let lockedTS2 = await getTimeFromKickoff(duration.mul(new BN(3))); - await staking.extendStakingDuration(lockedTS, lockedTS2); - }); - - it("Should be able to delegate stake after second stake (emulate issue with delegate checkpoint)", async () => { - let amount = "1000"; - let duration = new BN(TWO_WEEKS).mul(new BN(2)); - let lockedTS = await getTimeFromKickoff(duration); - await staking.stake(amount, lockedTS, root, root); - - await staking.withdraw(amount, lockedTS, root); - - await staking.stake(amount, lockedTS, root, root); - await staking.setDelegateStake(root, lockedTS, 0); - - await staking.delegate(account1, lockedTS); - }); - - it("Should be able to withdraw earlier for any lock date", async () => { - let amount = "10000"; - - for (let i = 1; i <= 78; i++) { - if (i !== 1 && i !== 78 && i % 10 !== 0) { - continue; - } - - // FeeSharingProxy - let feeSharingLogic = await FeeSharingLogic.new(); - feeSharingProxyObj = await FeeSharingProxy.new(sovryn.address, staking.address); - await feeSharingProxyObj.setImplementation(feeSharingLogic.address); - feeSharingProxy = await FeeSharingLogic.at(feeSharingProxyObj.address); - await sovryn.setFeesController(feeSharingProxy.address); - await staking.setFeeSharing(feeSharingProxy.address); - - let duration = new BN(i * TWO_WEEKS); - let lockedTS = await getTimeFromKickoff(duration); - await staking.stake(amount, lockedTS, root, root); - - let stakingBalance = await token.balanceOf.call(staking.address); - expect(stakingBalance.toString()).to.be.equal(amount); - - await mineBlock(); - let amounts = await staking.getWithdrawAmounts(amount, lockedTS); - let returnedAvailableAmount = amounts[0]; - let returnedPunishedAmount = amounts[1]; - - await staking.withdraw(amount, lockedTS, account2); - - stakingBalance = await token.balanceOf.call(staking.address); - expect(stakingBalance.toNumber()).to.be.equal(0); - - let feeSharingBalance = await token.balanceOf.call(feeSharingProxy.address); - let userBalance = await token.balanceOf.call(account2); - - let maxVotingWeight = await staking.MAX_VOTING_WEIGHT.call(); - let maxDuration = await staking.MAX_DURATION.call(); - let weightFactor = await staking.WEIGHT_FACTOR.call(); - let weight = weightingFunction(amount, duration, maxDuration, maxVotingWeight, weightFactor.toNumber()) / 100; - let weightScaling = await staking.weightScaling.call(); - weight = weight * weightScaling; - let punishedAmount = weight; - - let weeks = i * 2; - - expect(feeSharingBalance).to.be.bignumber.equal(new BN(punishedAmount)); - - expect(returnedPunishedAmount).to.be.bignumber.equal(new BN(punishedAmount)); - expect(returnedAvailableAmount).to.be.bignumber.equal(new BN(amount).sub(returnedPunishedAmount)); - } - }); - - it("if withdrawing with a vesting contract, the vesting chckpoints need to be updated", async () => { - let amount = "1000"; - let duration = new BN(TWO_WEEKS).mul(new BN(2)); - let lockedTS = await getTimeFromKickoff(duration); - let { vestingInstance, blockNumber } = await createVestingContractWithSingleDate(duration, amount, token, staking, root); - - //await setTime(lockedTS); - setNextBlockTimestamp(lockedTS.toNumber()); - mineBlock(); - - let stakingBalance = await token.balanceOf.call(staking.address); - expect(stakingBalance.toString()).to.be.equal(amount); - let beforeBalance = await token.balanceOf.call(root); - - let tx2 = await vestingInstance.withdrawTokens(root); - - stakingBalance = await token.balanceOf.call(staking.address); - expect(stakingBalance.toNumber()).to.be.equal(0); - let afterBalance = await token.balanceOf.call(root); - expect(afterBalance.sub(beforeBalance).toString()).to.be.equal(amount); - - //_decreaseDailyStake - let numTotalStakingCheckpoints = await staking.numTotalStakingCheckpoints.call(lockedTS); - expect(numTotalStakingCheckpoints.toNumber()).to.be.equal(2); - let checkpoint = await staking.totalStakingCheckpoints.call(lockedTS, 0); - expect(checkpoint.fromBlock.toNumber()).to.be.equal(blockNumber); - expect(checkpoint.stake.toString()).to.be.equal(amount); - checkpoint = await staking.totalStakingCheckpoints.call(lockedTS, 1); - expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx2.receipt.blockNumber); - expect(checkpoint.stake.toNumber()).to.be.equal(0); - - //_decreaseVestingStake - let numVestingCheckpoints = await staking.numVestingCheckpoints.call(lockedTS); - expect(numVestingCheckpoints.toNumber()).to.be.equal(2); - checkpoint = await staking.vestingCheckpoints.call(lockedTS, 0); - expect(checkpoint.fromBlock.toNumber()).to.be.equal(blockNumber); - expect(checkpoint.stake.toString()).to.be.equal(amount); - checkpoint = await staking.vestingCheckpoints.call(lockedTS, 1); - expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx2.receipt.blockNumber); - expect(checkpoint.stake.toNumber()).to.be.equal(0); - }); - }); - - describe("unlockAllTokens", () => { - it("Only owner should be able to unlock all tokens", async () => { - await expectRevert(staking.unlockAllTokens({ from: account1 }), "unauthorized"); - }); - - it("Should be able to unlock all tokens", async () => { - let amount = "1000"; - let duration = new BN(TWO_WEEKS).mul(new BN(2)); - let lockedTS = await getTimeFromKickoff(duration); - await staking.stake(amount, lockedTS, root, root); - - let tx = await staking.unlockAllTokens(); - - expectEvent(tx, "TokensUnlocked", { - amount: amount, - }); - - await staking.withdraw(amount, lockedTS, root); - }); - }); - - describe("timestampToLockDate", () => { - it("Lock date should be start + 1 period", async () => { - let kickoffTS = await staking.kickoffTS.call(); - let newTime = kickoffTS.add(new BN(TWO_WEEKS)); - // await setTime(newTime); - setNextBlockTimestamp(newTime.toNumber()); - - let result = await staking.timestampToLockDate(newTime); - expect(result.sub(kickoffTS).toNumber()).to.be.equal(TWO_WEEKS); - }); - - it("Lock date should be start + 2 period", async () => { - let kickoffTS = await staking.kickoffTS.call(); - let newTime = kickoffTS.add(new BN(TWO_WEEKS).mul(new BN(2)).add(new BN(DAY))); - // await setTime(newTime); - setNextBlockTimestamp(newTime.toNumber()); - - let result = await staking.timestampToLockDate(newTime); - expect(result.sub(kickoffTS).toNumber()).to.be.equal(TWO_WEEKS * 2); - }); - - it("Lock date should be start + 3 period", async () => { - let kickoffTS = await staking.kickoffTS.call(); - let newTime = kickoffTS.add(new BN(TWO_WEEKS).mul(new BN(3)).add(new BN(DAY))); - // await setTime(newTime); - setNextBlockTimestamp(newTime.toNumber()); - - let result = await staking.timestampToLockDate(newTime); - expect(result.sub(kickoffTS).toNumber()).to.be.equal(TWO_WEEKS * 3); - }); - }); - - describe("upgrade:", async () => { - it("Should be able to read correct data after an upgrade", async () => { - let amount = 100; - let lockedTS = await getTimeFromKickoff(MAX_DURATION); - let tx = await staking.stake(amount, lockedTS, root, root); - - // before upgrade - let balance = await staking.balanceOf.call(root); - expect(balance.toNumber()).to.be.equal(amount); - let checkpoint = await staking.userStakingCheckpoints.call(root, lockedTS, 0); - expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx.receipt.blockNumber); - expect(checkpoint.stake.toNumber()).to.be.equal(amount); - - // upgrade - staking = await StakingProxy.at(staking.address); - let stakingMockup = await StakingMockup.new(token.address); - await staking.setImplementation(stakingMockup.address); - staking = await StakingMockup.at(staking.address); - - // after upgrade: storage data remained the same - balance = await staking.balanceOf.call(root); - expect(balance.toNumber()).to.be.equal(amount); - checkpoint = await staking.userStakingCheckpoints.call(root, lockedTS, 0); - expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx.receipt.blockNumber); - expect(checkpoint.stake.toNumber()).to.be.equal(amount); - - // after upgrade: new method added - balance = await staking.balanceOf_MultipliedByTwo.call(root); - expect(balance.toNumber()).to.be.equal(amount * 2); - }); - }); - - async function getTimeFromKickoff(delay) { - let kickoffTS = await staking.kickoffTS.call(); - return kickoffTS.add(new BN(delay)); - } + let root, account1, account2; + let token, SUSD, WRBTC, staking; + let sovryn; + let loanTokenLogic, loanToken; + let feeSharingProxy; + let kickoffTS, inOneWeek; + + async function deploymentAndInitFixture(_wallets, _provider) { + // Deploying sovrynProtocol w/ generic function from initializer.js + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + await sovryn.setSovrynProtocolAddress(sovryn.address); + + // Custom tokens + /// @dev This SOV token is not a SOV test token + /// but a full-fledged SOV token including functionality + /// like the approveAndCall method. + token = await SOV.new(TOTAL_SUPPLY); + + // Staking + let stakingLogic = await StakingMockup.new(token.address); + staking = await StakingProxy.new(token.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingMockup.at(staking.address); + + // Upgradable Vesting Registry + vestingRegistryLogic = await VestingRegistryLogic.new(); + vesting = await VestingRegistryProxy.new(); + await vesting.setImplementation(vestingRegistryLogic.address); + vesting = await VestingRegistryLogic.at(vesting.address); + + await staking.setVestingRegistry(vesting.address); + + // Loan token + loanTokenSettings = await LoanTokenSettings.new(); + loanTokenLogic = await LoanTokenLogic.new(); + loanToken = await LoanToken.new( + root, + loanTokenLogic.address, + sovryn.address, + WRBTC.address + ); + // await loanToken.initialize(SUSD.address, "iSUSD", "iSUSD"); + loanToken = await LoanTokenLogic.at(loanToken.address); + + await sovryn.setLoanPool([loanToken.address], [SUSD.address]); + + //FeeSharingProxy + let feeSharingLogic = await FeeSharingLogic.new(); + feeSharingProxyObj = await FeeSharingProxy.new(sovryn.address, staking.address); + await feeSharingProxyObj.setImplementation(feeSharingLogic.address); + feeSharingProxy = await FeeSharingLogic.at(feeSharingProxyObj.address); + await sovryn.setFeesController(feeSharingProxy.address); + await staking.setFeeSharing(feeSharingProxy.address); + + await token.transfer(account1, 1000); + await token.approve(staking.address, TOTAL_SUPPLY); + kickoffTS = await staking.kickoffTS.call(); + inOneWeek = kickoffTS.add(new BN(DELAY)); + } + + before(async () => { + [root, account1, account2, ...accounts] = accounts; + }); + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + describe("stake", () => { + it("Amount should be positive", async () => { + await expectRevert( + staking.stake(0, inOneWeek, root, root), + "amount needs to be bigger than 0" + ); + }); + + it("Amount should be approved", async () => { + await expectRevert( + staking.stake(100, inOneWeek, root, root, { from: account1 }), + "ERC20: transfer amount exceeds allowance" + ); + }); + + it("Staking period too short", async () => { + await expectRevert( + staking.stake(100, await getTimeFromKickoff(DAY), root, root), + "Staking::timestampToLockDate: staking period too short" + ); + }); + + it("Shouldn't be able to stake longer than max duration", async () => { + let amount = "100"; + let lockedTS = await getTimeFromKickoff(MAX_DURATION); + let tx = await staking.stake(amount, lockedTS, account1, account1); + + expectEvent(tx, "TokensStaked", { + staker: account1, + amount: amount, + lockedUntil: lockedTS, + totalStaked: amount, + }); + + expectEvent(tx, "DelegateChanged", { + delegator: account1, + fromDelegate: ZERO_ADDRESS, + toDelegate: account1, + }); + }); + + it("Sender should be used if zero addresses passed", async () => { + let amount = "100"; + let lockedTS = await getTimeFromKickoff(MAX_DURATION); + let tx = await staking.stake(amount, lockedTS, ZERO_ADDRESS, ZERO_ADDRESS); + + expectEvent(tx, "TokensStaked", { + staker: root, + amount: amount, + lockedUntil: lockedTS, + totalStaked: amount, + }); + + expectEvent(tx, "DelegateChanged", { + delegator: root, + fromDelegate: ZERO_ADDRESS, + toDelegate: root, + }); + }); + + it("Should be able to stake and delegate for yourself", async () => { + let amount = "100"; + let duration = TWO_WEEKS; + let lockedTS = await getTimeFromKickoff(duration); + + let stakingBalance = await token.balanceOf.call(staking.address); + expect(stakingBalance.toNumber()).to.be.equal(0); + let beforeBalance = await token.balanceOf.call(root); + + let tx = await staking.stake(amount, lockedTS, root, root); + + stakingBalance = await token.balanceOf.call(staking.address); + expect(stakingBalance.toString()).to.be.equal(amount); + let afterBalance = await token.balanceOf.call(root); + expect(beforeBalance.sub(afterBalance).toString()).to.be.equal(amount); + + // _writeUserCheckpoint + let numUserCheckpoints = await staking.numUserStakingCheckpoints.call(root, lockedTS); + expect(numUserCheckpoints.toNumber()).to.be.equal(1); + let checkpoint = await staking.userStakingCheckpoints.call(root, lockedTS, 0); + expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx.receipt.blockNumber); + expect(checkpoint.stake.toString()).to.be.equal(amount); + + // _increaseDailyStake + let numTotalStakingCheckpoints = await staking.numTotalStakingCheckpoints.call( + lockedTS + ); + expect(numTotalStakingCheckpoints.toNumber()).to.be.equal(1); + checkpoint = await staking.totalStakingCheckpoints.call(lockedTS, 0); + expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx.receipt.blockNumber); + expect(checkpoint.stake.toString()).to.be.equal(amount); + + // _delegate + let delegator = await staking.delegates.call(root, lockedTS); + expect(delegator).to.be.equal(root); + + let numDelegateStakingCheckpoints = await staking.numDelegateStakingCheckpoints.call( + root, + lockedTS + ); + expect(numDelegateStakingCheckpoints.toNumber()).to.be.equal(1); + checkpoint = await staking.delegateStakingCheckpoints.call(root, lockedTS, 0); + expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx.receipt.blockNumber); + expect(checkpoint.stake.toString()).to.be.equal(amount); + + expectEvent(tx, "TokensStaked", { + staker: root, + amount: amount, + lockedUntil: lockedTS, + totalStaked: amount, + }); + + expectEvent(tx, "DelegateChanged", { + delegator: root, + fromDelegate: ZERO_ADDRESS, + toDelegate: root, + }); + }); + + it("Should be able to stake and delegate for another person", async () => { + let amount = "1000"; + let duration = new BN(TWO_WEEKS).mul(new BN(2)); + let lockedTS = await getTimeFromKickoff(duration); + + let tx = await staking.stake(amount, lockedTS, account1, account1); + + // _writeUserCheckpoint + let numUserCheckpoints = await staking.numUserStakingCheckpoints.call( + account1, + lockedTS + ); + expect(numUserCheckpoints.toNumber()).to.be.equal(1); + let checkpoint = await staking.userStakingCheckpoints.call(account1, lockedTS, 0); + expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx.receipt.blockNumber); + expect(checkpoint.stake.toString()).to.be.equal(amount); + + // _increaseDailyStake + let numTotalStakingCheckpoints = await staking.numTotalStakingCheckpoints.call( + lockedTS + ); + expect(numTotalStakingCheckpoints.toNumber()).to.be.equal(1); + checkpoint = await staking.totalStakingCheckpoints.call(lockedTS, 0); + expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx.receipt.blockNumber); + expect(checkpoint.stake.toString()).to.be.equal(amount); + + // _delegate + let delegator = await staking.delegates.call(account1, lockedTS); + expect(delegator).to.be.equal(account1); + + let numDelegateStakingCheckpoints = await staking.numDelegateStakingCheckpoints.call( + account1, + lockedTS + ); + expect(numDelegateStakingCheckpoints.toNumber()).to.be.equal(1); + checkpoint = await staking.delegateStakingCheckpoints.call(account1, lockedTS, 0); + expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx.receipt.blockNumber); + expect(checkpoint.stake.toString()).to.be.equal(amount); + + expectEvent(tx, "TokensStaked", { + staker: account1, + amount: amount, + lockedUntil: lockedTS, + totalStaked: amount, + }); + + expectEvent(tx, "DelegateChanged", { + delegator: account1, + fromDelegate: ZERO_ADDRESS, + toDelegate: account1, + }); + }); + + it("Should be able to stake after withdrawing whole amount", async () => { + let amount = "1000"; + let duration = new BN(TWO_WEEKS).mul(new BN(2)); + let lockedTS = await getTimeFromKickoff(duration); + await staking.stake(amount, lockedTS, root, root); + + // await setTime(lockedTS); + setNextBlockTimestamp(lockedTS.toNumber()); + + let stakingBalance = await token.balanceOf.call(staking.address); + expect(stakingBalance.toString()).to.be.equal(amount); + + await staking.withdraw(amount, lockedTS, root); + + stakingBalance = await token.balanceOf.call(staking.address); + expect(stakingBalance.toNumber()).to.be.equal(0); + + // stake second time + lockedTS = await getTimeFromKickoff(duration * 2); + let tx = await staking.stake(amount * 2, lockedTS, root, root); + + stakingBalance = await token.balanceOf.call(staking.address); + expect(stakingBalance.toNumber()).to.be.equal(amount * 2); + + // _writeUserCheckpoint + let numUserCheckpoints = await staking.numUserStakingCheckpoints.call(root, lockedTS); + expect(numUserCheckpoints.toNumber()).to.be.equal(1); + let checkpoint = await staking.userStakingCheckpoints.call(root, lockedTS, 0); + expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx.receipt.blockNumber); + expect(checkpoint.stake.toNumber()).to.be.equal(amount * 2); + }); + + it("Should be able to stake after withdrawing amount partially", async () => { + let amount = "1000"; + let duration = new BN(TWO_WEEKS).mul(new BN(2)); + let lockedTS = await getTimeFromKickoff(duration); + + let bb = await token.balanceOf.call(root); + + await staking.stake(amount, lockedTS, root, root); + + // await setTime(lockedTS); + setNextBlockTimestamp(lockedTS.toNumber()); + blockTimestamp = (await ethers.provider.getBlock("latest")).timestamp; + + let stakingBalance = await token.balanceOf.call(staking.address); + expect(stakingBalance.toString()).to.be.equal(amount); + let beforeBalance = await token.balanceOf.call(root); + + await staking.withdraw(amount / 2, lockedTS, root); + + stakingBalance = await token.balanceOf.call(staking.address); + expect(stakingBalance.toNumber()).to.be.equal(amount / 2); + let afterBalance = await token.balanceOf.call(root); + + expect(afterBalance.sub(beforeBalance).toNumber()).to.be.equal(amount / 2); + + // increase stake + lockedTS = await getTimeFromKickoff(duration * 2); + let tx = await staking.stake(amount * 2.5, lockedTS, root, root); + + stakingBalance = await token.balanceOf.call(staking.address); + expect(stakingBalance.toNumber()).to.be.equal(amount * 3); + + // _writeUserCheckpoint + let numUserCheckpoints = await staking.numUserStakingCheckpoints.call(root, lockedTS); + expect(numUserCheckpoints.toNumber()).to.be.equal(1); + let checkpoint = await staking.userStakingCheckpoints.call(root, lockedTS, 0); + expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx.receipt.blockNumber); + expect(checkpoint.stake.toNumber()).to.be.equal(amount * 2.5); + }); + }); + + describe("stakeWithApproval", () => { + it("Should be able to stake and delegate for yourself", async () => { + let amount = "100"; + let duration = TWO_WEEKS; + let lockedTS = await getTimeFromKickoff(duration); + + let stakingBalance = await token.balanceOf.call(staking.address); + expect(stakingBalance.toNumber()).to.be.equal(0); + let beforeBalance = await token.balanceOf.call(root); + + await token.approve(staking.address, 0); + + // TODO + await token.approve(staking.address, amount * 2, { from: account1 }); + + let contract = new web3.eth.Contract(staking.abi, staking.address); + let sender = root; + let data = contract.methods + .stakeWithApproval(sender, amount, lockedTS, root, root) + .encodeABI(); + // let data = contract.methods.stakeWithApproval(account1, amount * 2, lockedTS, root, root).encodeABI(); + let tx = await token.approveAndCall(staking.address, amount, data, { from: sender }); + + stakingBalance = await token.balanceOf.call(staking.address); + expect(stakingBalance.toString()).to.be.equal(amount); + let afterBalance = await token.balanceOf.call(root); + expect(beforeBalance.sub(afterBalance).toString()).to.be.equal(amount); + + // _writeUserCheckpoint + let numUserCheckpoints = await staking.numUserStakingCheckpoints.call(root, lockedTS); + expect(numUserCheckpoints.toNumber()).to.be.equal(1); + let checkpoint = await staking.userStakingCheckpoints.call(root, lockedTS, 0); + expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx.receipt.blockNumber); + expect(checkpoint.stake.toString()).to.be.equal(amount); + + // _increaseDailyStake + let numTotalStakingCheckpoints = await staking.numTotalStakingCheckpoints.call( + lockedTS + ); + expect(numTotalStakingCheckpoints.toNumber()).to.be.equal(1); + checkpoint = await staking.totalStakingCheckpoints.call(lockedTS, 0); + expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx.receipt.blockNumber); + expect(checkpoint.stake.toString()).to.be.equal(amount); + + // _delegate + let delegator = await staking.delegates.call(root, lockedTS); + expect(delegator).to.be.equal(root); + + let numDelegateStakingCheckpoints = await staking.numDelegateStakingCheckpoints.call( + root, + lockedTS + ); + expect(numDelegateStakingCheckpoints.toNumber()).to.be.equal(1); + checkpoint = await staking.delegateStakingCheckpoints.call(root, lockedTS, 0); + expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx.receipt.blockNumber); + expect(checkpoint.stake.toString()).to.be.equal(amount); + }); + + it("fails if invoked directly", async () => { + let lockedTS = await getTimeFromKickoff(TWO_WEEKS); + await expectRevert( + staking.stakeWithApproval(root, "100", lockedTS, root, root), + "unauthorized" + ); + }); + + it("fails if wrong method passed in data", async () => { + let amount = "100"; + let lockedTS = await getTimeFromKickoff(TWO_WEEKS); + let contract = new web3.eth.Contract(staking.abi, staking.address); + let data = contract.methods.stake(amount, lockedTS, root, root).encodeABI(); + + await expectRevert( + token.approveAndCall(staking.address, amount, data), + "method is not allowed" + ); + }); + + it("fails if wrong sender passed in data", async () => { + let amount = "100"; + let lockedTS = await getTimeFromKickoff(TWO_WEEKS); + let contract = new web3.eth.Contract(staking.abi, staking.address); + + await token.approve(staking.address, amount * 2, { from: account1 }); + let sender = root; + let data = contract.methods + .stakeWithApproval(account1, amount, lockedTS, root, root) + .encodeABI(); + + await expectRevert( + token.approveAndCall(staking.address, amount, data, { from: sender }), + "sender mismatch" + ); + }); + + it("fails if wrong amount passed in data", async () => { + let amount = "100"; + let lockedTS = await getTimeFromKickoff(TWO_WEEKS); + let contract = new web3.eth.Contract(staking.abi, staking.address); + + await token.approve(staking.address, amount * 2, { from: account1 }); + let sender = root; + let data = contract.methods + .stakeWithApproval(sender, amount, lockedTS, root, root) + .encodeABI(); + + await expectRevert( + token.approveAndCall(staking.address, amount * 2, data, { from: sender }), + "amount mismatch" + ); + }); + }); + + describe("WeightedStaking", () => { + /// @dev On governance/Staking/WeightedStaking.sol the conditional: + /// if (userStakingCheckpoints[account][date][nCheckpoints - 1].fromBlock <= blockNumber) + /// is always met, because when a checkpoint is created it is always set the blocknumber + /// of the transaction ocurring. So current blockNumber is to be equal to the + /// blockNumber of the checkpoint if it has been created on the same block, + /// or bigger if it was created on a previous block. Only exception to this + /// would be to request the prior stake for a blockNumber lower than + /// the current one, i.e. an historical query. + it("Coverage for WeightedStaking::_getPriorUserStakeByDate", async () => { + let amount = new BN(1000); + let duration = new BN(TWO_WEEKS).mul(new BN(2)); + let lockedTS = await getTimeFromKickoff(duration); + + // 1st Stake + await staking.stake(amount, lockedTS, root, account1); + + // 2nd Stake + await staking.stake(amount, lockedTS, root, account1); + + // Remember the blocknumber of the second staking + let block = await web3.eth.getBlock("latest"); + + // Time travel, just enough to jump 1 block + await time.increase(1); + + // Check stake is there for the block when 2nd staking took place + let priorStake = await staking.getPriorUserStakeByDate.call( + root, + lockedTS, + new BN(block.number) + ); + expect(priorStake).to.be.bignumber.equal(amount.mul(new BN(2))); + + // Check there is still stake for the block when 1st staking took place + priorStake = await staking.getPriorUserStakeByDate.call( + root, + lockedTS, + new BN(block.number).sub(new BN(1)) + ); + expect(priorStake).to.be.bignumber.equal(amount); + + // Check there is no stake for previous block to the block when staking took place + priorStake = await staking.getPriorUserStakeByDate.call( + root, + lockedTS, + new BN(block.number).sub(new BN(2)) + ); + expect(priorStake).to.be.bignumber.equal(new BN(0)); + }); + }); + + describe("extendStakingDuration", () => { + it("shouldn't extendStakingDuration when _getPriorUserStakeByDate == 0", async () => { + let duration = new BN(0); + let lockedTS = await getTimeFromKickoff(duration); + // console.log("lockedTS: ", lockedTS.toString()); + let newTime = await getTimeFromKickoff(TWO_WEEKS); + // console.log("newTime: ", newTime.toString()); + + // Trying to extend the stake when previous stake is 0 + await expectRevert( + staking.extendStakingDuration(lockedTS, newTime), + "nothing staked until the previous lock date" + ); + }); + + it("extend to a date inside the next 2 weeks granularity bucket", async () => { + let amount = "1000"; + let duration = new BN(TWO_WEEKS).mul(new BN(2)); + let lockedTS = await getTimeFromKickoff(duration); + // console.log("lockedTS: ", lockedTS.toString()); + + // Extending for 13 days + let newDuration = duration.add(new BN(DAY).mul(new BN(13))); + let newTime = await getTimeFromKickoff(newDuration); + console.log("newTime: ", newTime.toString()); + let newTimeLockDate = await staking.timestampToLockDate(newTime); + console.log("newTimeLockDate: ", newTimeLockDate.toString()); + + // Set delegate as account1 + await staking.stake(amount, lockedTS, root, account1); + + // Check the delegate of the stake + let delegate = await staking.delegates(root, lockedTS); + expect(delegate).equal(account1); + + // Extending the stake + await staking.extendStakingDuration(lockedTS, newTime); + + // Check the delegate of the extended stake + delegate = await staking.delegates(root, newTimeLockDate); + /// @dev A 13 days extension is setting delegate to address(0) + /// TODO: Should be fixed soon by contract upgrade. + /// When fixed, uncomment next line and test should be working ok. + // expect(delegate).equal(account1); + }); + + it("Cannot reduce the staking duration", async () => { + let amount = "1000"; + let duration = new BN(TWO_WEEKS).mul(new BN(2)); + let lockedTS = await getTimeFromKickoff(duration); + await staking.stake(amount, lockedTS, root, root); + + let newTime = await getTimeFromKickoff(TWO_WEEKS); + await expectRevert( + staking.extendStakingDuration(lockedTS, newTime), + "cannot reduce the staking duration" + ); + }); + + it("Do not exceed the max duration", async () => { + let amount = "1000"; + let duration = new BN(TWO_WEEKS).mul(new BN(2)); + let lockedTS = await getTimeFromKickoff(duration); + await staking.stake(amount, lockedTS, root, root); + + let newTime = await getTimeFromKickoff(MAX_DURATION.mul(new BN(2))); + let tx = await staking.extendStakingDuration(lockedTS, newTime); + + expectEvent(tx, "ExtendedStakingDuration", { + staker: root, + previousDate: lockedTS, + newDate: await getTimeFromKickoff(MAX_DURATION), + amountStaked: amount, + }); + }); + + it("Should be able to extend staking duration", async () => { + let amount = "1000"; + let lockedTS = await getTimeFromKickoff(TWO_WEEKS); + let tx1 = await staking.stake(amount, lockedTS, root, root); + + let stakingBalance = await token.balanceOf.call(staking.address); + expect(stakingBalance.toString()).to.be.equal(amount); + let beforeBalance = await token.balanceOf.call(root); + + expect(tx1.logs[2].args.lockedUntil.toNumber()).to.be.equal(lockedTS.toNumber()); + + let newLockedTS = await getTimeFromKickoff(TWO_WEEKS * 2); + let tx2 = await staking.extendStakingDuration(lockedTS, newLockedTS); + + stakingBalance = await token.balanceOf.call(staking.address); + expect(stakingBalance.toString()).to.be.equal(amount); + let afterBalance = await token.balanceOf.call(root); + expect(beforeBalance.sub(afterBalance).toNumber()).to.be.equal(0); + + // _decreaseDailyStake + let numTotalStakingCheckpoints = await staking.numTotalStakingCheckpoints.call( + lockedTS + ); + expect(numTotalStakingCheckpoints.toNumber()).to.be.equal(2); + let checkpoint = await staking.totalStakingCheckpoints.call(lockedTS, 0); + expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx1.receipt.blockNumber); + expect(checkpoint.stake.toString()).to.be.equal(amount); + checkpoint = await staking.totalStakingCheckpoints.call(lockedTS, 1); + expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx2.receipt.blockNumber); + expect(checkpoint.stake.toString()).to.be.equal("0"); + + // _increaseDailyStake + numTotalStakingCheckpoints = await staking.numTotalStakingCheckpoints.call( + newLockedTS + ); + expect(numTotalStakingCheckpoints.toNumber()).to.be.equal(1); + checkpoint = await staking.totalStakingCheckpoints.call(newLockedTS, 0); + expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx2.receipt.blockNumber); + expect(checkpoint.stake.toString()).to.be.equal(amount); + + // _writeUserCheckpoint + let numUserCheckpoints = await staking.numUserStakingCheckpoints.call(root, lockedTS); + expect(numUserCheckpoints.toNumber()).to.be.equal(2); + checkpoint = await staking.userStakingCheckpoints.call(root, lockedTS, 0); + expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx1.receipt.blockNumber); + expect(checkpoint.stake.toString()).to.be.equal(amount); + checkpoint = await staking.userStakingCheckpoints.call(root, newLockedTS, 0); + expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx2.receipt.blockNumber); + expect(checkpoint.stake.toString()).to.be.equal(amount); + + expectEvent(tx2, "ExtendedStakingDuration", { + staker: root, + previousDate: lockedTS, + newDate: newLockedTS, + amountStaked: amount, + }); + }); + + it("should update the vesting checkpoints if the stake is extended with a vesting contract", async () => { + //TODO if vesting contracts should ever support this function. + //currently, they don't and they are not upgradable. + }); + }); + + describe("increaseStake", () => { + it("stakesBySchedule w/ duration < = > MAX_DURATION", async () => { + let amount = "1000"; + let duration = new BN(MAX_DURATION).div(new BN(2)); + let cliff = new BN(TWO_WEEKS).mul(new BN(2)); + let intervalLength = new BN(10000000); + let lockTS = await getTimeFromKickoff(duration); + await staking.stakesBySchedule(amount, cliff, duration, intervalLength, root, root); + + // Check staking status for this staker + let rootStaked = await staking.getStakes(root); + // console.log("rootStaked['stakes']", rootStaked["stakes"].toString()); + let stakedDurationLowerThanMax = rootStaked["stakes"][0]; + + // Reset & duration = MAX + await loadFixture(deploymentAndInitFixture); + duration = new BN(MAX_DURATION); + await staking.stakesBySchedule(amount, cliff, duration, intervalLength, root, root); + + // Check staking status for this staker + rootStaked = await staking.getStakes(root); + // console.log("rootStaked['stakes']", rootStaked["stakes"].toString()); + let stakedDurationEqualToMax = rootStaked["stakes"][0]; + + // Reset & duration > MAX + await loadFixture(deploymentAndInitFixture); + duration = new BN(MAX_DURATION).mul(new BN(4)); + await staking.stakesBySchedule(amount, cliff, duration, intervalLength, root, root); + + // Check staking status for this staker + rootStaked = await staking.getStakes(root); + // console.log("rootStaked['stakes']", rootStaked["stakes"].toString()); + let stakedDurationHigherThanMax = rootStaked["stakes"][0]; + + /// @dev When duration = MAX or duration > MAX, contract deals w/ it as MAX + /// so the staked amount is higher when duration < MAX and equal when duration >= MAX + expect(stakedDurationLowerThanMax).to.be.bignumber.greaterThan( + stakedDurationEqualToMax + ); + expect(stakedDurationEqualToMax).to.be.bignumber.equal(stakedDurationHigherThanMax); + }); + + it("Check getCurrentStakedUntil", async () => { + let amount = "1000"; + let duration = new BN(TWO_WEEKS).mul(new BN(2)); + let lockTS = await getTimeFromKickoff(duration); + + // Check staking status before staking + let totalStaked = await staking.getCurrentStakedUntil(lockTS); + // console.log("totalStaked", totalStaked.toString()); + expect(totalStaked).to.be.bignumber.equal(new BN(0)); + + await staking.stake(amount, lockTS, root, root); + + // Check staking status after staking + totalStaked = await staking.getCurrentStakedUntil(lockTS); + // console.log("totalStaked", totalStaked.toString()); + expect(totalStaked).to.be.bignumber.equal(amount); + }); + + it("Amount of tokens to stake needs to be bigger than 0", async () => { + let amount = "1000"; + let duration = new BN(TWO_WEEKS).mul(new BN(2)); + let lockTS = await getTimeFromKickoff(duration); + await staking.stake(amount, lockTS, root, root); + + await expectRevert( + staking.stake("0", lockTS, root, root), + "amount needs to be bigger than 0" + ); + }); + + it("Amount of tokens to stake needs to be bigger than 0", async () => { + let amount = "1000"; + let duration = new BN(TWO_WEEKS).mul(new BN(2)); + let lockTS = await getTimeFromKickoff(duration); + await staking.stake(amount, lockTS, root, root); + + await token.approve(staking.address, 0); + await expectRevert( + staking.stake(amount, lockTS, root, root), + "ERC20: transfer amount exceeds allowance" + ); + }); + + it("Shouldn't be able to overflow balance", async () => { + let amount = "1000"; + let duration = new BN(TWO_WEEKS).mul(new BN(2)); + let lockTS = await getTimeFromKickoff(duration); + await staking.stake(amount, lockTS, root, root); + + let maxValue = new BN(2).pow(new BN(96)).sub(new BN(1)); + await expectRevert( + staking.stake(maxValue.sub(new BN(100)), lockTS, root, root), + "overflow" + ); + }); + + it("Should be able to increase stake", async () => { + let amount = "1000"; + let duration = new BN(TWO_WEEKS).mul(new BN(2)); + let lockedTS = await getTimeFromKickoff(duration); + let tx1 = await staking.stake(amount, lockedTS, root, root); + + // check delegatee + let delegatee = await staking.delegates(root, lockedTS); + expect(delegatee).equal(root); + + let stakingBalance = await token.balanceOf.call(staking.address); + expect(stakingBalance.toString()).to.be.equal(amount); + let beforeBalance = await token.balanceOf.call(root); + + let tx2 = await staking.stake(amount * 2, lockedTS, root, account1); + + // check delegatee + delegatee = await staking.delegates(root, lockedTS); + expect(delegatee).equal(account1); + + stakingBalance = await token.balanceOf.call(staking.address); + expect(stakingBalance.toNumber()).to.be.equal(amount * 3); + let afterBalance = await token.balanceOf.call(root); + expect(beforeBalance.sub(afterBalance).toNumber()).to.be.equal(amount * 2); + + // _increaseDailyStake + let numTotalStakingCheckpoints = await staking.numTotalStakingCheckpoints.call( + lockedTS + ); + expect(numTotalStakingCheckpoints.toNumber()).to.be.equal(2); + let checkpoint = await staking.totalStakingCheckpoints.call(lockedTS, 0); + expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx1.receipt.blockNumber); + expect(checkpoint.stake.toString()).to.be.equal(amount); + checkpoint = await staking.totalStakingCheckpoints.call(lockedTS, 1); + expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx2.receipt.blockNumber); + expect(checkpoint.stake.toNumber()).to.be.equal(amount * 3); + + // _writeUserCheckpoint + let numUserCheckpoints = await staking.numUserStakingCheckpoints.call(root, lockedTS); + expect(numUserCheckpoints.toNumber()).to.be.equal(2); + checkpoint = await staking.userStakingCheckpoints.call(root, lockedTS, 0); + expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx1.receipt.blockNumber); + expect(checkpoint.stake.toString()).to.be.equal(amount); + checkpoint = await staking.userStakingCheckpoints.call(root, lockedTS, 1); + expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx2.receipt.blockNumber); + expect(checkpoint.stake.toNumber()).to.be.equal(amount * 3); + + // delegateStakingCheckpoints - root + let numDelegateStakingCheckpoints = await staking.numDelegateStakingCheckpoints.call( + root, + lockedTS + ); + expect(numDelegateStakingCheckpoints.toNumber()).to.be.equal(2); + checkpoint = await staking.delegateStakingCheckpoints.call(root, lockedTS, 0); + expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx1.receipt.blockNumber); + expect(checkpoint.stake.toString()).to.be.equal(amount); + checkpoint = await staking.delegateStakingCheckpoints.call(root, lockedTS, 1); + expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx2.receipt.blockNumber); + expect(checkpoint.stake.toNumber()).to.be.equal(0); + + // delegateStakingCheckpoints - account1 + numDelegateStakingCheckpoints = await staking.numDelegateStakingCheckpoints.call( + account1, + lockedTS + ); + expect(numDelegateStakingCheckpoints.toNumber()).to.be.equal(1); + checkpoint = await staking.delegateStakingCheckpoints.call(account1, lockedTS, 0); + expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx2.receipt.blockNumber); + expect(checkpoint.stake.toNumber()).to.be.equal(amount * 3); + + expectEvent(tx2, "TokensStaked", { + staker: root, + amount: new BN(amount * 2), + lockedUntil: lockedTS, + totalStaked: new BN(amount * 3), + }); + }); + }); + + describe("getStakes", () => { + it("Should be able to increase stake", async () => { + let amount1 = "1000"; + let lockedTS1 = await getTimeFromKickoff(new BN(TWO_WEEKS)); + await staking.stake(amount1, lockedTS1, root, root); + + // time travel + await time.increase(TWO_WEEKS * 10); + + let amount2 = "5000"; + let lockedTS2 = await getTimeFromKickoff(new BN(MAX_DURATION)); + await staking.stake(amount2, lockedTS2, root, root); + + let data = await staking.getStakes.call(root); + + // for (let i = 0; i < data.dates.length; i++) { + + // } + + expect(data.dates[0]).to.be.bignumber.equal(new BN(lockedTS1)); + expect(data.stakes[0]).to.be.bignumber.equal(new BN(amount1)); + + expect(data.dates[1]).to.be.bignumber.equal(new BN(lockedTS2)); + expect(data.stakes[1]).to.be.bignumber.equal(new BN(amount2)); + }); + }); + + describe("setWeightScaling", () => { + it("Shouldn't be able to weight scaling less than min value", async () => { + await expectRevert( + staking.setWeightScaling(0), + "weight scaling doesn't belong to range [1, 9]" + ); + }); + + it("Shouldn't be able to weight scaling more than max value", async () => { + await expectRevert( + staking.setWeightScaling(10), + "weight scaling doesn't belong to range [1, 9]" + ); + }); + + it("Only owner should be able to weight scaling", async () => { + await expectRevert(staking.setWeightScaling(5, { from: account1 }), "unauthorized"); + }); + + it("Should be able to weight scaling", async () => { + await staking.setWeightScaling(7); + + expect(await staking.weightScaling.call()).to.be.bignumber.equal(new BN(7)); + }); + }); + + describe("withdraw", () => { + it("Amount of tokens to be withdrawn needs to be bigger than 0", async () => { + let amount = "1000"; + let duration = new BN(TWO_WEEKS).mul(new BN(2)); + let lockedTS = await getTimeFromKickoff(duration); + await staking.stake(amount, lockedTS, root, root); + + await expectRevert( + staking.withdraw("0", lockedTS, root), + "Staking::withdraw: amount of tokens to be withdrawn needs to be bigger than 0" + ); + }); + + it("Shouldn't be able to withdraw amount greater than balance", async () => { + let amount = 1000; + let duration = new BN(TWO_WEEKS).mul(new BN(2)); + let lockedTS = await getTimeFromKickoff(duration); + await staking.stake(amount, lockedTS, root, root); + + // await setTime(lockedTS); + setNextBlockTimestamp(lockedTS.toNumber()); + await expectRevert( + staking.withdraw(amount * 2, lockedTS, root), + "Staking::withdraw: not enough balance" + ); + }); + + it("Should be able to withdraw", async () => { + let amount = "1000"; + let duration = new BN(TWO_WEEKS).mul(new BN(2)); + let lockedTS = await getTimeFromKickoff(duration); + let tx1 = await staking.stake(amount, lockedTS, root, root); + + // await setTime(lockedTS); + setNextBlockTimestamp(lockedTS.toNumber()); + mineBlock(); + + let stakingBalance = await token.balanceOf.call(staking.address); + expect(stakingBalance.toString()).to.be.equal(amount); + let beforeBalance = await token.balanceOf.call(root); + + let tx2 = await staking.withdraw(amount / 2, lockedTS, root); + + stakingBalance = await token.balanceOf.call(staking.address); + expect(stakingBalance.toNumber()).to.be.equal(amount / 2); + let afterBalance = await token.balanceOf.call(root); + expect(afterBalance.sub(beforeBalance).toNumber()).to.be.equal(amount / 2); + + // _increaseDailyStake + let numTotalStakingCheckpoints = await staking.numTotalStakingCheckpoints.call( + lockedTS + ); + expect(numTotalStakingCheckpoints.toNumber()).to.be.equal(2); + let checkpoint = await staking.totalStakingCheckpoints.call(lockedTS, 0); + expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx1.receipt.blockNumber); + expect(checkpoint.stake.toString()).to.be.equal(amount); + checkpoint = await staking.totalStakingCheckpoints.call(lockedTS, 1); + expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx2.receipt.blockNumber); + expect(checkpoint.stake.toNumber()).to.be.equal(amount / 2); + + // _writeUserCheckpoint + let numUserCheckpoints = await staking.numUserStakingCheckpoints.call(root, lockedTS); + expect(numUserCheckpoints.toNumber()).to.be.equal(2); + checkpoint = await staking.userStakingCheckpoints.call(root, lockedTS, 0); + expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx1.receipt.blockNumber); + expect(checkpoint.stake.toString()).to.be.equal(amount); + checkpoint = await staking.userStakingCheckpoints.call(root, lockedTS, 1); + expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx2.receipt.blockNumber); + expect(checkpoint.stake.toNumber()).to.be.equal(amount / 2); + + // _decreaseDelegateStake + let numDelegateStakingCheckpoints = await staking.numDelegateStakingCheckpoints.call( + root, + lockedTS + ); + checkpoint = await staking.delegateStakingCheckpoints.call( + root, + lockedTS, + numDelegateStakingCheckpoints - 1 + ); + expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx2.receipt.blockNumber); + expect(checkpoint.stake.toNumber()).to.be.equal(amount / 2); + expect(numDelegateStakingCheckpoints.toNumber()).to.be.equal(2); + + expectEvent(tx2, "StakingWithdrawn", { + staker: root, + amount: new BN(amount / 2), + }); + }); + + it("Should be able to withdraw second time", async () => { + let amount = "1000"; + let duration = new BN(TWO_WEEKS).mul(new BN(2)); + let lockedTS = await getTimeFromKickoff(duration); + await staking.stake(amount, lockedTS, root, root); + + let stakingBalance = await token.balanceOf.call(staking.address); + expect(stakingBalance.toString()).to.be.equal(amount); + + await staking.withdraw(amount / 2, lockedTS, account2); + + stakingBalance = await token.balanceOf.call(staking.address); + expect(stakingBalance.toNumber()).to.be.equal(amount / 2); + + // _decreaseDelegateStake + let numDelegateStakingCheckpoints = await staking.numDelegateStakingCheckpoints.call( + root, + lockedTS + ); + let checkpoint = await staking.delegateStakingCheckpoints.call( + root, + lockedTS, + numDelegateStakingCheckpoints - 1 + ); + expect(checkpoint.stake.toNumber()).to.be.equal(amount / 2); + expect(numDelegateStakingCheckpoints.toNumber()).to.be.equal(2); + + await staking.withdraw(amount / 2, lockedTS, account2); + + stakingBalance = await token.balanceOf.call(staking.address); + expect(stakingBalance.toNumber()).to.be.equal(0); + + // _decreaseDelegateStake + numDelegateStakingCheckpoints = await staking.numDelegateStakingCheckpoints.call( + root, + lockedTS + ); + checkpoint = await staking.delegateStakingCheckpoints.call( + root, + lockedTS, + numDelegateStakingCheckpoints - 1 + ); + expect(checkpoint.stake.toNumber()).to.be.equal(0); + expect(numDelegateStakingCheckpoints.toNumber()).to.be.equal(3); + + let feeSharingBalance = await token.balanceOf.call(feeSharingProxy.address); + let userBalance = await token.balanceOf.call(account2); + + let maxVotingWeight = await staking.MAX_VOTING_WEIGHT.call(); + let maxDuration = await staking.MAX_DURATION.call(); + let weightFactor = await staking.WEIGHT_FACTOR.call(); + let weight = + weightingFunction( + amount, + duration, + maxDuration, + maxVotingWeight, + weightFactor.toNumber() + ) / 100; + let weightScaling = await staking.weightScaling.call(); + weight = weight * weightScaling; + let punishedAmount = weight; + + await expect(feeSharingBalance).to.be.bignumber.equal(new BN(punishedAmount)); + await expect(userBalance).to.be.bignumber.equal(new BN(amount - punishedAmount)); + }); + + it("Should be able to withdraw second time", async () => { + let amount = "1000"; + let duration = new BN(TWO_WEEKS).mul(new BN(2)); + let lockedTS = await getTimeFromKickoff(duration); + await staking.stake(amount, lockedTS, root, root); + + await staking.withdraw(amount, lockedTS, root); + + await staking.stake(amount, lockedTS, root, root); + + await staking.withdraw(amount, lockedTS, root); + }); + + it("Should be able to withdraw second time after partial withdraw", async () => { + let amount = new BN(1000); + let duration = new BN(TWO_WEEKS).mul(new BN(2)); + let lockedTS = await getTimeFromKickoff(duration); + await staking.stake(amount, lockedTS, root, root); + + await staking.withdraw(amount.sub(new BN(1)), lockedTS, root); + + await staking.stake(amount, lockedTS, root, root); + + await staking.withdraw(amount.add(new BN(1)), lockedTS, root); + }); + + it("Should be able to withdraw second time (emulate issue with delegate checkpoint)", async () => { + let amount = "1000"; + let duration = new BN(TWO_WEEKS).mul(new BN(2)); + let lockedTS = await getTimeFromKickoff(duration); + await staking.stake(amount, lockedTS, root, root); + + await staking.withdraw(amount, lockedTS, root); + + await staking.stake(amount, lockedTS, root, root); + await staking.setDelegateStake(root, lockedTS, 0); + + await staking.withdraw(amount, lockedTS, root); + }); + + it("Should be able to extend stake after second stake (emulate issue with delegate checkpoint)", async () => { + let amount = "1000"; + let duration = new BN(TWO_WEEKS).mul(new BN(2)); + let lockedTS = await getTimeFromKickoff(duration); + await staking.stake(amount, lockedTS, root, root); + + await staking.withdraw(amount, lockedTS, root); + + await staking.stake(amount, lockedTS, root, root); + await staking.setDelegateStake(root, lockedTS, 0); + + let lockedTS2 = await getTimeFromKickoff(duration.mul(new BN(3))); + await staking.extendStakingDuration(lockedTS, lockedTS2); + }); + + it("Should be able to delegate stake after second stake (emulate issue with delegate checkpoint)", async () => { + let amount = "1000"; + let duration = new BN(TWO_WEEKS).mul(new BN(2)); + let lockedTS = await getTimeFromKickoff(duration); + await staking.stake(amount, lockedTS, root, root); + + await staking.withdraw(amount, lockedTS, root); + + await staking.stake(amount, lockedTS, root, root); + await staking.setDelegateStake(root, lockedTS, 0); + + await staking.delegate(account1, lockedTS); + }); + + it("Should be able to withdraw earlier for any lock date", async () => { + let amount = "10000"; + + for (let i = 1; i <= 78; i++) { + if (i !== 1 && i !== 78 && i % 10 !== 0) { + continue; + } + + // FeeSharingProxy + let feeSharingLogic = await FeeSharingLogic.new(); + feeSharingProxyObj = await FeeSharingProxy.new(sovryn.address, staking.address); + await feeSharingProxyObj.setImplementation(feeSharingLogic.address); + feeSharingProxy = await FeeSharingLogic.at(feeSharingProxyObj.address); + await sovryn.setFeesController(feeSharingProxy.address); + await staking.setFeeSharing(feeSharingProxy.address); + + let duration = new BN(i * TWO_WEEKS); + let lockedTS = await getTimeFromKickoff(duration); + await staking.stake(amount, lockedTS, root, root); + + let stakingBalance = await token.balanceOf.call(staking.address); + expect(stakingBalance.toString()).to.be.equal(amount); + + await mineBlock(); + let amounts = await staking.getWithdrawAmounts(amount, lockedTS); + let returnedAvailableAmount = amounts[0]; + let returnedPunishedAmount = amounts[1]; + + await staking.withdraw(amount, lockedTS, account2); + + stakingBalance = await token.balanceOf.call(staking.address); + expect(stakingBalance.toNumber()).to.be.equal(0); + + let feeSharingBalance = await token.balanceOf.call(feeSharingProxy.address); + let userBalance = await token.balanceOf.call(account2); + + let maxVotingWeight = await staking.MAX_VOTING_WEIGHT.call(); + let maxDuration = await staking.MAX_DURATION.call(); + let weightFactor = await staking.WEIGHT_FACTOR.call(); + let weight = + weightingFunction( + amount, + duration, + maxDuration, + maxVotingWeight, + weightFactor.toNumber() + ) / 100; + let weightScaling = await staking.weightScaling.call(); + weight = weight * weightScaling; + let punishedAmount = weight; + + let weeks = i * 2; + + expect(feeSharingBalance).to.be.bignumber.equal(new BN(punishedAmount)); + + expect(returnedPunishedAmount).to.be.bignumber.equal(new BN(punishedAmount)); + expect(returnedAvailableAmount).to.be.bignumber.equal( + new BN(amount).sub(returnedPunishedAmount) + ); + } + }); + + it("if withdrawing with a vesting contract, the vesting chckpoints need to be updated", async () => { + let amount = "1000"; + let duration = new BN(TWO_WEEKS).mul(new BN(2)); + let lockedTS = await getTimeFromKickoff(duration); + let { vestingInstance, blockNumber } = await createVestingContractWithSingleDate( + duration, + amount, + token, + staking, + root + ); + + //await setTime(lockedTS); + setNextBlockTimestamp(lockedTS.toNumber()); + mineBlock(); + + let stakingBalance = await token.balanceOf.call(staking.address); + expect(stakingBalance.toString()).to.be.equal(amount); + let beforeBalance = await token.balanceOf.call(root); + + let tx2 = await vestingInstance.withdrawTokens(root); + + stakingBalance = await token.balanceOf.call(staking.address); + expect(stakingBalance.toNumber()).to.be.equal(0); + let afterBalance = await token.balanceOf.call(root); + expect(afterBalance.sub(beforeBalance).toString()).to.be.equal(amount); + + //_decreaseDailyStake + let numTotalStakingCheckpoints = await staking.numTotalStakingCheckpoints.call( + lockedTS + ); + expect(numTotalStakingCheckpoints.toNumber()).to.be.equal(2); + let checkpoint = await staking.totalStakingCheckpoints.call(lockedTS, 0); + expect(checkpoint.fromBlock.toNumber()).to.be.equal(blockNumber); + expect(checkpoint.stake.toString()).to.be.equal(amount); + checkpoint = await staking.totalStakingCheckpoints.call(lockedTS, 1); + expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx2.receipt.blockNumber); + expect(checkpoint.stake.toNumber()).to.be.equal(0); + + //_decreaseVestingStake + let numVestingCheckpoints = await staking.numVestingCheckpoints.call(lockedTS); + expect(numVestingCheckpoints.toNumber()).to.be.equal(2); + checkpoint = await staking.vestingCheckpoints.call(lockedTS, 0); + expect(checkpoint.fromBlock.toNumber()).to.be.equal(blockNumber); + expect(checkpoint.stake.toString()).to.be.equal(amount); + checkpoint = await staking.vestingCheckpoints.call(lockedTS, 1); + expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx2.receipt.blockNumber); + expect(checkpoint.stake.toNumber()).to.be.equal(0); + }); + }); + + describe("unlockAllTokens", () => { + it("Only owner should be able to unlock all tokens", async () => { + await expectRevert(staking.unlockAllTokens({ from: account1 }), "unauthorized"); + }); + + it("Should be able to unlock all tokens", async () => { + let amount = "1000"; + let duration = new BN(TWO_WEEKS).mul(new BN(2)); + let lockedTS = await getTimeFromKickoff(duration); + await staking.stake(amount, lockedTS, root, root); + + let tx = await staking.unlockAllTokens(); + + expectEvent(tx, "TokensUnlocked", { + amount: amount, + }); + + await staking.withdraw(amount, lockedTS, root); + }); + }); + + describe("timestampToLockDate", () => { + it("Lock date should be start + 1 period", async () => { + let kickoffTS = await staking.kickoffTS.call(); + let newTime = kickoffTS.add(new BN(TWO_WEEKS)); + // await setTime(newTime); + setNextBlockTimestamp(newTime.toNumber()); + + let result = await staking.timestampToLockDate(newTime); + expect(result.sub(kickoffTS).toNumber()).to.be.equal(TWO_WEEKS); + }); + + it("Lock date should be start + 2 period", async () => { + let kickoffTS = await staking.kickoffTS.call(); + let newTime = kickoffTS.add(new BN(TWO_WEEKS).mul(new BN(2)).add(new BN(DAY))); + // await setTime(newTime); + setNextBlockTimestamp(newTime.toNumber()); + + let result = await staking.timestampToLockDate(newTime); + expect(result.sub(kickoffTS).toNumber()).to.be.equal(TWO_WEEKS * 2); + }); + + it("Lock date should be start + 3 period", async () => { + let kickoffTS = await staking.kickoffTS.call(); + let newTime = kickoffTS.add(new BN(TWO_WEEKS).mul(new BN(3)).add(new BN(DAY))); + // await setTime(newTime); + setNextBlockTimestamp(newTime.toNumber()); + + let result = await staking.timestampToLockDate(newTime); + expect(result.sub(kickoffTS).toNumber()).to.be.equal(TWO_WEEKS * 3); + }); + }); + + describe("upgrade:", async () => { + it("Should be able to read correct data after an upgrade", async () => { + let amount = 100; + let lockedTS = await getTimeFromKickoff(MAX_DURATION); + let tx = await staking.stake(amount, lockedTS, root, root); + + // before upgrade + let balance = await staking.balanceOf.call(root); + expect(balance.toNumber()).to.be.equal(amount); + let checkpoint = await staking.userStakingCheckpoints.call(root, lockedTS, 0); + expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx.receipt.blockNumber); + expect(checkpoint.stake.toNumber()).to.be.equal(amount); + + // upgrade + staking = await StakingProxy.at(staking.address); + let stakingMockup = await StakingMockup.new(token.address); + await staking.setImplementation(stakingMockup.address); + staking = await StakingMockup.at(staking.address); + + // after upgrade: storage data remained the same + balance = await staking.balanceOf.call(root); + expect(balance.toNumber()).to.be.equal(amount); + checkpoint = await staking.userStakingCheckpoints.call(root, lockedTS, 0); + expect(checkpoint.fromBlock.toNumber()).to.be.equal(tx.receipt.blockNumber); + expect(checkpoint.stake.toNumber()).to.be.equal(amount); + + // after upgrade: new method added + balance = await staking.balanceOf_MultipliedByTwo.call(root); + expect(balance.toNumber()).to.be.equal(amount * 2); + }); + }); + + async function getTimeFromKickoff(delay) { + let kickoffTS = await staking.kickoffTS.call(); + return kickoffTS.add(new BN(delay)); + } }); function weightingFunction(stake, time, maxDuration, maxVotingWeight, weightFactor) { - let x = maxDuration - time; - let mD2 = maxDuration * maxDuration; - return Math.floor((stake * (Math.floor((maxVotingWeight * weightFactor * (mD2 - x * x)) / mD2) + weightFactor)) / weightFactor); + let x = maxDuration - time; + let mD2 = maxDuration * maxDuration; + return Math.floor( + (stake * + (Math.floor((maxVotingWeight * weightFactor * (mD2 - x * x)) / mD2) + weightFactor)) / + weightFactor + ); } async function createVestingContractWithSingleDate(cliff, amount, token, staking, tokenOwner) { - vestingLogic = await VestingLogic.new(); - let vestingInstance = await Vesting.new(vestingLogic.address, token.address, staking.address, tokenOwner, cliff, cliff, tokenOwner); - vestingInstance = await VestingLogic.at(vestingInstance.address); - //important, so it's recognized as vesting contract - await staking.addContractCodeHash(vestingInstance.address); - - await token.approve(vestingInstance.address, amount); - let result = await vestingInstance.stakeTokens(amount); - return { vestingInstance: vestingInstance, blockNumber: result.receipt.blockNumber }; + vestingLogic = await VestingLogic.new(); + let vestingInstance = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + tokenOwner, + cliff, + cliff, + tokenOwner + ); + vestingInstance = await VestingLogic.at(vestingInstance.address); + //important, so it's recognized as vesting contract + await staking.addContractCodeHash(vestingInstance.address); + + await token.approve(vestingInstance.address, amount); + let result = await vestingInstance.stakeTokens(amount); + return { vestingInstance: vestingInstance, blockNumber: result.receipt.blockNumber }; } diff --git a/tests/staking/SafeMath96.test.js b/tests/staking/SafeMath96.test.js index 288ae146b..572aca82f 100644 --- a/tests/staking/SafeMath96.test.js +++ b/tests/staking/SafeMath96.test.js @@ -4,78 +4,87 @@ const { BN, constants, expectEvent, expectRevert } = require("@openzeppelin/test const TestCoverage = artifacts.require("TestCoverage"); contract("SafeMath96", (accounts) => { - before(async () => { - testCoverage = await TestCoverage.new(); - }); + before(async () => { + testCoverage = await TestCoverage.new(); + }); - describe("SafeMath96 edge cases", () => { - it("shouldn't overflow: safe32 w/ 2**32-1", async () => { - let upperLimitValue = new BN(2).pow(new BN(32)).sub(new BN(1)); - let result = await testCoverage.testSafeMath96_safe32(upperLimitValue); - expect(result).to.be.bignumber.equal(upperLimitValue); - }); + describe("SafeMath96 edge cases", () => { + it("shouldn't overflow: safe32 w/ 2**32-1", async () => { + let upperLimitValue = new BN(2).pow(new BN(32)).sub(new BN(1)); + let result = await testCoverage.testSafeMath96_safe32(upperLimitValue); + expect(result).to.be.bignumber.equal(upperLimitValue); + }); - it("should overflow: safe32 w/ 2**32", async () => { - await expectRevert(testCoverage.testSafeMath96_safe32(new BN(2).pow(new BN(32))), "overflow"); - }); + it("should overflow: safe32 w/ 2**32", async () => { + await expectRevert( + testCoverage.testSafeMath96_safe32(new BN(2).pow(new BN(32))), + "overflow" + ); + }); - it("shouldn't overflow: safe64 w/ 2**64-1", async () => { - let upperLimitValue = new BN(2).pow(new BN(64)).sub(new BN(1)); - let result = await testCoverage.testSafeMath96_safe64(upperLimitValue); - expect(result).to.be.bignumber.equal(upperLimitValue); - }); + it("shouldn't overflow: safe64 w/ 2**64-1", async () => { + let upperLimitValue = new BN(2).pow(new BN(64)).sub(new BN(1)); + let result = await testCoverage.testSafeMath96_safe64(upperLimitValue); + expect(result).to.be.bignumber.equal(upperLimitValue); + }); - it("should overflow: safe64 w/ 2**64", async () => { - await expectRevert(testCoverage.testSafeMath96_safe64(new BN(2).pow(new BN(64))), "overflow"); - }); + it("should overflow: safe64 w/ 2**64", async () => { + await expectRevert( + testCoverage.testSafeMath96_safe64(new BN(2).pow(new BN(64))), + "overflow" + ); + }); - it("shouldn't overflow: safe96 w/ 2**96-1", async () => { - let upperLimitValue = new BN(2).pow(new BN(96)).sub(new BN(1)); - let result = await testCoverage.testSafeMath96_safe96(upperLimitValue); - expect(result).to.be.bignumber.equal(upperLimitValue); - }); + it("shouldn't overflow: safe96 w/ 2**96-1", async () => { + let upperLimitValue = new BN(2).pow(new BN(96)).sub(new BN(1)); + let result = await testCoverage.testSafeMath96_safe96(upperLimitValue); + expect(result).to.be.bignumber.equal(upperLimitValue); + }); - it("should overflow: safe96 w/ 2**96", async () => { - await expectRevert(testCoverage.testSafeMath96_safe96(new BN(2).pow(new BN(96))), "overflow"); - }); + it("should overflow: safe96 w/ 2**96", async () => { + await expectRevert( + testCoverage.testSafeMath96_safe96(new BN(2).pow(new BN(96))), + "overflow" + ); + }); - it("shouldn't underflow: sub96 w/ a > b", async () => { - let a = new BN(2).pow(new BN(96)).sub(new BN(1)); - let b = new BN(2).pow(new BN(95)); - let result = await testCoverage.testSafeMath96_sub96(a, b); - expect(result).to.be.bignumber.equal(a.sub(b)); - }); + it("shouldn't underflow: sub96 w/ a > b", async () => { + let a = new BN(2).pow(new BN(96)).sub(new BN(1)); + let b = new BN(2).pow(new BN(95)); + let result = await testCoverage.testSafeMath96_sub96(a, b); + expect(result).to.be.bignumber.equal(a.sub(b)); + }); - it("should underflow: sub96 w/ a < b", async () => { - let a = new BN(2).pow(new BN(95)); - let b = new BN(2).pow(new BN(96)).sub(new BN(1)); - await expectRevert(testCoverage.testSafeMath96_sub96(a, b), "underflow"); - }); + it("should underflow: sub96 w/ a < b", async () => { + let a = new BN(2).pow(new BN(95)); + let b = new BN(2).pow(new BN(96)).sub(new BN(1)); + await expectRevert(testCoverage.testSafeMath96_sub96(a, b), "underflow"); + }); - it("shouldn't overflow: mul96 w/ a * b < 2**96", async () => { - let a = new BN(2).pow(new BN(48)); - let b = new BN(2).pow(new BN(48)).sub(new BN(1)); - let result = await testCoverage.testSafeMath96_mul96(a, b); - expect(result).to.be.bignumber.equal(a.mul(b)); - }); + it("shouldn't overflow: mul96 w/ a * b < 2**96", async () => { + let a = new BN(2).pow(new BN(48)); + let b = new BN(2).pow(new BN(48)).sub(new BN(1)); + let result = await testCoverage.testSafeMath96_mul96(a, b); + expect(result).to.be.bignumber.equal(a.mul(b)); + }); - it("should overflow: mul96 w/ a * b >= 2**96", async () => { - let a = new BN(2).pow(new BN(48)); - let b = new BN(2).pow(new BN(48)); - await expectRevert(testCoverage.testSafeMath96_mul96(a, b), "overflow"); - }); + it("should overflow: mul96 w/ a * b >= 2**96", async () => { + let a = new BN(2).pow(new BN(48)); + let b = new BN(2).pow(new BN(48)); + await expectRevert(testCoverage.testSafeMath96_mul96(a, b), "overflow"); + }); - it("shouldn't revert: div96 w/ b > 0", async () => { - let a = new BN(2).pow(new BN(96)).sub(new BN(1)); - let b = new BN(2).pow(new BN(95)); - let result = await testCoverage.testSafeMath96_div96(a, b); - expect(result).to.be.bignumber.equal(a.div(b)); - }); + it("shouldn't revert: div96 w/ b > 0", async () => { + let a = new BN(2).pow(new BN(96)).sub(new BN(1)); + let b = new BN(2).pow(new BN(95)); + let result = await testCoverage.testSafeMath96_div96(a, b); + expect(result).to.be.bignumber.equal(a.div(b)); + }); - it("should revert: div96 w/ b == 0", async () => { - let a = new BN(2).pow(new BN(48)); - let b = new BN(0); - await expectRevert(testCoverage.testSafeMath96_div96(a, b), "division by 0"); - }); - }); + it("should revert: div96 w/ b == 0", async () => { + let a = new BN(2).pow(new BN(48)); + let b = new BN(0); + await expectRevert(testCoverage.testSafeMath96_div96(a, b), "division by 0"); + }); + }); }); diff --git a/tests/staking/StakingTest.js b/tests/staking/StakingTest.js index 530f0cb7a..d18522536 100644 --- a/tests/staking/StakingTest.js +++ b/tests/staking/StakingTest.js @@ -32,238 +32,242 @@ const DELAY = 86400 * 14; const TWO_WEEKS = 86400 * 14; contract("Staking", (accounts) => { - const name = "Test token"; - const symbol = "TST"; - - let root, a1, a2, a3, chainId; - let pA1; - let token, staking; - let MAX_VOTING_WEIGHT; - - let kickoffTS, inThreeYears; - let currentChainId; - - let vestingLogic1, vestingLogic2; - - async function deploymentAndInitFixture(_wallets, _provider) { - chainId = 1; // await web3.eth.net.getId(); See: https://github.com/trufflesuite/ganache-core/issues/515 - await web3.eth.net.getId(); - token = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); - - let stakingLogic = await StakingLogic.new(token.address); - staking = await StakingProxy.new(token.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingLogic.at(staking.address); - //Upgradable Vesting Registry - vestingRegistryLogic = await VestingRegistryLogic.new(); - vesting = await VestingRegistryProxy.new(); - await vesting.setImplementation(vestingRegistryLogic.address); - vesting = await VestingRegistryLogic.at(vesting.address); - - await staking.setVestingRegistry(vesting.address); - - MAX_VOTING_WEIGHT = await staking.MAX_VOTING_WEIGHT.call(); - - kickoffTS = await staking.kickoffTS.call(); - inThreeYears = kickoffTS.add(new BN(DELAY * 26 * 3)); - } - - before(async () => { - [root, a1, a2, a3, ...accounts] = accounts; - [pkbRoot, pkbA1] = getAccountsPrivateKeysBuffer(); - currentChainId = (await ethers.provider.getNetwork()).chainId; - - vestingLogic1 = await VestingLogic.new(); - vestingLogic2 = await VestingLogic.new(); - }); - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - // describe("metadata", () => { - // it("has given name", async () => { - // expect(await token.name.call()).to.be.equal(name); - // }); - // - // it("has given symbol", async () => { - // expect(await token.symbol.call()).to.be.equal(symbol); - // }); - // }); - // - // describe("balanceOf", () => { - // it("grants to initial account", async () => { - // expect((await token.balanceOf.call(root)).toString()).to.be.equal(TOTAL_SUPPLY); - // }); - // }); - // - // describe("delegateBySig", () => { - // const Domain = (staking) => ({ name: "SOVStaking", chainId: currentChainId, verifyingContract: staking.address }); - // const Types = { - // Delegation: [ - // { name: "delegatee", type: "address" }, - // { name: "lockDate", type: "uint256" }, - // { name: "nonce", type: "uint256" }, - // { name: "expiry", type: "uint256" }, - // ], - // }; - // - // it("reverts if the signatory is invalid", async () => { - // const delegatee = root, - // nonce = 0, - // expiry = 0; - // await expectRevert( - // staking.delegateBySig(delegatee, inThreeYears, nonce, expiry, 0, "0xbad", "0xbad"), - // "Staking::delegateBySig: invalid signature" - // ); - // }); - // - // it("reverts if the nonce is bad ", async () => { - // const delegatee = root, - // nonce = 1, - // expiry = 0, - // lockDate = inThreeYears; - // const { v, r, s } = EIP712.sign( - // Domain(staking), - // "Delegation", - // { - // delegatee, - // lockDate, - // nonce, - // expiry, - // }, - // Types, - // pkbA1 - // //pA1.privateKey - // //unlockedAccount(a1).secretKey - // ); - // /*const { v, r, s } = EIP712Ethers.sign( - // Domain(staking), - // "Delegation", - // { - // delegatee, - // lockDate, - // nonce, - // expiry, - // }, - // Types, - // pA1 - // );*/ - // - // await expectRevert( - // staking.delegateBySig(delegatee, inThreeYears, nonce, expiry, v, r, s), - // "Staking::delegateBySig: invalid nonce" - // ); - // }); - // - // it("reverts if the signature has expired", async () => { - // const delegatee = root, - // nonce = 0, - // expiry = 0, - // lockDate = inThreeYears; - // const { v, r, s } = EIP712.sign( - // Domain(staking), - // "Delegation", - // { - // delegatee, - // lockDate, - // nonce, - // expiry, - // }, - // Types, - // pkbA1 - // ); - // await expectRevert( - // staking.delegateBySig(delegatee, inThreeYears, nonce, expiry, v, r, s), - // "Staking::delegateBySig: signature expired" - // ); - // }); - // - // it("delegates on behalf of the signatory", async () => { - // const delegatee = root, - // nonce = 0, - // expiry = 10e9, - // lockDate = inThreeYears; - // const { v, r, s } = EIP712.sign( - // Domain(staking), - // "Delegation", - // { - // delegatee, - // lockDate, - // nonce, - // expiry, - // }, - // Types, - // pkbA1 - // //unlockedAccount(a1).secretKey - // ); - // - // expect(await staking.delegates.call(a1, inThreeYears)).to.be.equal(address(0)); - // const tx = await staking.delegateBySig(delegatee, inThreeYears, nonce, expiry, v, r, s); - // expect(tx.gasUsed < 80000); - // expect(await staking.delegates.call(a1, inThreeYears)).to.be.equal(root); - // }); - // }); - - describe("setVestingStakes", () => { - it("should fail if unauthorized", async () => { - await expectRevert(staking.setVestingStakes([], [], { from: a1 }), "unauthorized"); - }); - - it("should fail if arrays have different length", async () => { - let lockedDates = [kickoffTS.add(new BN(TWO_WEEKS))]; - let values = []; - await expectRevert(staking.setVestingStakes(lockedDates, values), "arrays mismatch"); - }); - }); - - describe("balanceOf", () => { - it("grants to initial account", async () => { - expect((await token.balanceOf.call(root)).toString()).to.be.equal(TOTAL_SUPPLY); - }); - }); - - describe("delegateBySig", () => { - const Domain = (staking) => ({ name: "SOVStaking", chainId: currentChainId, verifyingContract: staking.address }); - const Types = { - Delegation: [ - { name: "delegatee", type: "address" }, - { name: "lockDate", type: "uint256" }, - { name: "nonce", type: "uint256" }, - { name: "expiry", type: "uint256" }, - ], - }; - - it("reverts if the signatory is invalid", async () => { - const delegatee = root, - nonce = 0, - expiry = 0; - await expectRevert( - staking.delegateBySig(delegatee, inThreeYears, nonce, expiry, 0, "0xbad", "0xbad"), - "Staking::delegateBySig: invalid signature" - ); - }); - - it("reverts if the nonce is bad ", async () => { - const delegatee = root, - nonce = 1, - expiry = 0, - lockDate = inThreeYears; - const { v, r, s } = EIP712.sign( - Domain(staking), - "Delegation", - { - delegatee, - lockDate, - nonce, - expiry, - }, - Types, - pkbA1 - // pA1.privateKey - // unlockedAccount(a1).secretKey - ); - /*const { v, r, s } = EIP712Ethers.sign( + const name = "Test token"; + const symbol = "TST"; + + let root, a1, a2, a3, chainId; + let pA1; + let token, staking; + let MAX_VOTING_WEIGHT; + + let kickoffTS, inThreeYears; + let currentChainId; + + let vestingLogic1, vestingLogic2; + + async function deploymentAndInitFixture(_wallets, _provider) { + chainId = 1; // await web3.eth.net.getId(); See: https://github.com/trufflesuite/ganache-core/issues/515 + await web3.eth.net.getId(); + token = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); + + let stakingLogic = await StakingLogic.new(token.address); + staking = await StakingProxy.new(token.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); + //Upgradable Vesting Registry + vestingRegistryLogic = await VestingRegistryLogic.new(); + vesting = await VestingRegistryProxy.new(); + await vesting.setImplementation(vestingRegistryLogic.address); + vesting = await VestingRegistryLogic.at(vesting.address); + + await staking.setVestingRegistry(vesting.address); + + MAX_VOTING_WEIGHT = await staking.MAX_VOTING_WEIGHT.call(); + + kickoffTS = await staking.kickoffTS.call(); + inThreeYears = kickoffTS.add(new BN(DELAY * 26 * 3)); + } + + before(async () => { + [root, a1, a2, a3, ...accounts] = accounts; + [pkbRoot, pkbA1] = getAccountsPrivateKeysBuffer(); + currentChainId = (await ethers.provider.getNetwork()).chainId; + + vestingLogic1 = await VestingLogic.new(); + vestingLogic2 = await VestingLogic.new(); + }); + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + // describe("metadata", () => { + // it("has given name", async () => { + // expect(await token.name.call()).to.be.equal(name); + // }); + // + // it("has given symbol", async () => { + // expect(await token.symbol.call()).to.be.equal(symbol); + // }); + // }); + // + // describe("balanceOf", () => { + // it("grants to initial account", async () => { + // expect((await token.balanceOf.call(root)).toString()).to.be.equal(TOTAL_SUPPLY); + // }); + // }); + // + // describe("delegateBySig", () => { + // const Domain = (staking) => ({ name: "SOVStaking", chainId: currentChainId, verifyingContract: staking.address }); + // const Types = { + // Delegation: [ + // { name: "delegatee", type: "address" }, + // { name: "lockDate", type: "uint256" }, + // { name: "nonce", type: "uint256" }, + // { name: "expiry", type: "uint256" }, + // ], + // }; + // + // it("reverts if the signatory is invalid", async () => { + // const delegatee = root, + // nonce = 0, + // expiry = 0; + // await expectRevert( + // staking.delegateBySig(delegatee, inThreeYears, nonce, expiry, 0, "0xbad", "0xbad"), + // "Staking::delegateBySig: invalid signature" + // ); + // }); + // + // it("reverts if the nonce is bad ", async () => { + // const delegatee = root, + // nonce = 1, + // expiry = 0, + // lockDate = inThreeYears; + // const { v, r, s } = EIP712.sign( + // Domain(staking), + // "Delegation", + // { + // delegatee, + // lockDate, + // nonce, + // expiry, + // }, + // Types, + // pkbA1 + // //pA1.privateKey + // //unlockedAccount(a1).secretKey + // ); + // /*const { v, r, s } = EIP712Ethers.sign( + // Domain(staking), + // "Delegation", + // { + // delegatee, + // lockDate, + // nonce, + // expiry, + // }, + // Types, + // pA1 + // );*/ + // + // await expectRevert( + // staking.delegateBySig(delegatee, inThreeYears, nonce, expiry, v, r, s), + // "Staking::delegateBySig: invalid nonce" + // ); + // }); + // + // it("reverts if the signature has expired", async () => { + // const delegatee = root, + // nonce = 0, + // expiry = 0, + // lockDate = inThreeYears; + // const { v, r, s } = EIP712.sign( + // Domain(staking), + // "Delegation", + // { + // delegatee, + // lockDate, + // nonce, + // expiry, + // }, + // Types, + // pkbA1 + // ); + // await expectRevert( + // staking.delegateBySig(delegatee, inThreeYears, nonce, expiry, v, r, s), + // "Staking::delegateBySig: signature expired" + // ); + // }); + // + // it("delegates on behalf of the signatory", async () => { + // const delegatee = root, + // nonce = 0, + // expiry = 10e9, + // lockDate = inThreeYears; + // const { v, r, s } = EIP712.sign( + // Domain(staking), + // "Delegation", + // { + // delegatee, + // lockDate, + // nonce, + // expiry, + // }, + // Types, + // pkbA1 + // //unlockedAccount(a1).secretKey + // ); + // + // expect(await staking.delegates.call(a1, inThreeYears)).to.be.equal(address(0)); + // const tx = await staking.delegateBySig(delegatee, inThreeYears, nonce, expiry, v, r, s); + // expect(tx.gasUsed < 80000); + // expect(await staking.delegates.call(a1, inThreeYears)).to.be.equal(root); + // }); + // }); + + describe("setVestingStakes", () => { + it("should fail if unauthorized", async () => { + await expectRevert(staking.setVestingStakes([], [], { from: a1 }), "unauthorized"); + }); + + it("should fail if arrays have different length", async () => { + let lockedDates = [kickoffTS.add(new BN(TWO_WEEKS))]; + let values = []; + await expectRevert(staking.setVestingStakes(lockedDates, values), "arrays mismatch"); + }); + }); + + describe("balanceOf", () => { + it("grants to initial account", async () => { + expect((await token.balanceOf.call(root)).toString()).to.be.equal(TOTAL_SUPPLY); + }); + }); + + describe("delegateBySig", () => { + const Domain = (staking) => ({ + name: "SOVStaking", + chainId: currentChainId, + verifyingContract: staking.address, + }); + const Types = { + Delegation: [ + { name: "delegatee", type: "address" }, + { name: "lockDate", type: "uint256" }, + { name: "nonce", type: "uint256" }, + { name: "expiry", type: "uint256" }, + ], + }; + + it("reverts if the signatory is invalid", async () => { + const delegatee = root, + nonce = 0, + expiry = 0; + await expectRevert( + staking.delegateBySig(delegatee, inThreeYears, nonce, expiry, 0, "0xbad", "0xbad"), + "Staking::delegateBySig: invalid signature" + ); + }); + + it("reverts if the nonce is bad ", async () => { + const delegatee = root, + nonce = 1, + expiry = 0, + lockDate = inThreeYears; + const { v, r, s } = EIP712.sign( + Domain(staking), + "Delegation", + { + delegatee, + lockDate, + nonce, + expiry, + }, + Types, + pkbA1 + // pA1.privateKey + // unlockedAccount(a1).secretKey + ); + /*const { v, r, s } = EIP712Ethers.sign( Domain(staking), "Delegation", { @@ -276,262 +280,342 @@ contract("Staking", (accounts) => { pA1 );*/ - await expectRevert( - staking.delegateBySig(delegatee, inThreeYears, nonce, expiry, v, r, s), - "Staking::delegateBySig: invalid nonce" - ); - }); - - it("reverts if the signature has expired", async () => { - const delegatee = root, - nonce = 0, - expiry = 0, - lockDate = inThreeYears; - const { v, r, s } = EIP712.sign( - Domain(staking), - "Delegation", - { - delegatee, - lockDate, - nonce, - expiry, - }, - Types, - pkbA1 - ); - await expectRevert( - staking.delegateBySig(delegatee, inThreeYears, nonce, expiry, v, r, s), - "Staking::delegateBySig: signature expired" - ); - }); - - it("delegates on behalf of the signatory", async () => { - const delegatee = root, - nonce = 0, - expiry = 10e9, - lockDate = inThreeYears; - const { v, r, s } = EIP712.sign( - Domain(staking), - "Delegation", - { - delegatee, - lockDate, - nonce, - expiry, - }, - Types, - pkbA1 - // unlockedAccount(a1).secretKey - ); - - expect(await staking.delegates.call(a1, inThreeYears)).to.be.equal(address(0)); - const tx = await staking.delegateBySig(delegatee, inThreeYears, nonce, expiry, v, r, s); - expect(tx.gasUsed < 80000); - expect(await staking.delegates.call(a1, inThreeYears)).to.be.equal(root); - }); - }); - - describe("numCheckpoints", () => { - it("returns the number of checkpoints for a delegate", async () => { - let guy = accounts[0]; - await token.transfer(guy, "1000"); // give an account a few tokens for readability - await expect((await staking.numUserStakingCheckpoints.call(a1, inThreeYears)).toString()).to.be.equal("0"); - - await token.approve(staking.address, "1000", { from: guy }); - await staking.stake("100", inThreeYears, a1, a1, { from: guy }); - await expect((await staking.numUserStakingCheckpoints.call(a1, inThreeYears)).toString()).to.be.equal("1"); - - await staking.stake("50", inThreeYears, a1, a1, { from: guy }); - await expect((await staking.numUserStakingCheckpoints.call(a1, inThreeYears)).toString()).to.be.equal("2"); - }); - - it("does not add more than one checkpoint in a block", async () => { - let guy = accounts[1]; - await token.transfer(guy, "1000"); // give an account a few tokens for readability - await expect((await staking.numUserStakingCheckpoints.call(a3, inThreeYears)).toString()).to.be.equal("0"); - - await token.approve(staking.address, "1000", { from: guy }); - - // await minerStop(); - let t1 = staking.stake("80", inThreeYears, a3, a3, { from: guy }); - - let t2 = staking.delegate(a3, inThreeYears, { from: guy }); - let t3 = token.transfer(a2, 10, { from: guy }); - let t4 = token.transfer(a2, 10, { from: guy }); - - // await minerStart(); - t1 = await t1; - t2 = await t2; - t3 = await t3; - t4 = await t4; - - await expect((await staking.numUserStakingCheckpoints.call(a3, inThreeYears)).toString()).to.be.equal("1"); - - let checkpoint0 = await staking.userStakingCheckpoints.call(a3, inThreeYears, 0); - await expect(checkpoint0.fromBlock.toString()).to.be.equal(t1.receipt.blockNumber.toString()); - await expect(checkpoint0.stake.toString()).to.be.equal("80"); - - let checkpoint1 = await staking.userStakingCheckpoints.call(a3, inThreeYears, 1); - await expect(checkpoint1.fromBlock.toString()).to.be.equal("0"); - await expect(checkpoint1.stake.toString()).to.be.equal("0"); - - let checkpoint2 = await staking.userStakingCheckpoints.call(a3, inThreeYears, 2); - await expect(checkpoint2.fromBlock.toString()).to.be.equal("0"); - await expect(checkpoint2.stake.toString()).to.be.equal("0"); - - await token.approve(staking.address, "20", { from: a2 }); - let t5 = await staking.stake("20", inThreeYears, a3, a3, { from: a2 }); - - await expect((await staking.numUserStakingCheckpoints.call(a3, inThreeYears)).toString()).to.be.equal("2"); - - checkpoint1 = await staking.userStakingCheckpoints.call(a3, inThreeYears, 1); - await expect(checkpoint1.fromBlock.toString()).to.be.equal(t5.receipt.blockNumber.toString()); - await expect(checkpoint1.stake.toString()).to.be.equal("100"); - }); - }); - - describe("getPriorVotes", () => { - let amount = "1000"; - - it("reverts if block number >= current block", async () => { - let time = kickoffTS.add(new BN(DELAY)); - await expectRevert(staking.getPriorVotes.call(a1, 5e10, time), "not determined yet"); - }); - - it("returns 0 if there are no checkpoints", async () => { - expect((await staking.getPriorVotes.call(a1, 0, kickoffTS)).toString()).to.be.equal("0"); - }); - - it("returns the latest block if >= last checkpoint block", async () => { - await token.approve(staking.address, amount); - let t1 = await staking.stake(amount, inThreeYears, a1, a1); - await mineBlock(); - await mineBlock(); - - let amountWithWeight = getAmountWithWeight(amount); - expect((await staking.getPriorVotes.call(a1, new BN(t1.receipt.blockNumber), kickoffTS)).toString()).to.be.equal( - amountWithWeight.toString() - ); - expect((await staking.getPriorVotes.call(a1, new BN(t1.receipt.blockNumber + 1), kickoffTS)).toString()).to.be.equal( - amountWithWeight.toString() - ); - }); - - it("returns zero if < first checkpoint block", async () => { - await mineBlock(); - await token.approve(staking.address, amount); - let t1 = await staking.stake(amount, inThreeYears, a1, a1); - await mineBlock(); - await mineBlock(); - - let amountWithWeight = getAmountWithWeight(amount); - expect((await staking.getPriorVotes.call(a1, new BN(t1.receipt.blockNumber - 1), kickoffTS)).toString()).to.be.equal("0"); - expect((await staking.getPriorVotes.call(a1, new BN(t1.receipt.blockNumber + 1), kickoffTS)).toString()).to.be.equal( - amountWithWeight.toString() - ); - }); - - it("generally returns the voting balance at the appropriate checkpoint", async () => { - await token.approve(staking.address, "1000"); - await staking.stake("1000", inThreeYears, root, root); - const t1 = await staking.delegate(a1, inThreeYears); - await mineBlock(); - await mineBlock(); - await token.transfer(a2, 10); - await token.approve(staking.address, "10", { from: a2 }); - const t2 = await staking.stake("10", inThreeYears, a1, a1, { from: a2 }); - await mineBlock(); - await mineBlock(); - await token.transfer(a3, 101); - await token.approve(staking.address, "101", { from: a3 }); - const t3 = await staking.stake("101", inThreeYears, a1, a1, { from: a3 }); - await mineBlock(); - await mineBlock(); - - expect((await staking.getPriorVotes.call(a1, new BN(t1.receipt.blockNumber - 1), kickoffTS)).toString()).to.be.equal("0"); - expect((await staking.getPriorVotes.call(a1, new BN(t1.receipt.blockNumber), kickoffTS)).toString()).to.be.equal( - getAmountWithWeight("1000").toString() - ); - expect((await staking.getPriorVotes.call(a1, new BN(t1.receipt.blockNumber + 1), kickoffTS)).toString()).to.be.equal( - getAmountWithWeight("1000").toString() - ); - expect((await staking.getPriorVotes.call(a1, new BN(t2.receipt.blockNumber), kickoffTS)).toString()).to.be.equal( - getAmountWithWeight("1010").toString() - ); - expect((await staking.getPriorVotes.call(a1, new BN(t2.receipt.blockNumber + 1), kickoffTS)).toString()).to.be.equal( - getAmountWithWeight("1010").toString() - ); - expect((await staking.getPriorVotes.call(a1, new BN(t3.receipt.blockNumber), kickoffTS)).toString()).to.be.equal( - getAmountWithWeight("1111").toString() - ); - expect((await staking.getPriorVotes.call(a1, new BN(t3.receipt.blockNumber + 1), kickoffTS)).toString()).to.be.equal( - getAmountWithWeight("1111").toString() - ); - }); - }); - - describe("addAdmin", () => { - it("adds admin", async () => { - let tx = await staking.addAdmin(a1); - - expectEvent(tx, "AdminAdded", { - admin: a1, - }); - - let isAdmin = await staking.admins(a1); - expect(isAdmin).equal(true); - }); - - it("fails sender isn't an owner", async () => { - await expectRevert(staking.addAdmin(a1, { from: a1 }), "unauthorized"); - }); - }); - - describe("removeAdmin", () => { - it("removes admin", async () => { - await staking.addAdmin(a1); - let tx = await staking.removeAdmin(a1); - - expectEvent(tx, "AdminRemoved", { - admin: a1, - }); - - let isAdmin = await staking.admins(a1); - expect(isAdmin).equal(false); - }); - - it("fails sender isn't an owner", async () => { - await expectRevert(staking.removeAdmin(a1, { from: a1 }), "unauthorized"); - }); - }); - - describe("vesting stakes", () => { - it("should set vesting stakes", async () => { - let lockedDates = [ - kickoffTS.add(new BN(TWO_WEEKS)), - kickoffTS.add(new BN(TWO_WEEKS).mul(new BN(2))), - kickoffTS.add(new BN(TWO_WEEKS).mul(new BN(4))), - ]; - let values = [new BN(1000), new BN(30000000000), new BN(500000000000000)]; - - let tx = await staking.setVestingStakes(lockedDates, values); - - for (let i = 0; i < lockedDates.length; i++) { - let numCheckpoints = await staking.numVestingCheckpoints.call(lockedDates[i]); - expect(numCheckpoints).to.be.bignumber.equal(new BN(1)); - let value = await staking.vestingCheckpoints.call(lockedDates[i], 0); - expect(value.stake).to.be.bignumber.equal(values[i]); - expect(value.fromBlock).to.be.bignumber.equal(new BN(0)); - - expectEvent(tx, "VestingStakeSet", { - lockedTS: lockedDates[i], - value: values[i], - }); - } - }); - }); - - function getAmountWithWeight(amount) { - return new BN(MAX_VOTING_WEIGHT.toNumber() + 1).mul(new BN(amount)); - } + await expectRevert( + staking.delegateBySig(delegatee, inThreeYears, nonce, expiry, v, r, s), + "Staking::delegateBySig: invalid nonce" + ); + }); + + it("reverts if the signature has expired", async () => { + const delegatee = root, + nonce = 0, + expiry = 0, + lockDate = inThreeYears; + const { v, r, s } = EIP712.sign( + Domain(staking), + "Delegation", + { + delegatee, + lockDate, + nonce, + expiry, + }, + Types, + pkbA1 + ); + await expectRevert( + staking.delegateBySig(delegatee, inThreeYears, nonce, expiry, v, r, s), + "Staking::delegateBySig: signature expired" + ); + }); + + it("delegates on behalf of the signatory", async () => { + const delegatee = root, + nonce = 0, + expiry = 10e9, + lockDate = inThreeYears; + const { v, r, s } = EIP712.sign( + Domain(staking), + "Delegation", + { + delegatee, + lockDate, + nonce, + expiry, + }, + Types, + pkbA1 + // unlockedAccount(a1).secretKey + ); + + expect(await staking.delegates.call(a1, inThreeYears)).to.be.equal(address(0)); + const tx = await staking.delegateBySig( + delegatee, + inThreeYears, + nonce, + expiry, + v, + r, + s + ); + expect(tx.gasUsed < 80000); + expect(await staking.delegates.call(a1, inThreeYears)).to.be.equal(root); + }); + }); + + describe("numCheckpoints", () => { + it("returns the number of checkpoints for a delegate", async () => { + let guy = accounts[0]; + await token.transfer(guy, "1000"); // give an account a few tokens for readability + await expect( + (await staking.numUserStakingCheckpoints.call(a1, inThreeYears)).toString() + ).to.be.equal("0"); + + await token.approve(staking.address, "1000", { from: guy }); + await staking.stake("100", inThreeYears, a1, a1, { from: guy }); + await expect( + (await staking.numUserStakingCheckpoints.call(a1, inThreeYears)).toString() + ).to.be.equal("1"); + + await staking.stake("50", inThreeYears, a1, a1, { from: guy }); + await expect( + (await staking.numUserStakingCheckpoints.call(a1, inThreeYears)).toString() + ).to.be.equal("2"); + }); + + it("does not add more than one checkpoint in a block", async () => { + let guy = accounts[1]; + await token.transfer(guy, "1000"); // give an account a few tokens for readability + await expect( + (await staking.numUserStakingCheckpoints.call(a3, inThreeYears)).toString() + ).to.be.equal("0"); + + await token.approve(staking.address, "1000", { from: guy }); + + // await minerStop(); + let t1 = staking.stake("80", inThreeYears, a3, a3, { from: guy }); + + let t2 = staking.delegate(a3, inThreeYears, { from: guy }); + let t3 = token.transfer(a2, 10, { from: guy }); + let t4 = token.transfer(a2, 10, { from: guy }); + + // await minerStart(); + t1 = await t1; + t2 = await t2; + t3 = await t3; + t4 = await t4; + + await expect( + (await staking.numUserStakingCheckpoints.call(a3, inThreeYears)).toString() + ).to.be.equal("1"); + + let checkpoint0 = await staking.userStakingCheckpoints.call(a3, inThreeYears, 0); + await expect(checkpoint0.fromBlock.toString()).to.be.equal( + t1.receipt.blockNumber.toString() + ); + await expect(checkpoint0.stake.toString()).to.be.equal("80"); + + let checkpoint1 = await staking.userStakingCheckpoints.call(a3, inThreeYears, 1); + await expect(checkpoint1.fromBlock.toString()).to.be.equal("0"); + await expect(checkpoint1.stake.toString()).to.be.equal("0"); + + let checkpoint2 = await staking.userStakingCheckpoints.call(a3, inThreeYears, 2); + await expect(checkpoint2.fromBlock.toString()).to.be.equal("0"); + await expect(checkpoint2.stake.toString()).to.be.equal("0"); + + await token.approve(staking.address, "20", { from: a2 }); + let t5 = await staking.stake("20", inThreeYears, a3, a3, { from: a2 }); + + await expect( + (await staking.numUserStakingCheckpoints.call(a3, inThreeYears)).toString() + ).to.be.equal("2"); + + checkpoint1 = await staking.userStakingCheckpoints.call(a3, inThreeYears, 1); + await expect(checkpoint1.fromBlock.toString()).to.be.equal( + t5.receipt.blockNumber.toString() + ); + await expect(checkpoint1.stake.toString()).to.be.equal("100"); + }); + }); + + describe("getPriorVotes", () => { + let amount = "1000"; + + it("reverts if block number >= current block", async () => { + let time = kickoffTS.add(new BN(DELAY)); + await expectRevert(staking.getPriorVotes.call(a1, 5e10, time), "not determined yet"); + }); + + it("returns 0 if there are no checkpoints", async () => { + expect((await staking.getPriorVotes.call(a1, 0, kickoffTS)).toString()).to.be.equal( + "0" + ); + }); + + it("returns the latest block if >= last checkpoint block", async () => { + await token.approve(staking.address, amount); + let t1 = await staking.stake(amount, inThreeYears, a1, a1); + await mineBlock(); + await mineBlock(); + + let amountWithWeight = getAmountWithWeight(amount); + expect( + ( + await staking.getPriorVotes.call(a1, new BN(t1.receipt.blockNumber), kickoffTS) + ).toString() + ).to.be.equal(amountWithWeight.toString()); + expect( + ( + await staking.getPriorVotes.call( + a1, + new BN(t1.receipt.blockNumber + 1), + kickoffTS + ) + ).toString() + ).to.be.equal(amountWithWeight.toString()); + }); + + it("returns zero if < first checkpoint block", async () => { + await mineBlock(); + await token.approve(staking.address, amount); + let t1 = await staking.stake(amount, inThreeYears, a1, a1); + await mineBlock(); + await mineBlock(); + + let amountWithWeight = getAmountWithWeight(amount); + expect( + ( + await staking.getPriorVotes.call( + a1, + new BN(t1.receipt.blockNumber - 1), + kickoffTS + ) + ).toString() + ).to.be.equal("0"); + expect( + ( + await staking.getPriorVotes.call( + a1, + new BN(t1.receipt.blockNumber + 1), + kickoffTS + ) + ).toString() + ).to.be.equal(amountWithWeight.toString()); + }); + + it("generally returns the voting balance at the appropriate checkpoint", async () => { + await token.approve(staking.address, "1000"); + await staking.stake("1000", inThreeYears, root, root); + const t1 = await staking.delegate(a1, inThreeYears); + await mineBlock(); + await mineBlock(); + await token.transfer(a2, 10); + await token.approve(staking.address, "10", { from: a2 }); + const t2 = await staking.stake("10", inThreeYears, a1, a1, { from: a2 }); + await mineBlock(); + await mineBlock(); + await token.transfer(a3, 101); + await token.approve(staking.address, "101", { from: a3 }); + const t3 = await staking.stake("101", inThreeYears, a1, a1, { from: a3 }); + await mineBlock(); + await mineBlock(); + + expect( + ( + await staking.getPriorVotes.call( + a1, + new BN(t1.receipt.blockNumber - 1), + kickoffTS + ) + ).toString() + ).to.be.equal("0"); + expect( + ( + await staking.getPriorVotes.call(a1, new BN(t1.receipt.blockNumber), kickoffTS) + ).toString() + ).to.be.equal(getAmountWithWeight("1000").toString()); + expect( + ( + await staking.getPriorVotes.call( + a1, + new BN(t1.receipt.blockNumber + 1), + kickoffTS + ) + ).toString() + ).to.be.equal(getAmountWithWeight("1000").toString()); + expect( + ( + await staking.getPriorVotes.call(a1, new BN(t2.receipt.blockNumber), kickoffTS) + ).toString() + ).to.be.equal(getAmountWithWeight("1010").toString()); + expect( + ( + await staking.getPriorVotes.call( + a1, + new BN(t2.receipt.blockNumber + 1), + kickoffTS + ) + ).toString() + ).to.be.equal(getAmountWithWeight("1010").toString()); + expect( + ( + await staking.getPriorVotes.call(a1, new BN(t3.receipt.blockNumber), kickoffTS) + ).toString() + ).to.be.equal(getAmountWithWeight("1111").toString()); + expect( + ( + await staking.getPriorVotes.call( + a1, + new BN(t3.receipt.blockNumber + 1), + kickoffTS + ) + ).toString() + ).to.be.equal(getAmountWithWeight("1111").toString()); + }); + }); + + describe("addAdmin", () => { + it("adds admin", async () => { + let tx = await staking.addAdmin(a1); + + expectEvent(tx, "AdminAdded", { + admin: a1, + }); + + let isAdmin = await staking.admins(a1); + expect(isAdmin).equal(true); + }); + + it("fails sender isn't an owner", async () => { + await expectRevert(staking.addAdmin(a1, { from: a1 }), "unauthorized"); + }); + }); + + describe("removeAdmin", () => { + it("removes admin", async () => { + await staking.addAdmin(a1); + let tx = await staking.removeAdmin(a1); + + expectEvent(tx, "AdminRemoved", { + admin: a1, + }); + + let isAdmin = await staking.admins(a1); + expect(isAdmin).equal(false); + }); + + it("fails sender isn't an owner", async () => { + await expectRevert(staking.removeAdmin(a1, { from: a1 }), "unauthorized"); + }); + }); + + describe("vesting stakes", () => { + it("should set vesting stakes", async () => { + let lockedDates = [ + kickoffTS.add(new BN(TWO_WEEKS)), + kickoffTS.add(new BN(TWO_WEEKS).mul(new BN(2))), + kickoffTS.add(new BN(TWO_WEEKS).mul(new BN(4))), + ]; + let values = [new BN(1000), new BN(30000000000), new BN(500000000000000)]; + + let tx = await staking.setVestingStakes(lockedDates, values); + + for (let i = 0; i < lockedDates.length; i++) { + let numCheckpoints = await staking.numVestingCheckpoints.call(lockedDates[i]); + expect(numCheckpoints).to.be.bignumber.equal(new BN(1)); + let value = await staking.vestingCheckpoints.call(lockedDates[i], 0); + expect(value.stake).to.be.bignumber.equal(values[i]); + expect(value.fromBlock).to.be.bignumber.equal(new BN(0)); + + expectEvent(tx, "VestingStakeSet", { + lockedTS: lockedDates[i], + value: values[i], + }); + } + }); + }); + + function getAmountWithWeight(amount) { + return new BN(MAX_VOTING_WEIGHT.toNumber() + 1).mul(new BN(amount)); + } }); diff --git a/tests/staking/WeightedStakingTest.js b/tests/staking/WeightedStakingTest.js index a04af6185..37eb9594a 100644 --- a/tests/staking/WeightedStakingTest.js +++ b/tests/staking/WeightedStakingTest.js @@ -28,324 +28,539 @@ const TWO_WEEKS = 1209600; const DELAY = TWO_WEEKS; contract("WeightedStaking", (accounts) => { - const name = "Test token"; - const symbol = "TST"; - - let root, a1, a2, a3; - let token, staking; - let kickoffTS, inTwoWeeks, inOneYear, inTwoYears, inThreeYears; - - async function deploymentAndInitFixture(_wallets, _provider) { - token = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); - - let stakingLogic = await StakingLogic.new(token.address); - staking = await StakingProxy.new(token.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingLogic.at(staking.address); - - await token.transfer(a2, "1000"); - await token.approve(staking.address, "1000", { from: a2 }); - - kickoffTS = await staking.kickoffTS.call(); - inTwoWeeks = kickoffTS.add(new BN(DELAY)); - inOneYear = kickoffTS.add(new BN(DELAY * 26)); - inTwoYears = kickoffTS.add(new BN(DELAY * 26 * 2)); - inThreeYears = kickoffTS.add(new BN(DELAY * 26 * 3)); - } - - before(async () => { - [root, a1, a2, a3, ...accounts] = accounts; - }); - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - describe("numCheckpoints", () => { - it("returns the number of checkpoints for a user", async () => { - await expect((await staking.numUserStakingCheckpoints.call(a1, inTwoWeeks)).toString()).to.be.equal("0"); - - await staking.stake("100", inTwoWeeks, a1, a1, { from: a2 }); - await expect((await staking.numUserStakingCheckpoints.call(a1, inTwoWeeks)).toString()).to.be.equal("1"); - - await expect(await staking.stake("50", inTwoWeeks, a1, a1, { from: a2 })); - await expect((await staking.numUserStakingCheckpoints.call(a1, inTwoWeeks)).toString()).to.be.equal("2"); - }); - - it("returns the number of checkpoints for a delegate and date", async () => { - await expect((await staking.numDelegateStakingCheckpoints.call(a3, inTwoWeeks)).toString()).to.be.equal("0"); - - await staking.stake("100", inTwoWeeks, a1, a3, { from: a2 }); - await expect((await staking.numDelegateStakingCheckpoints.call(a3, inTwoWeeks)).toString()).to.be.equal("1"); - - await expect(await staking.stake("50", inTwoWeeks, a1, a1, { from: a2 })); - await expect((await staking.numDelegateStakingCheckpoints.call(a3, inTwoWeeks)).toString()).to.be.equal("2"); - - await staking.stake("100", inTwoWeeks, a2, a3, { from: a2 }); - await expect((await staking.numDelegateStakingCheckpoints.call(a3, inTwoWeeks)).toString()).to.be.equal("3"); - }); - - it("returns the number of total staking checkpoints for a date", async () => { - await expect((await staking.numTotalStakingCheckpoints.call(inTwoWeeks)).toString()).to.be.equal("0"); - - await staking.stake("100", inTwoWeeks, a1, a3, { from: a2 }); - await expect((await staking.numTotalStakingCheckpoints.call(inTwoWeeks)).toString()).to.be.equal("1"); - - await expect(await staking.stake("50", inTwoWeeks, a1, a1, { from: a2 })); - await expect((await staking.numTotalStakingCheckpoints.call(inTwoWeeks)).toString()).to.be.equal("2"); - - await staking.stake("100", inTwoWeeks, a2, a3, { from: a2 }); - await expect((await staking.numTotalStakingCheckpoints.call(inTwoWeeks)).toString()).to.be.equal("3"); - }); - }); - - describe("checkpoints", () => { - it("returns the correct checkpoint for an user", async () => { - // shortest staking duration - let result = await staking.stake("100", inTwoWeeks, a1, a3, { from: a2 }); - await expect((await staking.balanceOf(a1)).toString()).to.be.equal("100"); - let checkpoint = await staking.userStakingCheckpoints(a1, inTwoWeeks, 0); - - await expect(checkpoint.fromBlock.toNumber()).to.be.equal(result.receipt.blockNumber); - await expect(checkpoint.stake.toString()).to.be.equal("100"); - - // max staking duration - result = await staking.stake("100", inThreeYears, a2, a3, { from: a2 }); - checkpoint = await staking.userStakingCheckpoints(a2, inThreeYears, 0); - await expect(checkpoint.fromBlock.toNumber()).to.be.equal(result.receipt.blockNumber); - await expect(checkpoint.stake.toString()).to.be.equal("100"); - }); - - it("returns the correct checkpoint for a delegate", async () => { - let result = await staking.stake("100", inTwoWeeks, a1, a3, { from: a2 }); - await expect((await staking.balanceOf(a1)).toString()).to.be.equal("100"); - - let checkpoint = await staking.delegateStakingCheckpoints(a3, inTwoWeeks, 0); - await expect(checkpoint.fromBlock.toNumber()).to.be.equal(result.receipt.blockNumber); - await expect(checkpoint.stake.toString()).to.be.equal("100"); - - // add stake and change delegate - result = await staking.stake("200", inTwoWeeks, a1, a2, { from: a2 }); - await expect((await staking.balanceOf(a1)).toString()).to.be.equal("300"); - - // old delegate - checkpoint = await staking.delegateStakingCheckpoints(a3, inTwoWeeks, 1); - await expect(checkpoint.fromBlock.toNumber()).to.be.equal(result.receipt.blockNumber); - await expect(checkpoint.stake.toString()).to.be.equal("0"); - - // new delegate - checkpoint = await staking.delegateStakingCheckpoints(a2, inTwoWeeks, 0); - await expect(checkpoint.fromBlock.toNumber()).to.be.equal(result.receipt.blockNumber); - await expect(checkpoint.stake.toString()).to.be.equal("300"); - }); - - it("returns the correct checkpoint for a total stakes", async () => { - let result = await staking.stake("100", inTwoWeeks, a1, a3, { from: a2 }); - await expect((await staking.balanceOf(a1)).toString()).to.be.equal("100"); - let checkpoint = await staking.totalStakingCheckpoints(inTwoWeeks, 0); - - await expect(checkpoint.fromBlock.toNumber()).to.be.equal(result.receipt.blockNumber); - await expect(checkpoint.stake.toString()).to.be.equal("100"); - }); - - it("returns the correct checkpoint for vested stakes", async () => { - //verify that regular staking does not create a vesting checkpoint - await staking.stake("100", inTwoWeeks, a1, a3, { from: a2 }); - await expect((await staking.numVestingCheckpoints(kickoffTS.add(new BN(DELAY)))).toNumber()).to.be.equal(0); - - //verify that vested staking does - let { vestingInstance, blockNumber } = await createVestingContractWithSingleDate(2 * WEEK, 1000, token, staking, root); - - await expect((await staking.balanceOf(vestingInstance.address)).toString()).to.be.equal("1000"); - - await expect((await staking.numVestingCheckpoints(kickoffTS.add(new BN(DELAY)))).toNumber()).to.be.equal(1); - - checkpoint = await staking.vestingCheckpoints(kickoffTS.add(new BN(DELAY)), 0); - - await expect(checkpoint.fromBlock.toNumber()).to.be.equal(blockNumber); - await expect(checkpoint.stake.toString()).to.be.equal("1000"); - }); - }); - - describe("total voting power computation", () => { - it("should compute the expected voting power", async () => { - await staking.stake("100", inThreeYears, a1, a2, { from: a2 }); - await staking.stake("100", inTwoYears, a2, a2, { from: a2 }); - let result = await staking.stake("100", inOneYear, a3, a3, { from: a2 }); - await mineBlock(); - - let maxVotingWeight = await staking.MAX_VOTING_WEIGHT.call(); - let maxDuration = await staking.MAX_DURATION.call(); - let weightFactor = await staking.WEIGHT_FACTOR.call(); - - // power on kickoff date - let expectedPower = - weightingFunction(100, DELAY * (26 * 3), maxDuration, maxVotingWeight, weightFactor.toNumber()) + - weightingFunction(100, DELAY * 26 * 2, maxDuration, maxVotingWeight, weightFactor.toNumber()) + - weightingFunction(100, DELAY * 26, maxDuration, maxVotingWeight, weightFactor.toNumber()); - let totalVotingPower = await staking.getPriorTotalVotingPower(result.receipt.blockNumber, kickoffTS); - await expect(totalVotingPower.toNumber()).to.be.equal(expectedPower); - - // power 52 weeks later - expectedPower = - weightingFunction(100, DELAY * (26 * 2), maxDuration, maxVotingWeight, weightFactor.toNumber()) + - weightingFunction(100, DELAY * 26 * 1, maxDuration, maxVotingWeight, weightFactor.toNumber()) + - weightingFunction(100, DELAY * 26 * 0, maxDuration, maxVotingWeight, weightFactor.toNumber()); - totalVotingPower = await staking.getPriorTotalVotingPower(result.receipt.blockNumber, kickoffTS.add(new BN(DELAY * 26))); - await expect(totalVotingPower.toNumber()).to.be.equal(expectedPower); - }); - - it("should be unable to compute the total voting power for the current block", async () => { - let result = await staking.stake("100", inOneYear, a3, a3, { from: a2 }); - await expectRevert(staking.getPriorTotalVotingPower(result.receipt.blockNumber, kickoffTS), "not yet determined"); - }); - }); - - describe("delegated voting power computation", () => { - it("should compute the expected voting power", async () => { - await staking.stake("100", inThreeYears, a1, a2, { from: a2 }); - await staking.stake("100", inTwoYears, a2, a3, { from: a2 }); - let result = await staking.stake("100", inOneYear, a3, a2, { from: a2 }); - await mineBlock(); - - let maxVotingWeight = await staking.MAX_VOTING_WEIGHT.call(); - let maxDuration = await staking.MAX_DURATION.call(); - let weightFactor = await staking.WEIGHT_FACTOR.call(); - - // power on kickoff date - let expectedPower = - weightingFunction(100, DELAY * (26 * 3), maxDuration, maxVotingWeight, weightFactor.toNumber()) + - weightingFunction(100, DELAY * 26, maxDuration, maxVotingWeight, weightFactor.toNumber()); - let totalVotingPower = await staking.getPriorVotes(a2, result.receipt.blockNumber, kickoffTS); - await expect(totalVotingPower.toNumber()).to.be.equal(expectedPower); - - // power 52 weeks later - expectedPower = - weightingFunction(100, DELAY * (26 * 2), maxDuration, maxVotingWeight, weightFactor.toNumber()) + - weightingFunction(100, DELAY * 26 * 0, maxDuration, maxVotingWeight, weightFactor.toNumber()); - totalVotingPower = await staking.getPriorVotes(a2, result.receipt.blockNumber, kickoffTS.add(new BN(DELAY * 26))); - await expect(totalVotingPower.toNumber()).to.be.equal(expectedPower); - }); - - it("should be unable to compute the voting power for the current block", async () => { - let result = await staking.stake("100", inOneYear, a3, a3, { from: a2 }); - await expectRevert(staking.getPriorVotes(a3, result.receipt.blockNumber, kickoffTS), "not determined yet"); - }); - - it("should return the current votes", async () => { - await staking.stake("100", inThreeYears, a2, a2, { from: a2 }); - await mineBlock(); - - let maxVotingWeight = await staking.MAX_VOTING_WEIGHT.call(); - let maxDuration = await staking.MAX_DURATION.call(); - let weightFactor = await staking.WEIGHT_FACTOR.call(); - - let expectedPower = weightingFunction(100, DELAY * (26 * 3), maxDuration, maxVotingWeight, weightFactor.toNumber()); - let currentVotes = await staking.getCurrentVotes.call(a2); - await expect(currentVotes.toNumber()).to.be.equal(expectedPower); - }); - }); - - describe("user weighted stake computation", () => { - it("should compute the expected weighted stake", async () => { - await staking.stake("100", inThreeYears, a2, a2, { from: a2 }); - await staking.stake("100", inTwoYears, a1, a3, { from: a2 }); - let result = await staking.stake("100", inThreeYears, a2, a2, { from: a2 }); - await mineBlock(); - - let maxVotingWeight = await staking.MAX_VOTING_WEIGHT.call(); - let maxDuration = await staking.MAX_DURATION.call(); - let weightFactor = await staking.WEIGHT_FACTOR.call(); - - // power on kickoff date - let expectedPower = weightingFunction(200, DELAY * (26 * 3), maxDuration, maxVotingWeight, weightFactor.toNumber()); - let totalVotingPower = await staking.getPriorWeightedStake(a2, result.receipt.blockNumber, kickoffTS); - await expect(totalVotingPower.toNumber()).to.be.equal(expectedPower); - - // power 52 weeks later - expectedPower = weightingFunction(200, DELAY * (26 * 2), maxDuration, maxVotingWeight, weightFactor.toNumber()); - totalVotingPower = await staking.getPriorWeightedStake(a2, result.receipt.blockNumber, kickoffTS.add(new BN(DELAY * 26))); - await expect(totalVotingPower.toNumber()).to.be.equal(expectedPower); - }); - - it("should be unable to compute the weighted stake for the current block", async () => { - let result = await staking.stake("100", inOneYear, a3, a3, { from: a2 }); - await expectRevert(staking.getPriorWeightedStake(a3, result.receipt.blockNumber, kickoffTS), "not determined"); - }); - }); - - describe("vested weighted stake computation", () => { - it("should compute the expected vesting weighted stake", async () => { - await createVestingContractWithSingleDate(3 * 52 * WEEK, 100, token, staking, root); - await createVestingContractWithSingleDate(2 * 52 * WEEK, 100, token, staking, root); - let { blockNumber } = await createVestingContractWithSingleDate(1 * 52 * WEEK, 100, token, staking, root); - await mineBlock(); - - let maxVotingWeight = await staking.MAX_VOTING_WEIGHT.call(); - let maxDuration = await staking.MAX_DURATION.call(); - let weightFactor = await staking.WEIGHT_FACTOR.call(); - - //power on kickoff date - let expectedPower = - weightingFunction(100, DELAY * (26 * 3), maxDuration, maxVotingWeight, weightFactor.toNumber()) + - weightingFunction(100, DELAY * 26 * 2, maxDuration, maxVotingWeight, weightFactor.toNumber()) + - weightingFunction(100, DELAY * 26, maxDuration, maxVotingWeight, weightFactor.toNumber()); - let totalVotingPower = await staking.getPriorTotalVotingPower(blockNumber, kickoffTS); - await expect(totalVotingPower.toNumber()).to.be.equal(expectedPower); - let vestedVotingPower = await staking.getPriorVestingWeightedStake(blockNumber, kickoffTS); - await expect(vestedVotingPower.toNumber()).to.be.equal(expectedPower); - - //power 52 weeks later - expectedPower = - weightingFunction(100, DELAY * (26 * 2), maxDuration, maxVotingWeight, weightFactor.toNumber()) + - weightingFunction(100, DELAY * 26 * 1, maxDuration, maxVotingWeight, weightFactor.toNumber()) + - weightingFunction(100, DELAY * 26 * 0, maxDuration, maxVotingWeight, weightFactor.toNumber()); - totalVotingPower = await staking.getPriorTotalVotingPower(blockNumber, kickoffTS.add(new BN(DELAY * 26))); - await expect(totalVotingPower.toNumber()).to.be.equal(expectedPower); - vestedVotingPower = await staking.getPriorVestingWeightedStake(blockNumber, kickoffTS.add(new BN(DELAY * 26))); - await expect(vestedVotingPower.toNumber()).to.be.equal(expectedPower); - }); - }); - - describe("general weight computation", () => { - it("should compute the expected weight for every staking duration", async () => { - let kickoffTS = await staking.kickoffTS.call(); - let maxVotingWeight = await staking.MAX_VOTING_WEIGHT.call(); - let maxDuration = await staking.MAX_DURATION.call(); - let weightFactor = await staking.WEIGHT_FACTOR.call(); - let expectedWeight; - for (let i = 0; i <= 78; i++) { - expectedWeight = weightingFunction(100, i * DELAY, maxDuration, maxVotingWeight, weightFactor.toNumber()); - let newTime = kickoffTS.add(new BN(i * DELAY)); - let w = Math.floor((100 * (await staking.computeWeightByDate(newTime, kickoffTS)).toNumber()) / weightFactor.toNumber()); - await expect(w).to.be.equal(expectedWeight); - // console.log(expectedWeight); - } - }); - }); + const name = "Test token"; + const symbol = "TST"; + + let root, a1, a2, a3; + let token, staking; + let kickoffTS, inTwoWeeks, inOneYear, inTwoYears, inThreeYears; + + async function deploymentAndInitFixture(_wallets, _provider) { + token = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); + + let stakingLogic = await StakingLogic.new(token.address); + staking = await StakingProxy.new(token.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); + + await token.transfer(a2, "1000"); + await token.approve(staking.address, "1000", { from: a2 }); + + kickoffTS = await staking.kickoffTS.call(); + inTwoWeeks = kickoffTS.add(new BN(DELAY)); + inOneYear = kickoffTS.add(new BN(DELAY * 26)); + inTwoYears = kickoffTS.add(new BN(DELAY * 26 * 2)); + inThreeYears = kickoffTS.add(new BN(DELAY * 26 * 3)); + } + + before(async () => { + [root, a1, a2, a3, ...accounts] = accounts; + }); + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + describe("numCheckpoints", () => { + it("returns the number of checkpoints for a user", async () => { + await expect( + (await staking.numUserStakingCheckpoints.call(a1, inTwoWeeks)).toString() + ).to.be.equal("0"); + + await staking.stake("100", inTwoWeeks, a1, a1, { from: a2 }); + await expect( + (await staking.numUserStakingCheckpoints.call(a1, inTwoWeeks)).toString() + ).to.be.equal("1"); + + await expect(await staking.stake("50", inTwoWeeks, a1, a1, { from: a2 })); + await expect( + (await staking.numUserStakingCheckpoints.call(a1, inTwoWeeks)).toString() + ).to.be.equal("2"); + }); + + it("returns the number of checkpoints for a delegate and date", async () => { + await expect( + (await staking.numDelegateStakingCheckpoints.call(a3, inTwoWeeks)).toString() + ).to.be.equal("0"); + + await staking.stake("100", inTwoWeeks, a1, a3, { from: a2 }); + await expect( + (await staking.numDelegateStakingCheckpoints.call(a3, inTwoWeeks)).toString() + ).to.be.equal("1"); + + await expect(await staking.stake("50", inTwoWeeks, a1, a1, { from: a2 })); + await expect( + (await staking.numDelegateStakingCheckpoints.call(a3, inTwoWeeks)).toString() + ).to.be.equal("2"); + + await staking.stake("100", inTwoWeeks, a2, a3, { from: a2 }); + await expect( + (await staking.numDelegateStakingCheckpoints.call(a3, inTwoWeeks)).toString() + ).to.be.equal("3"); + }); + + it("returns the number of total staking checkpoints for a date", async () => { + await expect( + (await staking.numTotalStakingCheckpoints.call(inTwoWeeks)).toString() + ).to.be.equal("0"); + + await staking.stake("100", inTwoWeeks, a1, a3, { from: a2 }); + await expect( + (await staking.numTotalStakingCheckpoints.call(inTwoWeeks)).toString() + ).to.be.equal("1"); + + await expect(await staking.stake("50", inTwoWeeks, a1, a1, { from: a2 })); + await expect( + (await staking.numTotalStakingCheckpoints.call(inTwoWeeks)).toString() + ).to.be.equal("2"); + + await staking.stake("100", inTwoWeeks, a2, a3, { from: a2 }); + await expect( + (await staking.numTotalStakingCheckpoints.call(inTwoWeeks)).toString() + ).to.be.equal("3"); + }); + }); + + describe("checkpoints", () => { + it("returns the correct checkpoint for an user", async () => { + // shortest staking duration + let result = await staking.stake("100", inTwoWeeks, a1, a3, { from: a2 }); + await expect((await staking.balanceOf(a1)).toString()).to.be.equal("100"); + let checkpoint = await staking.userStakingCheckpoints(a1, inTwoWeeks, 0); + + await expect(checkpoint.fromBlock.toNumber()).to.be.equal(result.receipt.blockNumber); + await expect(checkpoint.stake.toString()).to.be.equal("100"); + + // max staking duration + result = await staking.stake("100", inThreeYears, a2, a3, { from: a2 }); + checkpoint = await staking.userStakingCheckpoints(a2, inThreeYears, 0); + await expect(checkpoint.fromBlock.toNumber()).to.be.equal(result.receipt.blockNumber); + await expect(checkpoint.stake.toString()).to.be.equal("100"); + }); + + it("returns the correct checkpoint for a delegate", async () => { + let result = await staking.stake("100", inTwoWeeks, a1, a3, { from: a2 }); + await expect((await staking.balanceOf(a1)).toString()).to.be.equal("100"); + + let checkpoint = await staking.delegateStakingCheckpoints(a3, inTwoWeeks, 0); + await expect(checkpoint.fromBlock.toNumber()).to.be.equal(result.receipt.blockNumber); + await expect(checkpoint.stake.toString()).to.be.equal("100"); + + // add stake and change delegate + result = await staking.stake("200", inTwoWeeks, a1, a2, { from: a2 }); + await expect((await staking.balanceOf(a1)).toString()).to.be.equal("300"); + + // old delegate + checkpoint = await staking.delegateStakingCheckpoints(a3, inTwoWeeks, 1); + await expect(checkpoint.fromBlock.toNumber()).to.be.equal(result.receipt.blockNumber); + await expect(checkpoint.stake.toString()).to.be.equal("0"); + + // new delegate + checkpoint = await staking.delegateStakingCheckpoints(a2, inTwoWeeks, 0); + await expect(checkpoint.fromBlock.toNumber()).to.be.equal(result.receipt.blockNumber); + await expect(checkpoint.stake.toString()).to.be.equal("300"); + }); + + it("returns the correct checkpoint for a total stakes", async () => { + let result = await staking.stake("100", inTwoWeeks, a1, a3, { from: a2 }); + await expect((await staking.balanceOf(a1)).toString()).to.be.equal("100"); + let checkpoint = await staking.totalStakingCheckpoints(inTwoWeeks, 0); + + await expect(checkpoint.fromBlock.toNumber()).to.be.equal(result.receipt.blockNumber); + await expect(checkpoint.stake.toString()).to.be.equal("100"); + }); + + it("returns the correct checkpoint for vested stakes", async () => { + //verify that regular staking does not create a vesting checkpoint + await staking.stake("100", inTwoWeeks, a1, a3, { from: a2 }); + await expect( + (await staking.numVestingCheckpoints(kickoffTS.add(new BN(DELAY)))).toNumber() + ).to.be.equal(0); + + //verify that vested staking does + let { vestingInstance, blockNumber } = await createVestingContractWithSingleDate( + 2 * WEEK, + 1000, + token, + staking, + root + ); + + await expect( + (await staking.balanceOf(vestingInstance.address)).toString() + ).to.be.equal("1000"); + + await expect( + (await staking.numVestingCheckpoints(kickoffTS.add(new BN(DELAY)))).toNumber() + ).to.be.equal(1); + + checkpoint = await staking.vestingCheckpoints(kickoffTS.add(new BN(DELAY)), 0); + + await expect(checkpoint.fromBlock.toNumber()).to.be.equal(blockNumber); + await expect(checkpoint.stake.toString()).to.be.equal("1000"); + }); + }); + + describe("total voting power computation", () => { + it("should compute the expected voting power", async () => { + await staking.stake("100", inThreeYears, a1, a2, { from: a2 }); + await staking.stake("100", inTwoYears, a2, a2, { from: a2 }); + let result = await staking.stake("100", inOneYear, a3, a3, { from: a2 }); + await mineBlock(); + + let maxVotingWeight = await staking.MAX_VOTING_WEIGHT.call(); + let maxDuration = await staking.MAX_DURATION.call(); + let weightFactor = await staking.WEIGHT_FACTOR.call(); + + // power on kickoff date + let expectedPower = + weightingFunction( + 100, + DELAY * (26 * 3), + maxDuration, + maxVotingWeight, + weightFactor.toNumber() + ) + + weightingFunction( + 100, + DELAY * 26 * 2, + maxDuration, + maxVotingWeight, + weightFactor.toNumber() + ) + + weightingFunction( + 100, + DELAY * 26, + maxDuration, + maxVotingWeight, + weightFactor.toNumber() + ); + let totalVotingPower = await staking.getPriorTotalVotingPower( + result.receipt.blockNumber, + kickoffTS + ); + await expect(totalVotingPower.toNumber()).to.be.equal(expectedPower); + + // power 52 weeks later + expectedPower = + weightingFunction( + 100, + DELAY * (26 * 2), + maxDuration, + maxVotingWeight, + weightFactor.toNumber() + ) + + weightingFunction( + 100, + DELAY * 26 * 1, + maxDuration, + maxVotingWeight, + weightFactor.toNumber() + ) + + weightingFunction( + 100, + DELAY * 26 * 0, + maxDuration, + maxVotingWeight, + weightFactor.toNumber() + ); + totalVotingPower = await staking.getPriorTotalVotingPower( + result.receipt.blockNumber, + kickoffTS.add(new BN(DELAY * 26)) + ); + await expect(totalVotingPower.toNumber()).to.be.equal(expectedPower); + }); + + it("should be unable to compute the total voting power for the current block", async () => { + let result = await staking.stake("100", inOneYear, a3, a3, { from: a2 }); + await expectRevert( + staking.getPriorTotalVotingPower(result.receipt.blockNumber, kickoffTS), + "not yet determined" + ); + }); + }); + + describe("delegated voting power computation", () => { + it("should compute the expected voting power", async () => { + await staking.stake("100", inThreeYears, a1, a2, { from: a2 }); + await staking.stake("100", inTwoYears, a2, a3, { from: a2 }); + let result = await staking.stake("100", inOneYear, a3, a2, { from: a2 }); + await mineBlock(); + + let maxVotingWeight = await staking.MAX_VOTING_WEIGHT.call(); + let maxDuration = await staking.MAX_DURATION.call(); + let weightFactor = await staking.WEIGHT_FACTOR.call(); + + // power on kickoff date + let expectedPower = + weightingFunction( + 100, + DELAY * (26 * 3), + maxDuration, + maxVotingWeight, + weightFactor.toNumber() + ) + + weightingFunction( + 100, + DELAY * 26, + maxDuration, + maxVotingWeight, + weightFactor.toNumber() + ); + let totalVotingPower = await staking.getPriorVotes( + a2, + result.receipt.blockNumber, + kickoffTS + ); + await expect(totalVotingPower.toNumber()).to.be.equal(expectedPower); + + // power 52 weeks later + expectedPower = + weightingFunction( + 100, + DELAY * (26 * 2), + maxDuration, + maxVotingWeight, + weightFactor.toNumber() + ) + + weightingFunction( + 100, + DELAY * 26 * 0, + maxDuration, + maxVotingWeight, + weightFactor.toNumber() + ); + totalVotingPower = await staking.getPriorVotes( + a2, + result.receipt.blockNumber, + kickoffTS.add(new BN(DELAY * 26)) + ); + await expect(totalVotingPower.toNumber()).to.be.equal(expectedPower); + }); + + it("should be unable to compute the voting power for the current block", async () => { + let result = await staking.stake("100", inOneYear, a3, a3, { from: a2 }); + await expectRevert( + staking.getPriorVotes(a3, result.receipt.blockNumber, kickoffTS), + "not determined yet" + ); + }); + + it("should return the current votes", async () => { + await staking.stake("100", inThreeYears, a2, a2, { from: a2 }); + await mineBlock(); + + let maxVotingWeight = await staking.MAX_VOTING_WEIGHT.call(); + let maxDuration = await staking.MAX_DURATION.call(); + let weightFactor = await staking.WEIGHT_FACTOR.call(); + + let expectedPower = weightingFunction( + 100, + DELAY * (26 * 3), + maxDuration, + maxVotingWeight, + weightFactor.toNumber() + ); + let currentVotes = await staking.getCurrentVotes.call(a2); + await expect(currentVotes.toNumber()).to.be.equal(expectedPower); + }); + }); + + describe("user weighted stake computation", () => { + it("should compute the expected weighted stake", async () => { + await staking.stake("100", inThreeYears, a2, a2, { from: a2 }); + await staking.stake("100", inTwoYears, a1, a3, { from: a2 }); + let result = await staking.stake("100", inThreeYears, a2, a2, { from: a2 }); + await mineBlock(); + + let maxVotingWeight = await staking.MAX_VOTING_WEIGHT.call(); + let maxDuration = await staking.MAX_DURATION.call(); + let weightFactor = await staking.WEIGHT_FACTOR.call(); + + // power on kickoff date + let expectedPower = weightingFunction( + 200, + DELAY * (26 * 3), + maxDuration, + maxVotingWeight, + weightFactor.toNumber() + ); + let totalVotingPower = await staking.getPriorWeightedStake( + a2, + result.receipt.blockNumber, + kickoffTS + ); + await expect(totalVotingPower.toNumber()).to.be.equal(expectedPower); + + // power 52 weeks later + expectedPower = weightingFunction( + 200, + DELAY * (26 * 2), + maxDuration, + maxVotingWeight, + weightFactor.toNumber() + ); + totalVotingPower = await staking.getPriorWeightedStake( + a2, + result.receipt.blockNumber, + kickoffTS.add(new BN(DELAY * 26)) + ); + await expect(totalVotingPower.toNumber()).to.be.equal(expectedPower); + }); + + it("should be unable to compute the weighted stake for the current block", async () => { + let result = await staking.stake("100", inOneYear, a3, a3, { from: a2 }); + await expectRevert( + staking.getPriorWeightedStake(a3, result.receipt.blockNumber, kickoffTS), + "not determined" + ); + }); + }); + + describe("vested weighted stake computation", () => { + it("should compute the expected vesting weighted stake", async () => { + await createVestingContractWithSingleDate(3 * 52 * WEEK, 100, token, staking, root); + await createVestingContractWithSingleDate(2 * 52 * WEEK, 100, token, staking, root); + let { blockNumber } = await createVestingContractWithSingleDate( + 1 * 52 * WEEK, + 100, + token, + staking, + root + ); + await mineBlock(); + + let maxVotingWeight = await staking.MAX_VOTING_WEIGHT.call(); + let maxDuration = await staking.MAX_DURATION.call(); + let weightFactor = await staking.WEIGHT_FACTOR.call(); + + //power on kickoff date + let expectedPower = + weightingFunction( + 100, + DELAY * (26 * 3), + maxDuration, + maxVotingWeight, + weightFactor.toNumber() + ) + + weightingFunction( + 100, + DELAY * 26 * 2, + maxDuration, + maxVotingWeight, + weightFactor.toNumber() + ) + + weightingFunction( + 100, + DELAY * 26, + maxDuration, + maxVotingWeight, + weightFactor.toNumber() + ); + let totalVotingPower = await staking.getPriorTotalVotingPower(blockNumber, kickoffTS); + await expect(totalVotingPower.toNumber()).to.be.equal(expectedPower); + let vestedVotingPower = await staking.getPriorVestingWeightedStake( + blockNumber, + kickoffTS + ); + await expect(vestedVotingPower.toNumber()).to.be.equal(expectedPower); + + //power 52 weeks later + expectedPower = + weightingFunction( + 100, + DELAY * (26 * 2), + maxDuration, + maxVotingWeight, + weightFactor.toNumber() + ) + + weightingFunction( + 100, + DELAY * 26 * 1, + maxDuration, + maxVotingWeight, + weightFactor.toNumber() + ) + + weightingFunction( + 100, + DELAY * 26 * 0, + maxDuration, + maxVotingWeight, + weightFactor.toNumber() + ); + totalVotingPower = await staking.getPriorTotalVotingPower( + blockNumber, + kickoffTS.add(new BN(DELAY * 26)) + ); + await expect(totalVotingPower.toNumber()).to.be.equal(expectedPower); + vestedVotingPower = await staking.getPriorVestingWeightedStake( + blockNumber, + kickoffTS.add(new BN(DELAY * 26)) + ); + await expect(vestedVotingPower.toNumber()).to.be.equal(expectedPower); + }); + }); + + describe("general weight computation", () => { + it("should compute the expected weight for every staking duration", async () => { + let kickoffTS = await staking.kickoffTS.call(); + let maxVotingWeight = await staking.MAX_VOTING_WEIGHT.call(); + let maxDuration = await staking.MAX_DURATION.call(); + let weightFactor = await staking.WEIGHT_FACTOR.call(); + let expectedWeight; + for (let i = 0; i <= 78; i++) { + expectedWeight = weightingFunction( + 100, + i * DELAY, + maxDuration, + maxVotingWeight, + weightFactor.toNumber() + ); + let newTime = kickoffTS.add(new BN(i * DELAY)); + let w = Math.floor( + (100 * (await staking.computeWeightByDate(newTime, kickoffTS)).toNumber()) / + weightFactor.toNumber() + ); + await expect(w).to.be.equal(expectedWeight); + // console.log(expectedWeight); + } + }); + }); }); async function updateTime(staking, multiplier) { - let kickoffTS = await staking.kickoffTS.call(); - let newTime = kickoffTS.add(new BN(DELAY).mul(new BN(multiplier))); - await setTime(newTime); - return newTime; + let kickoffTS = await staking.kickoffTS.call(); + let newTime = kickoffTS.add(new BN(DELAY).mul(new BN(multiplier))); + await setTime(newTime); + return newTime; } function weightingFunction(stake, time, maxDuration, maxVotingWeight, weightFactor) { - let x = maxDuration - time; - let mD2 = maxDuration * maxDuration; - return Math.floor((stake * (Math.floor((maxVotingWeight * weightFactor * (mD2 - x * x)) / mD2) + weightFactor)) / weightFactor); + let x = maxDuration - time; + let mD2 = maxDuration * maxDuration; + return Math.floor( + (stake * + (Math.floor((maxVotingWeight * weightFactor * (mD2 - x * x)) / mD2) + weightFactor)) / + weightFactor + ); } async function createVestingContractWithSingleDate(cliff, amount, token, staking, tokenOwner) { - vestingLogic = await VestingLogic.new(); - let vestingInstance = await Vesting.new(vestingLogic.address, token.address, staking.address, tokenOwner, cliff, cliff, tokenOwner); - vestingInstance = await VestingLogic.at(vestingInstance.address); - //important, so it's recognized as vesting contract - await staking.addContractCodeHash(vestingInstance.address); - - await token.approve(vestingInstance.address, amount); - let result = await vestingInstance.stakeTokens(amount); - return { vestingInstance: vestingInstance, blockNumber: result.receipt.blockNumber }; + vestingLogic = await VestingLogic.new(); + let vestingInstance = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + tokenOwner, + cliff, + cliff, + tokenOwner + ); + vestingInstance = await VestingLogic.at(vestingInstance.address); + //important, so it's recognized as vesting contract + await staking.addContractCodeHash(vestingInstance.address); + + await token.approve(vestingInstance.address, amount); + let result = await vestingInstance.stakeTokens(amount); + return { vestingInstance: vestingInstance, blockNumber: result.receipt.blockNumber }; } diff --git a/tests/stakingRewards/Rewards.js b/tests/stakingRewards/Rewards.js index 8bee140a3..59356273b 100644 --- a/tests/stakingRewards/Rewards.js +++ b/tests/stakingRewards/Rewards.js @@ -32,198 +32,222 @@ const TWO_WEEKS = 1209600; const DELAY = TWO_WEEKS; contract("StakingRewards - First Period", (accounts) => { - let root, a1, a2, a3, a4, a5; - let SOV, staking; - let kickoffTS, inOneYear, inTwoYears, inThreeYears; - - before(async () => { - [root, a1, a2, a3, a4, a5, ...accounts] = accounts; - SOV = await SOV_ABI.new(TOTAL_SUPPLY); - - // BlockMockUp - blockMockUp = await BlockMockUp.new(); - - // Deployed Staking Functionality - let stakingLogic = await StakingLogic.new(SOV.address); - staking = await StakingProxy.new(SOV.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingLogic.at(staking.address); - - //Upgradable Vesting Registry - vestingRegistryLogic = await VestingRegistryLogic.new(); - vesting = await VestingRegistryProxy.new(); - await vesting.setImplementation(vestingRegistryLogic.address); - vesting = await VestingRegistryLogic.at(vesting.address); - await staking.setVestingRegistry(vesting.address); - - kickoffTS = await staking.kickoffTS.call(); - inOneWeek = kickoffTS.add(new BN(DELAY)); - inOneYear = kickoffTS.add(new BN(DELAY * 26)); - inTwoYears = kickoffTS.add(new BN(DELAY * 26 * 2)); - inThreeYears = kickoffTS.add(new BN(DELAY * 26 * 3)); - - // Transferred SOVs to a1 - await SOV.transfer(a1, wei("10000", "ether")); - await SOV.approve(staking.address, wei("10000", "ether"), { from: a1 }); - - // Transferred SOVs to a2 - await SOV.transfer(a2, wei("50000", "ether")); - await SOV.approve(staking.address, wei("50000", "ether"), { from: a2 }); - - // Transferred SOVs to a3 - await SOV.transfer(a3, wei("10000", "ether")); - await SOV.approve(staking.address, wei("10000", "ether"), { from: a3 }); - - let latest = await blockNumber(); - let blockNum = new BN(latest).add(new BN(291242 / 30)); - await blockMockUp.setBlockNum(blockNum); - await increaseTime(291242); - await staking.stake(wei("10000", "ether"), inThreeYears, a3, a3, { from: a3 }); - - // Staking Reward Program is deployed - let stakingRewardsLogic = await StakingRewards.new(); - stakingRewards = await StakingRewardsProxy.new(); - await stakingRewards.setImplementation(stakingRewardsLogic.address); - stakingRewards = await StakingRewards.at(stakingRewards.address); - stakingRewards.setAverageBlockTime(30); - await stakingRewards.setBlockMockUpAddr(blockMockUp.address); - await staking.setBlockMockUpAddr(blockMockUp.address); - }); - - describe("Flow - StakingRewards", () => { - it("should revert if SOV Address is invalid", async () => { - await expectRevert(stakingRewards.initialize(constants.ZERO_ADDRESS, staking.address), "Invalid SOV Address."); - // Staking Rewards Contract is loaded - await SOV.transfer(stakingRewards.address, wei("1000000", "ether")); - // Initialize - await stakingRewards.initialize(SOV.address, staking.address); // Test - 24/08/2021 - await increaseTimeAndBlocks(100800); - await staking.stake(wei("1000", "ether"), inOneYear, a1, a1, { from: a1 }); // Staking after program is initialised - await increaseTimeAndBlocks(100800); - await staking.stake(wei("50000", "ether"), inTwoYears, a2, a2, { from: a2 }); - }); - - it("should account for stakes made till start date of the program for a1", async () => { - await increaseTimeAndBlocks(1209614); - - let fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a1 }); - let numOfIntervals = 1; - let fullTermAvg = avgWeight(26, 27, 9, 78); - let expectedAmount = numOfIntervals * ((1000 * fullTermAvg) / 26); - expect(new BN(Math.floor(expectedAmount * 10 ** 10))).to.be.bignumber.equal( - new BN(fields.amount).div(new BN(10).pow(new BN(8))) - ); - }); - - it("should account for stakes made till start date of the program for a2", async () => { - let numOfIntervals = 1; - fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a2 }); - fullTermAvg = avgWeight(52, 53, 9, 78); - expectedAmount = numOfIntervals * ((50000 * fullTermAvg) / 26); - expect(new BN(Math.floor(expectedAmount * 10 ** 10))).to.be.bignumber.equal( - new BN(fields.amount).div(new BN(10).pow(new BN(8))) - ); - }); - - it("should account for stakes made till start date of the program for a3", async () => { - let numOfIntervals = 1; - fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a3 }); - fullTermAvg = avgWeight(78, 79, 9, 78); - expectedAmount = numOfIntervals * ((10000 * fullTermAvg) / 26); - expect(new BN(Math.floor(expectedAmount * 10 ** 10))).to.be.bignumber.equal( - new BN(fields.amount).div(new BN(10).pow(new BN(8))) - ); - }); - - it("should compute and send Rewards to the stakers a1, a2 and a3 correctly after 4 weeks", async () => { - await increaseTimeAndBlocks(1209614); - await stakingRewards.setBlock(); - let startTime = await stakingRewards.startTime(); - await stakingRewards.setHistoricalBlock(parseInt(startTime)); - await stakingRewards.setHistoricalBlock(parseInt(startTime) + 1209600); - let fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a1 }); - let numOfIntervals = 2; - let fullTermAvg = avgWeight(25, 27, 9, 78); - expectedAmount = numOfIntervals * ((1000 * fullTermAvg) / 26); - expect(new BN(Math.floor(expectedAmount * 10 ** 10))).to.be.bignumber.equal( - new BN(fields.amount).div(new BN(10).pow(new BN(8))) - ); - - fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a2 }); - fullTermAvg = avgWeight(51, 53, 9, 78); - expectedAmount = numOfIntervals * ((50000 * fullTermAvg) / 26); - expect(new BN(Math.floor(expectedAmount * 10 ** 10))).to.be.bignumber.equal( - new BN(fields.amount).div(new BN(10).pow(new BN(8))) - ); - - fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a3 }); - fullTermAvg = avgWeight(77, 79, 9, 78); - expectedAmount = numOfIntervals * ((10000 * fullTermAvg) / 26); - expect(new BN(Math.floor(expectedAmount * 10 ** 10))).to.be.bignumber.equal( - new BN(fields.amount).div(new BN(10).pow(new BN(8))) - ); - }); - - it("should compute and send Rewards to the stakers a1 after 6 weeks", async () => { - await increaseTimeAndBlocks(1209614); - - let fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a1 }); - let numOfIntervals = 3; - let fullTermAvg = avgWeight(24, 27, 9, 78); - expectedAmount = numOfIntervals * ((1000 * fullTermAvg) / 26); - expect(new BN(Math.floor(expectedAmount * 10 ** 10))).to.be.bignumber.equal( - new BN(fields.amount).div(new BN(10).pow(new BN(8))) - ); - }); - - it("should be able to stake and get rewards after 30 weeks", async () => { - let block = await web3.eth.getBlock("latest"); - let timestamp = block.timestamp; - let startTime = await stakingRewards.startTime(); - await increaseTimeAndBlocks(12096000); // 20 weeks - - // Transferred SOVs to a4 - await SOV.transfer(a4, wei("10000", "ether")); - await SOV.approve(staking.address, wei("8000", "ether"), { from: a4 }); - - // Transferred SOVs to a5 - await SOV.transfer(a5, wei("10000", "ether")); - await SOV.approve(staking.address, wei("8000", "ether"), { from: a5 }); - - // Stake - await staking.stake(wei("8000", "ether"), new BN(timestamp).add(new BN(TWO_WEEKS * 10 * 26)), a4, a4, { from: a4 }); - await staking.stake(wei("8000", "ether"), new BN(timestamp).add(new BN(TWO_WEEKS * 10 * 26)), a5, a5, { from: a5 }); - - await increaseTimeAndBlocks(3628800); // 6 Weeks - - let tx = await stakingRewards.collectReward(0, { from: a4 }); - console.log("when restartTime = ", 0, ", gasUsed: " + tx.receipt.gasUsed); // 2.6M - - // Using restartTime saves gas - let restartTime = new BN(startTime).add(new BN(13 * 1209600)); - tx = await stakingRewards.collectReward(restartTime, { from: a5 }); - console.log("when restartTime = ", restartTime.toString(), ", gasUsed: " + tx.receipt.gasUsed); // 0.7M - - // let fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a4 }); - // console.log(fields.amount.toString()); - // let fields = await stakingRewards.getStakerCurrentReward(true, restartTime, { from: a4 }); - // console.log(fields.amount.toString()); - }); - }); - - function avgWeight(from, to, maxWeight, maxDuration) { - let weight = 0; - for (let i = from; i < to; i++) { - weight += Math.floor(((maxWeight * (maxDuration ** 2 - (maxDuration - i) ** 2)) / maxDuration ** 2 + 1) * 10, 2); - } - weight /= to - from; - return (weight / 100) * 0.2975; - } - - async function increaseTimeAndBlocks(seconds) { - let latest = await blockMockUp.getBlockNum(); - let blockNum = new BN(latest).add(new BN(seconds / 30)); - await blockMockUp.setBlockNum(blockNum); - await increaseTime(seconds); - } + let root, a1, a2, a3, a4, a5; + let SOV, staking; + let kickoffTS, inOneYear, inTwoYears, inThreeYears; + + before(async () => { + [root, a1, a2, a3, a4, a5, ...accounts] = accounts; + SOV = await SOV_ABI.new(TOTAL_SUPPLY); + + // BlockMockUp + blockMockUp = await BlockMockUp.new(); + + // Deployed Staking Functionality + let stakingLogic = await StakingLogic.new(SOV.address); + staking = await StakingProxy.new(SOV.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); + + //Upgradable Vesting Registry + vestingRegistryLogic = await VestingRegistryLogic.new(); + vesting = await VestingRegistryProxy.new(); + await vesting.setImplementation(vestingRegistryLogic.address); + vesting = await VestingRegistryLogic.at(vesting.address); + await staking.setVestingRegistry(vesting.address); + + kickoffTS = await staking.kickoffTS.call(); + inOneWeek = kickoffTS.add(new BN(DELAY)); + inOneYear = kickoffTS.add(new BN(DELAY * 26)); + inTwoYears = kickoffTS.add(new BN(DELAY * 26 * 2)); + inThreeYears = kickoffTS.add(new BN(DELAY * 26 * 3)); + + // Transferred SOVs to a1 + await SOV.transfer(a1, wei("10000", "ether")); + await SOV.approve(staking.address, wei("10000", "ether"), { from: a1 }); + + // Transferred SOVs to a2 + await SOV.transfer(a2, wei("50000", "ether")); + await SOV.approve(staking.address, wei("50000", "ether"), { from: a2 }); + + // Transferred SOVs to a3 + await SOV.transfer(a3, wei("10000", "ether")); + await SOV.approve(staking.address, wei("10000", "ether"), { from: a3 }); + + let latest = await blockNumber(); + let blockNum = new BN(latest).add(new BN(291242 / 30)); + await blockMockUp.setBlockNum(blockNum); + await increaseTime(291242); + await staking.stake(wei("10000", "ether"), inThreeYears, a3, a3, { from: a3 }); + + // Staking Reward Program is deployed + let stakingRewardsLogic = await StakingRewards.new(); + stakingRewards = await StakingRewardsProxy.new(); + await stakingRewards.setImplementation(stakingRewardsLogic.address); + stakingRewards = await StakingRewards.at(stakingRewards.address); + stakingRewards.setAverageBlockTime(30); + await stakingRewards.setBlockMockUpAddr(blockMockUp.address); + await staking.setBlockMockUpAddr(blockMockUp.address); + }); + + describe("Flow - StakingRewards", () => { + it("should revert if SOV Address is invalid", async () => { + await expectRevert( + stakingRewards.initialize(constants.ZERO_ADDRESS, staking.address), + "Invalid SOV Address." + ); + // Staking Rewards Contract is loaded + await SOV.transfer(stakingRewards.address, wei("1000000", "ether")); + // Initialize + await stakingRewards.initialize(SOV.address, staking.address); // Test - 24/08/2021 + await increaseTimeAndBlocks(100800); + await staking.stake(wei("1000", "ether"), inOneYear, a1, a1, { from: a1 }); // Staking after program is initialised + await increaseTimeAndBlocks(100800); + await staking.stake(wei("50000", "ether"), inTwoYears, a2, a2, { from: a2 }); + }); + + it("should account for stakes made till start date of the program for a1", async () => { + await increaseTimeAndBlocks(1209614); + + let fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a1 }); + let numOfIntervals = 1; + let fullTermAvg = avgWeight(26, 27, 9, 78); + let expectedAmount = numOfIntervals * ((1000 * fullTermAvg) / 26); + expect(new BN(Math.floor(expectedAmount * 10 ** 10))).to.be.bignumber.equal( + new BN(fields.amount).div(new BN(10).pow(new BN(8))) + ); + }); + + it("should account for stakes made till start date of the program for a2", async () => { + let numOfIntervals = 1; + fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a2 }); + fullTermAvg = avgWeight(52, 53, 9, 78); + expectedAmount = numOfIntervals * ((50000 * fullTermAvg) / 26); + expect(new BN(Math.floor(expectedAmount * 10 ** 10))).to.be.bignumber.equal( + new BN(fields.amount).div(new BN(10).pow(new BN(8))) + ); + }); + + it("should account for stakes made till start date of the program for a3", async () => { + let numOfIntervals = 1; + fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a3 }); + fullTermAvg = avgWeight(78, 79, 9, 78); + expectedAmount = numOfIntervals * ((10000 * fullTermAvg) / 26); + expect(new BN(Math.floor(expectedAmount * 10 ** 10))).to.be.bignumber.equal( + new BN(fields.amount).div(new BN(10).pow(new BN(8))) + ); + }); + + it("should compute and send Rewards to the stakers a1, a2 and a3 correctly after 4 weeks", async () => { + await increaseTimeAndBlocks(1209614); + await stakingRewards.setBlock(); + let startTime = await stakingRewards.startTime(); + await stakingRewards.setHistoricalBlock(parseInt(startTime)); + await stakingRewards.setHistoricalBlock(parseInt(startTime) + 1209600); + let fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a1 }); + let numOfIntervals = 2; + let fullTermAvg = avgWeight(25, 27, 9, 78); + expectedAmount = numOfIntervals * ((1000 * fullTermAvg) / 26); + expect(new BN(Math.floor(expectedAmount * 10 ** 10))).to.be.bignumber.equal( + new BN(fields.amount).div(new BN(10).pow(new BN(8))) + ); + + fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a2 }); + fullTermAvg = avgWeight(51, 53, 9, 78); + expectedAmount = numOfIntervals * ((50000 * fullTermAvg) / 26); + expect(new BN(Math.floor(expectedAmount * 10 ** 10))).to.be.bignumber.equal( + new BN(fields.amount).div(new BN(10).pow(new BN(8))) + ); + + fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a3 }); + fullTermAvg = avgWeight(77, 79, 9, 78); + expectedAmount = numOfIntervals * ((10000 * fullTermAvg) / 26); + expect(new BN(Math.floor(expectedAmount * 10 ** 10))).to.be.bignumber.equal( + new BN(fields.amount).div(new BN(10).pow(new BN(8))) + ); + }); + + it("should compute and send Rewards to the stakers a1 after 6 weeks", async () => { + await increaseTimeAndBlocks(1209614); + + let fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a1 }); + let numOfIntervals = 3; + let fullTermAvg = avgWeight(24, 27, 9, 78); + expectedAmount = numOfIntervals * ((1000 * fullTermAvg) / 26); + expect(new BN(Math.floor(expectedAmount * 10 ** 10))).to.be.bignumber.equal( + new BN(fields.amount).div(new BN(10).pow(new BN(8))) + ); + }); + + it("should be able to stake and get rewards after 30 weeks", async () => { + let block = await web3.eth.getBlock("latest"); + let timestamp = block.timestamp; + let startTime = await stakingRewards.startTime(); + await increaseTimeAndBlocks(12096000); // 20 weeks + + // Transferred SOVs to a4 + await SOV.transfer(a4, wei("10000", "ether")); + await SOV.approve(staking.address, wei("8000", "ether"), { from: a4 }); + + // Transferred SOVs to a5 + await SOV.transfer(a5, wei("10000", "ether")); + await SOV.approve(staking.address, wei("8000", "ether"), { from: a5 }); + + // Stake + await staking.stake( + wei("8000", "ether"), + new BN(timestamp).add(new BN(TWO_WEEKS * 10 * 26)), + a4, + a4, + { from: a4 } + ); + await staking.stake( + wei("8000", "ether"), + new BN(timestamp).add(new BN(TWO_WEEKS * 10 * 26)), + a5, + a5, + { from: a5 } + ); + + await increaseTimeAndBlocks(3628800); // 6 Weeks + + let tx = await stakingRewards.collectReward(0, { from: a4 }); + console.log("when restartTime = ", 0, ", gasUsed: " + tx.receipt.gasUsed); // 2.6M + + // Using restartTime saves gas + let restartTime = new BN(startTime).add(new BN(13 * 1209600)); + tx = await stakingRewards.collectReward(restartTime, { from: a5 }); + console.log( + "when restartTime = ", + restartTime.toString(), + ", gasUsed: " + tx.receipt.gasUsed + ); // 0.7M + + // let fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a4 }); + // console.log(fields.amount.toString()); + // let fields = await stakingRewards.getStakerCurrentReward(true, restartTime, { from: a4 }); + // console.log(fields.amount.toString()); + }); + }); + + function avgWeight(from, to, maxWeight, maxDuration) { + let weight = 0; + for (let i = from; i < to; i++) { + weight += Math.floor( + ((maxWeight * (maxDuration ** 2 - (maxDuration - i) ** 2)) / maxDuration ** 2 + + 1) * + 10, + 2 + ); + } + weight /= to - from; + return (weight / 100) * 0.2975; + } + + async function increaseTimeAndBlocks(seconds) { + let latest = await blockMockUp.getBlockNum(); + let blockNum = new BN(latest).add(new BN(seconds / 30)); + await blockMockUp.setBlockNum(blockNum); + await increaseTime(seconds); + } }); diff --git a/tests/stakingRewards/StakingRewards.js b/tests/stakingRewards/StakingRewards.js index 831287e93..df6351ed1 100644 --- a/tests/stakingRewards/StakingRewards.js +++ b/tests/stakingRewards/StakingRewards.js @@ -27,20 +27,20 @@ const { expect } = require("chai"); const { expectRevert, BN, constants } = require("@openzeppelin/test-helpers"); const { increaseTime, blockNumber } = require("../Utils/Ethereum"); const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getLoanTokenLogic, - getLoanToken, - getLoanTokenLogicWrbtc, - getLoanTokenWRBTC, - loan_pool_setup, - set_demand_curve, - getPriceFeeds, - getSovryn, - decodeLogs, - getSOV, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getLoanTokenLogic, + getLoanToken, + getLoanTokenLogicWrbtc, + getLoanTokenWRBTC, + loan_pool_setup, + set_demand_curve, + getPriceFeeds, + getSovryn, + decodeLogs, + getSOV, } = require("../Utils/initializer.js"); const StakingLogic = artifacts.require("StakingMock"); @@ -61,326 +61,348 @@ const TWO_WEEKS = 1209600; const DELAY = TWO_WEEKS; contract("StakingRewards", (accounts) => { - let root, a1, a2, a3; - let SOV, staking; - let kickoffTS, inOneYear, inTwoYears, inThreeYears; - let totalRewards; - - before(async () => { - [root, a1, a2, a3, a4, ...accounts] = accounts; - - // Deploying sovrynProtocol w/ generic function from initializer.js - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - await sovryn.setSovrynProtocolAddress(sovryn.address); - - // Custom tokens - SOV = await getSOV(sovryn, priceFeeds, SUSD, accounts); - - // BlockMockUp - blockMockUp = await BlockMockUp.new(); - - // Deployed Staking Functionality - let stakingLogic = await StakingLogic.new(SOV.address); - staking = await StakingProxy.new(SOV.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingLogic.at(staking.address); // Test - 01/07/2021 - - // FeeSharingProxy - let feeSharingLogic = await FeeSharingLogic.new(); - feeSharingProxyObj = await FeeSharingProxy.new(sovryn.address, staking.address); - await feeSharingProxyObj.setImplementation(feeSharingLogic.address); - feeSharingProxy = await FeeSharingLogic.at(feeSharingProxyObj.address); - await staking.setFeeSharing(feeSharingProxy.address); - - //Upgradable Vesting Registry - vestingRegistryLogic = await VestingRegistryLogic.new(); - vesting = await VestingRegistryProxy.new(); - await vesting.setImplementation(vestingRegistryLogic.address); - vesting = await VestingRegistryLogic.at(vesting.address); - - await staking.setVestingRegistry(vesting.address); - - kickoffTS = await staking.kickoffTS.call(); - inOneWeek = kickoffTS.add(new BN(DELAY)); - inOneYear = kickoffTS.add(new BN(DELAY * 26)); - inTwoYears = kickoffTS.add(new BN(DELAY * 26 * 2)); - inThreeYears = kickoffTS.add(new BN(DELAY * 26 * 3)); - - // Transferred SOVs to a1 - await SOV.transfer(a1, wei("10000", "ether")); - await SOV.approve(staking.address, wei("10000", "ether"), { from: a1 }); - - // Transferred SOVs to a2 - await SOV.transfer(a2, wei("10000", "ether")); - await SOV.approve(staking.address, wei("10000", "ether"), { from: a2 }); - - // Transferred SOVs to a3 - await SOV.transfer(a3, wei("10000", "ether")); - await SOV.approve(staking.address, wei("10000", "ether"), { from: a3 }); - - // Transferred SOVs to a4 - await SOV.transfer(a4, wei("10000", "ether")); - await SOV.approve(staking.address, wei("10000", "ether"), { from: a4 }); - - await staking.stake(wei("1000", "ether"), inOneYear, a1, a1, { from: a1 }); // Test - 15/07/2021 - await staking.stake(wei("1000", "ether"), inTwoYears, a2, a2, { from: a2 }); // Test - 15/07/2021 - await staking.stake(wei("1000", "ether"), inThreeYears, a3, a3, { from: a3 }); - await staking.stake(wei("1000", "ether"), inThreeYears, a4, a4, { from: a4 }); - - let latest = await blockNumber(); - let blockNum = new BN(latest).add(new BN(1295994 / 30)); - await blockMockUp.setBlockNum(blockNum); - await increaseTime(291242); - - // Staking Reward Program is deployed - let stakingRewardsLogic = await StakingRewards.new(); - stakingRewards = await StakingRewardsProxy.new(); - await stakingRewards.setImplementation(stakingRewardsLogic.address); - stakingRewards = await StakingRewards.at(stakingRewards.address); //Test - 12/08/2021 - stakingRewards.setAverageBlockTime(30); - await stakingRewards.setBlockMockUpAddr(blockMockUp.address); - await staking.setBlockMockUpAddr(blockMockUp.address); - }); - - describe("Flow - StakingRewards", () => { - it("should revert if SOV Address is not a contract address", async () => { - await expectRevert(stakingRewards.initialize(a3, staking.address), "_SOV not a contract"); - }); - - it("should revert if SOV Address is invalid", async () => { - await expectRevert(stakingRewards.initialize(constants.ZERO_ADDRESS, staking.address), "Invalid SOV Address."); - // Staking Rewards Contract is loaded - await SOV.transfer(stakingRewards.address, wei("1000000", "ether")); - // Initialize - await stakingRewards.initialize(SOV.address, staking.address); - }); - - it("should revert if rewards are claimed before completion of two weeks from start date", async () => { - await expectRevert(stakingRewards.collectReward(0, { from: a2 }), "no valid reward"); - }); - - it("should compute and send rewards to the stakers a1, a2 and a3 correctly after 2 weeks", async () => { - await increaseTimeAndBlocks(1295994); - - let fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a1 }); - let numOfIntervals = 1; - let fullTermAvg = avgWeight(26, 27, 9, 78); - let expectedAmount = numOfIntervals * ((1000 * fullTermAvg) / 26); - expect(new BN(Math.floor(expectedAmount * 10 ** 10))).to.be.bignumber.equal( - new BN(fields.amount).div(new BN(10).pow(new BN(8))) - ); - - fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a2 }); - fullTermAvg = avgWeight(52, 53, 9, 78); - expectedAmount = numOfIntervals * ((1000 * fullTermAvg) / 26); - expect(new BN(Math.floor(expectedAmount * 10 ** 10))).to.be.bignumber.equal( - new BN(fields.amount).div(new BN(10).pow(new BN(8))) - ); - - fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a3 }); - fullTermAvg = avgWeight(78, 79, 9, 78); - expectedAmount = numOfIntervals * ((1000 * fullTermAvg) / 26); - expect(new BN(Math.floor(expectedAmount * 10 ** 10))).to.be.bignumber.equal( - new BN(fields.amount).div(new BN(10).pow(new BN(8))) - ); - }); - - it("should compute and send rewards to the stakers a1, a2 and a3 correctly after 4 weeks", async () => { - await increaseTimeAndBlocks(1295994); - - let fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a1 }); - let numOfIntervals = 2; - let fullTermAvg = avgWeight(25, 27, 9, 78); - expectedAmount = numOfIntervals * ((1000 * fullTermAvg) / 26); - expect(new BN(Math.floor(expectedAmount * 10 ** 10))).to.be.bignumber.equal( - new BN(fields.amount).div(new BN(10).pow(new BN(8))) - ); - - fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a2 }); - fullTermAvg = avgWeight(51, 53, 9, 78); - expectedAmount = numOfIntervals * ((1000 * fullTermAvg) / 26); - expect(new BN(Math.floor(expectedAmount * 10 ** 10))).to.be.bignumber.equal( - new BN(fields.amount).div(new BN(10).pow(new BN(8))) - ); - - fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a3 }); - fullTermAvg = avgWeight(77, 79, 9, 78); - expectedAmount = numOfIntervals * ((1000 * fullTermAvg) / 26); - expect(new BN(Math.floor(expectedAmount * 10 ** 10))).to.be.bignumber.equal( - new BN(fields.amount).div(new BN(10).pow(new BN(8))) - ); - }); - - it("should compute send rewards to the staker including added staking", async () => { - await increaseTimeAndBlocks(86400); - await staking.stake(wei("3000", "ether"), inTwoYears, a2, a2, { from: a2 }); - - await increaseTimeAndBlocks(1209600); - const fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a2 }); - beforeBalance = await SOV.balanceOf(a2); - await stakingRewards.collectReward(0, { from: a2 }); - afterBalance = await SOV.balanceOf(a2); - rewards = afterBalance.sub(beforeBalance); - expect(rewards).to.be.bignumber.equal(fields.amount); - totalRewards = new BN(totalRewards).add(new BN(rewards)); - expect(afterBalance).to.be.bignumber.greaterThan(beforeBalance); - }); - - it("should revert if the user tries to claim rewards early", async () => { - await increaseTimeAndBlocks(86400); // One day - await expectRevert(stakingRewards.collectReward(0, { from: a2 }), "no valid reward"); - }); - - it("should compute and send rewards to the staker after recalculating withdrawn stake", async () => { - await increaseTimeAndBlocks(32659200); // More than a year - first stake expires - feeSharingProxy = await FeeSharingProxy.new(sovryn.address, staking.address); - await staking.withdraw(wei("1000", "ether"), inTwoYears, a2, { from: a2 }); // Withdraw first stake - await increaseTimeAndBlocks(3600); - const fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a2 }); // For entire duration - beforeBalance = await SOV.balanceOf(a2); - await stakingRewards.collectReward(0, { from: a2 }); // For maxDuration only - afterBalance = await SOV.balanceOf(a2); - rewards = afterBalance.sub(beforeBalance); - expect(rewards).to.be.bignumber.equal(fields.amount); - totalRewards = new BN(totalRewards).add(new BN(rewards)); - expect(afterBalance).to.be.bignumber.greaterThan(beforeBalance); - }); - - it("should consider max duration", async () => { - const fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a4 }); - const fieldsTotal = await stakingRewards.getStakerCurrentReward(false, 0, { from: a4 }); - expect(fieldsTotal.amount).to.be.bignumber.greaterThan(fields.amount); - }); - - it("should continue getting rewards for the staking period even after the program stops", async () => { - await increaseTimeAndBlocks(1209600); // Second Payment - 13 days approx - await stakingRewards.stop(); - await increaseTimeAndBlocks(3600); // Increase a few blocks - const fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a2 }); - beforeBalance = await SOV.balanceOf(a2); - await stakingRewards.collectReward(0, { from: a2 }); - afterBalance = await SOV.balanceOf(a2); - rewards = afterBalance.sub(beforeBalance); - expect(rewards).to.be.bignumber.equal(fields.amount); - totalRewards = new BN(totalRewards).add(new BN(rewards)); - expect(afterBalance).to.be.bignumber.greaterThan(beforeBalance); - }); - - it("should compute and send rewards to the staker a3 as applicable", async () => { - const fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a3 }); // For entire duration - beforeBalance = await SOV.balanceOf(a3); - let tx = await stakingRewards.collectReward(0, { from: a3 }); - console.log("gasUsed: " + tx.receipt.gasUsed); - afterBalance = await SOV.balanceOf(a3); - rewards = afterBalance.sub(beforeBalance); // For maxDuration only - expect(rewards).to.be.bignumber.equal(fields.amount); - totalRewards = new BN(totalRewards).add(new BN(rewards)); - expect(afterBalance).to.be.bignumber.greaterThan(beforeBalance); - }); - - it("should NOT pay rewards for staking after the program stops", async () => { - await increaseTimeAndBlocks(1209600); // 2 Weeks - await staking.stake(wei("1000", "ether"), inTwoYears, a2, a2, { from: a2 }); - const fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a2 }); - beforeBalance = await SOV.balanceOf(a2); - await stakingRewards.collectReward(0, { from: a2 }); - afterBalance = await SOV.balanceOf(a2); - rewards = afterBalance.sub(beforeBalance); - expect(rewards).to.be.bignumber.equal(fields.amount); - totalRewards = new BN(totalRewards).add(new BN(rewards)); - expect(afterBalance).to.be.bignumber.greaterThan(beforeBalance); - }); - - it("should stop getting rewards when the staking ends after the program stops", async () => { - await increaseTimeAndBlocks(1209600); // 2 Weeks - await staking.withdraw(wei("1000", "ether"), inTwoYears, a2, { from: a2 }); - await staking.withdraw(wei("3000", "ether"), inTwoYears, a2, { from: a2 }); // Withdraw second stake - await increaseTimeAndBlocks(3600); // Increase a few blocks - beforeBalance = await SOV.balanceOf(a2); - await expectRevert(stakingRewards.collectReward(0, { from: a2 }), "no valid reward"); - afterBalance = await SOV.balanceOf(a2); - rewards = afterBalance.sub(beforeBalance); - totalRewards = new BN(totalRewards).add(new BN(rewards)); - expect(afterBalance).to.be.bignumber.equal(beforeBalance); - }); - - it("should process for max duration at a time", async () => { - await increaseTimeAndBlocks(7890000); // 3 Months - await expectRevert(stakingRewards.stop(), "Already stopped"); - const fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a1 }); // For entire duration - beforeBalance = await SOV.balanceOf(a1); - await stakingRewards.collectReward(0, { from: a1 }); // For maxDuration only - afterBalance = await SOV.balanceOf(a1); - rewards = afterBalance.sub(beforeBalance); - expect(rewards).to.be.bignumber.equal(fields.amount); - totalRewards = new BN(totalRewards).add(new BN(rewards)); - expect(afterBalance).to.be.bignumber.greaterThan(beforeBalance); - }); - - it("should be able to process again immediately when processing after the max duration", async () => { - const fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a1 }); - beforeBalance = await SOV.balanceOf(a1); - await stakingRewards.collectReward(0, { from: a1 }); - afterBalance = await SOV.balanceOf(a1); - rewards = afterBalance.sub(beforeBalance); - expect(rewards).to.be.bignumber.equal(fields.amount); - totalRewards = new BN(totalRewards).add(new BN(rewards)); - expect(afterBalance).to.be.bignumber.greaterThan(beforeBalance); - }); - - it("should revert withdraw all tokens if address is invalid", async () => { - await expectRevert(stakingRewards.withdrawTokensByOwner(constants.ZERO_ADDRESS), "revert invalid transfer"); - }); - - it("should revert withdraw all tokens if sender isn't the owner", async () => { - await expectRevert(stakingRewards.withdrawTokensByOwner(a3, { from: a3 }), "unauthorized"); - }); - - it("should withdraw all tokens", async () => { - beforeBalance = await SOV.balanceOf(a3); - await stakingRewards.withdrawTokensByOwner(a3); - afterBalance = await SOV.balanceOf(a3); - let amount = new BN(wei("1000000", "ether")).sub(totalRewards); - expect(afterBalance.sub(beforeBalance)).to.be.bignumber.equal(amount); - }); - - it("should revert while withdrawing 0 amount", async () => { - await expectRevert(stakingRewards.withdrawTokensByOwner(a3), "amount invalid"); - }); - - it("should revert if contract doesn't have enough funds to reward user", async () => { - await increaseTimeAndBlocks(1209600); // 2 Weeks - - /// @dev Optimization: Following tx takes 4s to execute on a standard laptop! - await expectRevert(stakingRewards.collectReward(0, { from: a3 }), "not enough funds to reward user"); - }); - - it("should revert if sender is a ZERO Address", async () => { - await expectRevert( - stakingRewards.collectReward(0, { from: constants.ZERO_ADDRESS }), - "unknown account 0x0000000000000000000000000000000000000000" - ); - }); - }); - - function avgWeight(from, to, maxWeight, maxDuration) { - let weight = 0; - for (let i = from; i < to; i++) { - weight += Math.floor(((maxWeight * (maxDuration ** 2 - (maxDuration - i) ** 2)) / maxDuration ** 2 + 1) * 10, 2); - } - weight /= to - from; - return (weight / 100) * 0.2975; - } - - async function increaseTimeAndBlocks(seconds) { - let latest = await blockMockUp.getBlockNum(); - let blockNum = new BN(latest).add(new BN(seconds / 30)); - await blockMockUp.setBlockNum(blockNum); - await increaseTime(seconds); - } + let root, a1, a2, a3; + let SOV, staking; + let kickoffTS, inOneYear, inTwoYears, inThreeYears; + let totalRewards; + + before(async () => { + [root, a1, a2, a3, a4, ...accounts] = accounts; + + // Deploying sovrynProtocol w/ generic function from initializer.js + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + await sovryn.setSovrynProtocolAddress(sovryn.address); + + // Custom tokens + SOV = await getSOV(sovryn, priceFeeds, SUSD, accounts); + + // BlockMockUp + blockMockUp = await BlockMockUp.new(); + + // Deployed Staking Functionality + let stakingLogic = await StakingLogic.new(SOV.address); + staking = await StakingProxy.new(SOV.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); // Test - 01/07/2021 + + // FeeSharingProxy + let feeSharingLogic = await FeeSharingLogic.new(); + feeSharingProxyObj = await FeeSharingProxy.new(sovryn.address, staking.address); + await feeSharingProxyObj.setImplementation(feeSharingLogic.address); + feeSharingProxy = await FeeSharingLogic.at(feeSharingProxyObj.address); + await staking.setFeeSharing(feeSharingProxy.address); + + //Upgradable Vesting Registry + vestingRegistryLogic = await VestingRegistryLogic.new(); + vesting = await VestingRegistryProxy.new(); + await vesting.setImplementation(vestingRegistryLogic.address); + vesting = await VestingRegistryLogic.at(vesting.address); + + await staking.setVestingRegistry(vesting.address); + + kickoffTS = await staking.kickoffTS.call(); + inOneWeek = kickoffTS.add(new BN(DELAY)); + inOneYear = kickoffTS.add(new BN(DELAY * 26)); + inTwoYears = kickoffTS.add(new BN(DELAY * 26 * 2)); + inThreeYears = kickoffTS.add(new BN(DELAY * 26 * 3)); + + // Transferred SOVs to a1 + await SOV.transfer(a1, wei("10000", "ether")); + await SOV.approve(staking.address, wei("10000", "ether"), { from: a1 }); + + // Transferred SOVs to a2 + await SOV.transfer(a2, wei("10000", "ether")); + await SOV.approve(staking.address, wei("10000", "ether"), { from: a2 }); + + // Transferred SOVs to a3 + await SOV.transfer(a3, wei("10000", "ether")); + await SOV.approve(staking.address, wei("10000", "ether"), { from: a3 }); + + // Transferred SOVs to a4 + await SOV.transfer(a4, wei("10000", "ether")); + await SOV.approve(staking.address, wei("10000", "ether"), { from: a4 }); + + await staking.stake(wei("1000", "ether"), inOneYear, a1, a1, { from: a1 }); // Test - 15/07/2021 + await staking.stake(wei("1000", "ether"), inTwoYears, a2, a2, { from: a2 }); // Test - 15/07/2021 + await staking.stake(wei("1000", "ether"), inThreeYears, a3, a3, { from: a3 }); + await staking.stake(wei("1000", "ether"), inThreeYears, a4, a4, { from: a4 }); + + let latest = await blockNumber(); + let blockNum = new BN(latest).add(new BN(1295994 / 30)); + await blockMockUp.setBlockNum(blockNum); + await increaseTime(291242); + + // Staking Reward Program is deployed + let stakingRewardsLogic = await StakingRewards.new(); + stakingRewards = await StakingRewardsProxy.new(); + await stakingRewards.setImplementation(stakingRewardsLogic.address); + stakingRewards = await StakingRewards.at(stakingRewards.address); //Test - 12/08/2021 + stakingRewards.setAverageBlockTime(30); + await stakingRewards.setBlockMockUpAddr(blockMockUp.address); + await staking.setBlockMockUpAddr(blockMockUp.address); + }); + + describe("Flow - StakingRewards", () => { + it("should revert if SOV Address is not a contract address", async () => { + await expectRevert( + stakingRewards.initialize(a3, staking.address), + "_SOV not a contract" + ); + }); + + it("should revert if SOV Address is invalid", async () => { + await expectRevert( + stakingRewards.initialize(constants.ZERO_ADDRESS, staking.address), + "Invalid SOV Address." + ); + // Staking Rewards Contract is loaded + await SOV.transfer(stakingRewards.address, wei("1000000", "ether")); + // Initialize + await stakingRewards.initialize(SOV.address, staking.address); + }); + + it("should revert if rewards are claimed before completion of two weeks from start date", async () => { + await expectRevert(stakingRewards.collectReward(0, { from: a2 }), "no valid reward"); + }); + + it("should compute and send rewards to the stakers a1, a2 and a3 correctly after 2 weeks", async () => { + await increaseTimeAndBlocks(1295994); + + let fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a1 }); + let numOfIntervals = 1; + let fullTermAvg = avgWeight(26, 27, 9, 78); + let expectedAmount = numOfIntervals * ((1000 * fullTermAvg) / 26); + expect(new BN(Math.floor(expectedAmount * 10 ** 10))).to.be.bignumber.equal( + new BN(fields.amount).div(new BN(10).pow(new BN(8))) + ); + + fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a2 }); + fullTermAvg = avgWeight(52, 53, 9, 78); + expectedAmount = numOfIntervals * ((1000 * fullTermAvg) / 26); + expect(new BN(Math.floor(expectedAmount * 10 ** 10))).to.be.bignumber.equal( + new BN(fields.amount).div(new BN(10).pow(new BN(8))) + ); + + fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a3 }); + fullTermAvg = avgWeight(78, 79, 9, 78); + expectedAmount = numOfIntervals * ((1000 * fullTermAvg) / 26); + expect(new BN(Math.floor(expectedAmount * 10 ** 10))).to.be.bignumber.equal( + new BN(fields.amount).div(new BN(10).pow(new BN(8))) + ); + }); + + it("should compute and send rewards to the stakers a1, a2 and a3 correctly after 4 weeks", async () => { + await increaseTimeAndBlocks(1295994); + + let fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a1 }); + let numOfIntervals = 2; + let fullTermAvg = avgWeight(25, 27, 9, 78); + expectedAmount = numOfIntervals * ((1000 * fullTermAvg) / 26); + expect(new BN(Math.floor(expectedAmount * 10 ** 10))).to.be.bignumber.equal( + new BN(fields.amount).div(new BN(10).pow(new BN(8))) + ); + + fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a2 }); + fullTermAvg = avgWeight(51, 53, 9, 78); + expectedAmount = numOfIntervals * ((1000 * fullTermAvg) / 26); + expect(new BN(Math.floor(expectedAmount * 10 ** 10))).to.be.bignumber.equal( + new BN(fields.amount).div(new BN(10).pow(new BN(8))) + ); + + fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a3 }); + fullTermAvg = avgWeight(77, 79, 9, 78); + expectedAmount = numOfIntervals * ((1000 * fullTermAvg) / 26); + expect(new BN(Math.floor(expectedAmount * 10 ** 10))).to.be.bignumber.equal( + new BN(fields.amount).div(new BN(10).pow(new BN(8))) + ); + }); + + it("should compute send rewards to the staker including added staking", async () => { + await increaseTimeAndBlocks(86400); + await staking.stake(wei("3000", "ether"), inTwoYears, a2, a2, { from: a2 }); + + await increaseTimeAndBlocks(1209600); + const fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a2 }); + beforeBalance = await SOV.balanceOf(a2); + await stakingRewards.collectReward(0, { from: a2 }); + afterBalance = await SOV.balanceOf(a2); + rewards = afterBalance.sub(beforeBalance); + expect(rewards).to.be.bignumber.equal(fields.amount); + totalRewards = new BN(totalRewards).add(new BN(rewards)); + expect(afterBalance).to.be.bignumber.greaterThan(beforeBalance); + }); + + it("should revert if the user tries to claim rewards early", async () => { + await increaseTimeAndBlocks(86400); // One day + await expectRevert(stakingRewards.collectReward(0, { from: a2 }), "no valid reward"); + }); + + it("should compute and send rewards to the staker after recalculating withdrawn stake", async () => { + await increaseTimeAndBlocks(32659200); // More than a year - first stake expires + feeSharingProxy = await FeeSharingProxy.new(sovryn.address, staking.address); + await staking.withdraw(wei("1000", "ether"), inTwoYears, a2, { from: a2 }); // Withdraw first stake + await increaseTimeAndBlocks(3600); + const fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a2 }); // For entire duration + beforeBalance = await SOV.balanceOf(a2); + await stakingRewards.collectReward(0, { from: a2 }); // For maxDuration only + afterBalance = await SOV.balanceOf(a2); + rewards = afterBalance.sub(beforeBalance); + expect(rewards).to.be.bignumber.equal(fields.amount); + totalRewards = new BN(totalRewards).add(new BN(rewards)); + expect(afterBalance).to.be.bignumber.greaterThan(beforeBalance); + }); + + it("should consider max duration", async () => { + const fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a4 }); + const fieldsTotal = await stakingRewards.getStakerCurrentReward(false, 0, { + from: a4, + }); + expect(fieldsTotal.amount).to.be.bignumber.greaterThan(fields.amount); + }); + + it("should continue getting rewards for the staking period even after the program stops", async () => { + await increaseTimeAndBlocks(1209600); // Second Payment - 13 days approx + await stakingRewards.stop(); + await increaseTimeAndBlocks(3600); // Increase a few blocks + const fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a2 }); + beforeBalance = await SOV.balanceOf(a2); + await stakingRewards.collectReward(0, { from: a2 }); + afterBalance = await SOV.balanceOf(a2); + rewards = afterBalance.sub(beforeBalance); + expect(rewards).to.be.bignumber.equal(fields.amount); + totalRewards = new BN(totalRewards).add(new BN(rewards)); + expect(afterBalance).to.be.bignumber.greaterThan(beforeBalance); + }); + + it("should compute and send rewards to the staker a3 as applicable", async () => { + const fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a3 }); // For entire duration + beforeBalance = await SOV.balanceOf(a3); + let tx = await stakingRewards.collectReward(0, { from: a3 }); + console.log("gasUsed: " + tx.receipt.gasUsed); + afterBalance = await SOV.balanceOf(a3); + rewards = afterBalance.sub(beforeBalance); // For maxDuration only + expect(rewards).to.be.bignumber.equal(fields.amount); + totalRewards = new BN(totalRewards).add(new BN(rewards)); + expect(afterBalance).to.be.bignumber.greaterThan(beforeBalance); + }); + + it("should NOT pay rewards for staking after the program stops", async () => { + await increaseTimeAndBlocks(1209600); // 2 Weeks + await staking.stake(wei("1000", "ether"), inTwoYears, a2, a2, { from: a2 }); + const fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a2 }); + beforeBalance = await SOV.balanceOf(a2); + await stakingRewards.collectReward(0, { from: a2 }); + afterBalance = await SOV.balanceOf(a2); + rewards = afterBalance.sub(beforeBalance); + expect(rewards).to.be.bignumber.equal(fields.amount); + totalRewards = new BN(totalRewards).add(new BN(rewards)); + expect(afterBalance).to.be.bignumber.greaterThan(beforeBalance); + }); + + it("should stop getting rewards when the staking ends after the program stops", async () => { + await increaseTimeAndBlocks(1209600); // 2 Weeks + await staking.withdraw(wei("1000", "ether"), inTwoYears, a2, { from: a2 }); + await staking.withdraw(wei("3000", "ether"), inTwoYears, a2, { from: a2 }); // Withdraw second stake + await increaseTimeAndBlocks(3600); // Increase a few blocks + beforeBalance = await SOV.balanceOf(a2); + await expectRevert(stakingRewards.collectReward(0, { from: a2 }), "no valid reward"); + afterBalance = await SOV.balanceOf(a2); + rewards = afterBalance.sub(beforeBalance); + totalRewards = new BN(totalRewards).add(new BN(rewards)); + expect(afterBalance).to.be.bignumber.equal(beforeBalance); + }); + + it("should process for max duration at a time", async () => { + await increaseTimeAndBlocks(7890000); // 3 Months + await expectRevert(stakingRewards.stop(), "Already stopped"); + const fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a1 }); // For entire duration + beforeBalance = await SOV.balanceOf(a1); + await stakingRewards.collectReward(0, { from: a1 }); // For maxDuration only + afterBalance = await SOV.balanceOf(a1); + rewards = afterBalance.sub(beforeBalance); + expect(rewards).to.be.bignumber.equal(fields.amount); + totalRewards = new BN(totalRewards).add(new BN(rewards)); + expect(afterBalance).to.be.bignumber.greaterThan(beforeBalance); + }); + + it("should be able to process again immediately when processing after the max duration", async () => { + const fields = await stakingRewards.getStakerCurrentReward(true, 0, { from: a1 }); + beforeBalance = await SOV.balanceOf(a1); + await stakingRewards.collectReward(0, { from: a1 }); + afterBalance = await SOV.balanceOf(a1); + rewards = afterBalance.sub(beforeBalance); + expect(rewards).to.be.bignumber.equal(fields.amount); + totalRewards = new BN(totalRewards).add(new BN(rewards)); + expect(afterBalance).to.be.bignumber.greaterThan(beforeBalance); + }); + + it("should revert withdraw all tokens if address is invalid", async () => { + await expectRevert( + stakingRewards.withdrawTokensByOwner(constants.ZERO_ADDRESS), + "revert invalid transfer" + ); + }); + + it("should revert withdraw all tokens if sender isn't the owner", async () => { + await expectRevert( + stakingRewards.withdrawTokensByOwner(a3, { from: a3 }), + "unauthorized" + ); + }); + + it("should withdraw all tokens", async () => { + beforeBalance = await SOV.balanceOf(a3); + await stakingRewards.withdrawTokensByOwner(a3); + afterBalance = await SOV.balanceOf(a3); + let amount = new BN(wei("1000000", "ether")).sub(totalRewards); + expect(afterBalance.sub(beforeBalance)).to.be.bignumber.equal(amount); + }); + + it("should revert while withdrawing 0 amount", async () => { + await expectRevert(stakingRewards.withdrawTokensByOwner(a3), "amount invalid"); + }); + + it("should revert if contract doesn't have enough funds to reward user", async () => { + await increaseTimeAndBlocks(1209600); // 2 Weeks + + /// @dev Optimization: Following tx takes 4s to execute on a standard laptop! + await expectRevert( + stakingRewards.collectReward(0, { from: a3 }), + "not enough funds to reward user" + ); + }); + + it("should revert if sender is a ZERO Address", async () => { + await expectRevert( + stakingRewards.collectReward(0, { from: constants.ZERO_ADDRESS }), + "unknown account 0x0000000000000000000000000000000000000000" + ); + }); + }); + + function avgWeight(from, to, maxWeight, maxDuration) { + let weight = 0; + for (let i = from; i < to; i++) { + weight += Math.floor( + ((maxWeight * (maxDuration ** 2 - (maxDuration - i) ** 2)) / maxDuration ** 2 + + 1) * + 10, + 2 + ); + } + weight /= to - from; + return (weight / 100) * 0.2975; + } + + async function increaseTimeAndBlocks(seconds) { + let latest = await blockMockUp.getBlockNum(); + let blockNum = new BN(latest).add(new BN(seconds / 30)); + await blockMockUp.setBlockNum(blockNum); + await increaseTime(seconds); + } }); diff --git a/tests/swaps/SwapsExternal.js b/tests/swaps/SwapsExternal.js index 9ff4d8d91..e6ffceac5 100644 --- a/tests/swaps/SwapsExternal.js +++ b/tests/swaps/SwapsExternal.js @@ -40,20 +40,20 @@ const VestingLogic = artifacts.require("VestingLogic"); const VestingFactory = artifacts.require("VestingFactory"); const VestingRegistry = artifacts.require("VestingRegistry3"); const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getLoanTokenLogic, - getLoanToken, - getLoanTokenLogicWrbtc, - getLoanTokenWRBTC, - loan_pool_setup, - set_demand_curve, - getPriceFeeds, - getSovryn, - decodeLogs, - getSOV, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getLoanTokenLogic, + getLoanToken, + getLoanTokenLogicWrbtc, + getLoanTokenWRBTC, + loan_pool_setup, + set_demand_curve, + getPriceFeeds, + getSovryn, + decodeLogs, + getSOV, } = require("../Utils/initializer.js"); const { etherGasCost } = require("../Utils/Ethereum.js"); @@ -65,375 +65,511 @@ let cliff = 1; // This is in 4 weeks. i.e. 1 * 4 weeks. let duration = 11; // This is in 4 weeks. i.e. 11 * 4 weeks. contract("SwapsExternal", (accounts) => { - const name = "Test token"; - const symbol = "TST"; - - let lender; - let SUSD, WRBTC; - let sovryn, loanToken; - - async function deploymentAndInitFixture(_wallets, _provider) { - // Deploying sovrynProtocol w/ generic function from initializer.js - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - await sovryn.setSovrynProtocolAddress(sovryn.address); - - SOVToken = await getSOV(sovryn, priceFeeds, SUSD, accounts); - - // Overwritting priceFeeds - priceFeeds = await PriceFeedsLocal.new(WRBTC.address, sovryn.address); - await priceFeeds.setRates(SUSD.address, WRBTC.address, wei("1", "ether")); - const sovrynSwapSimulator = await TestSovrynSwap.new(priceFeeds.address); - await sovryn.setSovrynSwapContractRegistryAddress(sovrynSwapSimulator.address); - await sovryn.setSupportedTokens([SUSD.address, WRBTC.address], [true, true]); - - await sovryn.setFeesController(lender); - await sovryn.setSwapExternalFeePercent(wei("3", "ether")); - - const initLoanTokenLogic = await getLoanTokenLogic(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] - loanTokenLogic = initLoanTokenLogic[0]; - loanTokenLogicBeacon = initLoanTokenLogic[1]; - - loanToken = await LoanToken.new(lender, loanTokenLogic.address, sovryn.address, WRBTC.address); - await loanToken.initialize(SUSD.address, name, symbol); // iToken - - /** Initialize the loan token logic proxy */ - loanToken = await ILoanTokenLogicProxy.at(loanToken.address); - await loanToken.setBeaconAddress(loanTokenLogicBeacon.address); - - /** Use interface of LoanTokenModules */ - loanToken = await ILoanTokenModules.at(loanToken.address); - - // Staking - let stakingLogic = await StakingLogic.new(SUSD.address); - staking = await StakingProxy.new(SUSD.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingLogic.at(staking.address); - - // FeeSharingProxy - feeSharingLogic = await FeeSharingLogic.new(); - feeSharingProxyObj = await FeeSharingProxy.new(sovryn.address, staking.address); - await feeSharingProxyObj.setImplementation(feeSharingLogic.address); - feeSharingProxy = await FeeSharingLogic.at(feeSharingProxyObj.address); - await sovryn.setFeesController(feeSharingProxy.address); - - // Set loan pool for wRBTC -- because our fee sharing proxy required the loanPool of wRBTC - loanTokenLogicWrbtc = await LoanTokenLogicWrbtc.new(); - loanTokenWrbtc = await LoanToken.new(accounts[0], loanTokenLogicWrbtc.address, sovryn.address, WRBTC.address); - await loanTokenWrbtc.initialize(WRBTC.address, "iWRBTC", "iWRBTC"); - - loanTokenWrbtc = await LoanTokenLogicWrbtc.at(loanTokenWrbtc.address); - const loanTokenAddressWrbtc = await loanTokenWrbtc.loanTokenAddress(); - await sovryn.setLoanPool([loanTokenWrbtc.address], [loanTokenAddressWrbtc]); - - await WRBTC.mint(sovryn.address, wei("500", "ether")); - - // Creating the Vesting Instance. - vestingLogic = await VestingLogic.new(); - vestingFactory = await VestingFactory.new(vestingLogic.address); - vestingRegistry = await VestingRegistry.new( - vestingFactory.address, - SOVToken.address, - staking.address, - feeSharingProxy.address, - lender // This should be Governance Timelock Contract. - ); - vestingFactory.transferOwnership(vestingRegistry.address); - - await sovryn.setLockedSOVAddress( - ( - await LockedSOV.new(SOVToken.address, vestingRegistry.address, cliff, duration, [lender]) - ).address - ); - - params = [ - "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object - false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans - lender, // address owner; // owner of this object - SUSD.address, // address loanToken; // the token being loaned - WRBTC.address, // address collateralToken; // the required collateral token - wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin - wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value - 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) - ]; - - await loanToken.setupLoanParams([params], false); - - const loanTokenAddress = await loanToken.loanTokenAddress(); - if (lender == (await sovryn.owner())) await sovryn.setLoanPool([loanToken.address], [loanTokenAddress]); - - await WRBTC.mint(sovryn.address, wei("500", "ether")); - } - - before(async () => { - [lender, staker] = accounts; - }); - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - describe("SwapsExternal - Swap External", () => { - it("Doesn't allow fallback function calls", async () => { - const swapsExternal = await SwapsExternal.new(); - await expectRevert( - swapsExternal.send(wei("0.0000000000000001", "ether")), - "fallback function is not payable and was called with value 100" - ); - await expectRevert(swapsExternal.sendTransaction({}), "fallback not allowed"); - }); - - it("Doesn't allow swaps if source token amount = 0", async () => { - await expectRevert( - sovryn.swapExternal(SUSD.address, WRBTC.address, accounts[0], accounts[0], 0, 0, 0, "0x"), - "sourceTokenAmount == 0" - ); - }); - - it("Doesn't allow swaps without enough allowance", async () => { - await expectRevert( - sovryn.swapExternal(SUSD.address, WRBTC.address, accounts[0], accounts[0], hunEth, 0, 0, "0x"), - "SafeERC20: low-level call failed" - ); - }); - - it("Doesn't allow swaps if token address contract unavailable", async () => { - await expectRevert( - sovryn.swapExternal(ZERO_ADDRESS, WRBTC.address, accounts[0], accounts[0], 100, 0, 0, "0x"), - "call to non-contract" - ); - }); - - it("Doesn't allow swaps if source token address is missing", async () => { - const assetBalance = await loanToken.assetBalanceOf(lender); - await SUSD.approve(sovryn.address, assetBalance.add(new BN(wei("10", "ether"))).toString()); - await expectRevert( - sovryn.swapExternal(ZERO_ADDRESS, WRBTC.address, accounts[0], accounts[0], wei("1", "ether"), 0, 0, "0x", { - value: wei("1", "ether"), - }), - "swap failed" - ); - }); - - it("Doesn't allow swaps if destination token is zero address", async () => { - const assetBalance = await loanToken.assetBalanceOf(lender); - await SUSD.approve(sovryn.address, assetBalance.add(new BN(wei("10", "ether"))).toString()); - await expectRevert(sovryn.swapExternal(SUSD.address, ZERO_ADDRESS, accounts[0], accounts[0], 100, 0, 0, "0x"), "swap failed"); - }); - - it("Doesn't allow source token mismatch", async () => { - const assetBalance = await loanToken.assetBalanceOf(lender); - await SUSD.approve(sovryn.address, assetBalance.add(new BN(wei("10", "ether"))).toString()); - await expectRevert( - sovryn.swapExternal(SUSD.address, WRBTC.address, accounts[0], accounts[0], wei("1", "ether"), 0, 0, "0x", { - value: wei("1", "ether"), - }), - "sourceToken mismatch" - ); - }); - - it("Doesn't allow source token amount mismatch", async () => { - const assetBalance = await loanToken.assetBalanceOf(lender); - await WRBTC.approve(sovryn.address, assetBalance.add(new BN(wei("10", "ether"))).toString()); - await expectRevert( - sovryn.swapExternal(WRBTC.address, SUSD.address, accounts[0], accounts[0], wei("1", "ether"), 0, 0, "0x", { - value: 100, - }), - "sourceTokenAmount mismatch" - ); - }); - - it("Check swapExternal with minReturn > 0 should revert if minReturn is not valid (higher)", async () => { - const assetBalance = await loanToken.assetBalanceOf(lender); - await SUSD.approve(sovryn.address, assetBalance.add(new BN(wei("10", "ether"))).toString()); - await expectRevert( - sovryn.swapExternal(SUSD.address, WRBTC.address, accounts[0], accounts[0], wei("1", "ether"), 0, wei("10", "ether"), "0x"), - "destTokenAmountReceived too low" - ); - }); - - it("Check swapExternal with minReturn > 0", async () => { - const assetBalance = await loanToken.assetBalanceOf(lender); - await SUSD.approve(sovryn.address, assetBalance.add(new BN(wei("10", "ether"))).toString()); - // feeds price is set 0.01, so test minReturn with 0.01 as well for the 1 ether swap - const tx = await sovryn.swapExternal( - SUSD.address, - WRBTC.address, - accounts[0], - accounts[0], - wei("1", "ether"), - 0, - wei("0.01", "ether"), - "0x" - ); - const fields = await sovryn.swapExternal.call( - SUSD.address, - WRBTC.address, - accounts[0], - accounts[0], - wei("1", "ether"), - 0, - wei("0.01", "ether"), - "0x" - ); - expectEvent(tx, "ExternalSwap", { - user: lender, - sourceToken: SUSD.address, - destToken: WRBTC.address, - sourceAmount: wei("1", "ether"), - destAmount: fields.destTokenAmountReceived.toString(), - }); - - expectEvent(tx, "PayTradingFee", { - amount: new BN(wei("0.3", "ether")) - .mul(new BN(wei("10", "ether"))) - .div(new BN(wei("100", "ether"))) - .toString(), - }); - - let destTokenAmount = await sovryn.getSwapExpectedReturn(SUSD.address, WRBTC.address, wei("1", "ether")); - const trading_fee_percent = await sovryn.getSwapExternalFeePercent(); - const trading_fee = destTokenAmount.mul(trading_fee_percent).div(hunEth); - let desTokenAmountAfterFee = destTokenAmount - trading_fee; - assert.equal(desTokenAmountAfterFee, fields.destTokenAmountReceived.toString()); - }); - - it("Should be able to withdraw fees", async () => { - const maxDisagreement = new BN(wei("5", "ether")); - await sovryn.setMaxDisagreement(maxDisagreement); - const assetBalance = await loanToken.assetBalanceOf(lender); - await SUSD.approve(sovryn.address, assetBalance.add(new BN(wei("10", "ether"))).toString()); - // feeds price is set 0.01, so test minReturn with 0.01 as well for the 1 ether swap - await sovryn.swapExternal( - SUSD.address, - WRBTC.address, - accounts[0], - accounts[0], - wei("1", "ether"), - 0, - wei("0.01", "ether"), - "0x" - ); - - const fields = await sovryn.swapExternal.call( - SUSD.address, - WRBTC.address, - accounts[0], - accounts[0], - wei("1", "ether"), - 0, - wei("0.01", "ether"), - "0x" - ); - - let destTokenAmount = await sovryn.getSwapExpectedReturn(SUSD.address, WRBTC.address, wei("1", "ether")); - const trading_fee_percent = await sovryn.getSwapExternalFeePercent(); - const trading_fee = destTokenAmount.mul(trading_fee_percent).div(hunEth); - await SUSD.transfer(sovryn.address, wei("1", "ether")); - - // stake - getPriorTotalVotingPower - let amount = trading_fee; - // await SUSD.transfer(lender, amount); - await SUSD.approve(staking.address, amount, { from: lender }); - let kickoffTS = await staking.kickoffTS.call(); - await staking.stake(amount, kickoffTS.add(new BN(TWO_WEEKS)), lender, lender, { from: lender }); - - const tx = await feeSharingProxy.withdrawFees([SUSD.address]); - - let swapFee = amount.mul(trading_fee_percent).div(new BN(wei("100", "ether"))); - - // need to sub by swap fee because at this point, protocol will received the trading fee again. - loanTokenWRBTCBalanceShouldBe = amount.mul(new BN(1)).sub(swapFee); - - expectEvent(tx, "FeeWithdrawn", { - sender: lender, - token: loanTokenWrbtc.address, - amount: loanTokenWRBTCBalanceShouldBe, - }); - }); - - it("Check swapExternal with minReturn > 0 should revert if minReturn is invalid", async () => { - await expectRevert( - sovryn.checkPriceDivergence(SUSD.address, WRBTC.address, wei("1", "ether"), wei("2", "ether")), - "destTokenAmountReceived too low" - ); - }); - - it("Swap external using RBTC", async () => { - const swapper = accounts[2]; - const underlyingBalancePrev = await SUSD.balanceOf(swapper); - const rbtcBalancePrev = new BN(await web3.eth.getBalance(swapper)); - const assetBalance = await loanToken.assetBalanceOf(swapper); - const rbtcValueBeingSent = 1e14; - await SUSD.approve(sovryn.address, assetBalance.add(new BN(wei("10", "ether"))).toString()); - - const tx = await sovryn.swapExternal( - WRBTC.address, // source token must be wrbtc - SUSD.address, // dest token - swapper, // receiver - swapper, // return to sender address - rbtcValueBeingSent, // sourceTokenAmount - 0, // requiredDestTokenAmount - 0, // minReturn (slippage) - "0x", - { value: rbtcValueBeingSent, from: swapper } - ); - - const underlyingBalanceAfter = await SUSD.balanceOf(swapper); - const rbtcBalanceAfter = new BN(await web3.eth.getBalance(swapper)); - - let event_name = "ExternalSwap"; - let decode = decodeLogs(tx.receipt.rawLogs, SwapsExternal, event_name); - if (!decode.length) { - throw "Event ExternalSwap is not fired properly"; - } - - const user = decode[0].args["user"]; - const sourceToken = decode[0].args["sourceToken"]; - const destToken = decode[0].args["destToken"]; - const sourceAmount = decode[0].args["sourceAmount"]; - const destAmount = decode[0].args["destAmount"]; - const txFee = new BN((await etherGasCost(tx.receipt)).toString()); - - const finalUnderlyingBalance = underlyingBalanceAfter.sub(underlyingBalancePrev); - const finalRbtcBalance = rbtcBalancePrev.sub(rbtcBalanceAfter); - - expect(user).to.be.equal(swapper); - expect(sourceToken).to.be.equal(WRBTC.address); - expect(destToken).to.be.equal(SUSD.address); - expect(destAmount.toString()).to.be.equal(finalUnderlyingBalance.toString()); - expect(sourceAmount.toString()).to.be.equal(finalRbtcBalance.sub(txFee).toString()); - expect(sourceAmount.toString()).to.be.equal(rbtcValueBeingSent.toString()); - }); - - it("Swap external using RBTC should failed if source token amount is not matched with rbtc being sent", async () => { - const assetBalance = await loanToken.assetBalanceOf(lender); - const rbtcValueBeingSent = 1e14; - await SUSD.approve(sovryn.address, assetBalance.add(new BN(wei("10", "ether"))).toString()); - - await expectRevert( - sovryn.swapExternal( - constants.ZERO_ADDRESS, // source token must be wrbtc - SUSD.address, // dest token - lender, // receiver - lender, // return to sender address - rbtcValueBeingSent, // sourceTokenAmount - 0, // requiredDestTokenAmount - 0, // minReturn (slippage) - "0x", - { value: 2e14 } - ), - "sourceTokenAmount mismatch" - ); - }); - - // Should fail to change swap external fee percent by invalid value (more than 100%) - it("Test set swapExternalFeePercent with invalid value", async () => { - await expectRevert(sovryn.setSwapExternalFeePercent(wei("101", "ether")), "value too high"); - }); - }); + const name = "Test token"; + const symbol = "TST"; + + let lender; + let SUSD, WRBTC; + let sovryn, loanToken; + + async function deploymentAndInitFixture(_wallets, _provider) { + // Deploying sovrynProtocol w/ generic function from initializer.js + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + await sovryn.setSovrynProtocolAddress(sovryn.address); + + SOVToken = await getSOV(sovryn, priceFeeds, SUSD, accounts); + + // Overwritting priceFeeds + priceFeeds = await PriceFeedsLocal.new(WRBTC.address, sovryn.address); + await priceFeeds.setRates(SUSD.address, WRBTC.address, wei("1", "ether")); + const sovrynSwapSimulator = await TestSovrynSwap.new(priceFeeds.address); + await sovryn.setSovrynSwapContractRegistryAddress(sovrynSwapSimulator.address); + await sovryn.setSupportedTokens([SUSD.address, WRBTC.address], [true, true]); + + await sovryn.setFeesController(lender); + await sovryn.setSwapExternalFeePercent(wei("3", "ether")); + + const initLoanTokenLogic = await getLoanTokenLogic(); // function will return [LoanTokenLogicProxy, LoanTokenLogicBeacon] + loanTokenLogic = initLoanTokenLogic[0]; + loanTokenLogicBeacon = initLoanTokenLogic[1]; + + loanToken = await LoanToken.new( + lender, + loanTokenLogic.address, + sovryn.address, + WRBTC.address + ); + await loanToken.initialize(SUSD.address, name, symbol); // iToken + + /** Initialize the loan token logic proxy */ + loanToken = await ILoanTokenLogicProxy.at(loanToken.address); + await loanToken.setBeaconAddress(loanTokenLogicBeacon.address); + + /** Use interface of LoanTokenModules */ + loanToken = await ILoanTokenModules.at(loanToken.address); + + // Staking + let stakingLogic = await StakingLogic.new(SUSD.address); + staking = await StakingProxy.new(SUSD.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); + + // FeeSharingProxy + feeSharingLogic = await FeeSharingLogic.new(); + feeSharingProxyObj = await FeeSharingProxy.new(sovryn.address, staking.address); + await feeSharingProxyObj.setImplementation(feeSharingLogic.address); + feeSharingProxy = await FeeSharingLogic.at(feeSharingProxyObj.address); + await sovryn.setFeesController(feeSharingProxy.address); + + // Set loan pool for wRBTC -- because our fee sharing proxy required the loanPool of wRBTC + loanTokenLogicWrbtc = await LoanTokenLogicWrbtc.new(); + loanTokenWrbtc = await LoanToken.new( + accounts[0], + loanTokenLogicWrbtc.address, + sovryn.address, + WRBTC.address + ); + await loanTokenWrbtc.initialize(WRBTC.address, "iWRBTC", "iWRBTC"); + + loanTokenWrbtc = await LoanTokenLogicWrbtc.at(loanTokenWrbtc.address); + const loanTokenAddressWrbtc = await loanTokenWrbtc.loanTokenAddress(); + await sovryn.setLoanPool([loanTokenWrbtc.address], [loanTokenAddressWrbtc]); + + await WRBTC.mint(sovryn.address, wei("500", "ether")); + + // Creating the Vesting Instance. + vestingLogic = await VestingLogic.new(); + vestingFactory = await VestingFactory.new(vestingLogic.address); + vestingRegistry = await VestingRegistry.new( + vestingFactory.address, + SOVToken.address, + staking.address, + feeSharingProxy.address, + lender // This should be Governance Timelock Contract. + ); + vestingFactory.transferOwnership(vestingRegistry.address); + + await sovryn.setLockedSOVAddress( + ( + await LockedSOV.new(SOVToken.address, vestingRegistry.address, cliff, duration, [ + lender, + ]) + ).address + ); + + params = [ + "0x0000000000000000000000000000000000000000000000000000000000000000", // bytes32 id; // id of loan params object + false, // bool active; // if false, this object has been disabled by the owner and can't be used for future loans + lender, // address owner; // owner of this object + SUSD.address, // address loanToken; // the token being loaned + WRBTC.address, // address collateralToken; // the required collateral token + wei("20", "ether"), // uint256 minInitialMargin; // the minimum allowed initial margin + wei("15", "ether"), // uint256 maintenanceMargin; // an unhealthy loan when current margin is at or below this value + 2419200, // uint256 maxLoanTerm; // the maximum term for new loans (0 means there's no max term) + ]; + + await loanToken.setupLoanParams([params], false); + + const loanTokenAddress = await loanToken.loanTokenAddress(); + if (lender == (await sovryn.owner())) + await sovryn.setLoanPool([loanToken.address], [loanTokenAddress]); + + await WRBTC.mint(sovryn.address, wei("500", "ether")); + } + + before(async () => { + [lender, staker] = accounts; + }); + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + describe("SwapsExternal - Swap External", () => { + it("Doesn't allow fallback function calls", async () => { + const swapsExternal = await SwapsExternal.new(); + await expectRevert( + swapsExternal.send(wei("0.0000000000000001", "ether")), + "fallback function is not payable and was called with value 100" + ); + await expectRevert(swapsExternal.sendTransaction({}), "fallback not allowed"); + }); + + it("Doesn't allow swaps if source token amount = 0", async () => { + await expectRevert( + sovryn.swapExternal( + SUSD.address, + WRBTC.address, + accounts[0], + accounts[0], + 0, + 0, + 0, + "0x" + ), + "sourceTokenAmount == 0" + ); + }); + + it("Doesn't allow swaps without enough allowance", async () => { + await expectRevert( + sovryn.swapExternal( + SUSD.address, + WRBTC.address, + accounts[0], + accounts[0], + hunEth, + 0, + 0, + "0x" + ), + "SafeERC20: low-level call failed" + ); + }); + + it("Doesn't allow swaps if token address contract unavailable", async () => { + await expectRevert( + sovryn.swapExternal( + ZERO_ADDRESS, + WRBTC.address, + accounts[0], + accounts[0], + 100, + 0, + 0, + "0x" + ), + "call to non-contract" + ); + }); + + it("Doesn't allow swaps if source token address is missing", async () => { + const assetBalance = await loanToken.assetBalanceOf(lender); + await SUSD.approve( + sovryn.address, + assetBalance.add(new BN(wei("10", "ether"))).toString() + ); + await expectRevert( + sovryn.swapExternal( + ZERO_ADDRESS, + WRBTC.address, + accounts[0], + accounts[0], + wei("1", "ether"), + 0, + 0, + "0x", + { + value: wei("1", "ether"), + } + ), + "swap failed" + ); + }); + + it("Doesn't allow swaps if destination token is zero address", async () => { + const assetBalance = await loanToken.assetBalanceOf(lender); + await SUSD.approve( + sovryn.address, + assetBalance.add(new BN(wei("10", "ether"))).toString() + ); + await expectRevert( + sovryn.swapExternal( + SUSD.address, + ZERO_ADDRESS, + accounts[0], + accounts[0], + 100, + 0, + 0, + "0x" + ), + "swap failed" + ); + }); + + it("Doesn't allow source token mismatch", async () => { + const assetBalance = await loanToken.assetBalanceOf(lender); + await SUSD.approve( + sovryn.address, + assetBalance.add(new BN(wei("10", "ether"))).toString() + ); + await expectRevert( + sovryn.swapExternal( + SUSD.address, + WRBTC.address, + accounts[0], + accounts[0], + wei("1", "ether"), + 0, + 0, + "0x", + { + value: wei("1", "ether"), + } + ), + "sourceToken mismatch" + ); + }); + + it("Doesn't allow source token amount mismatch", async () => { + const assetBalance = await loanToken.assetBalanceOf(lender); + await WRBTC.approve( + sovryn.address, + assetBalance.add(new BN(wei("10", "ether"))).toString() + ); + await expectRevert( + sovryn.swapExternal( + WRBTC.address, + SUSD.address, + accounts[0], + accounts[0], + wei("1", "ether"), + 0, + 0, + "0x", + { + value: 100, + } + ), + "sourceTokenAmount mismatch" + ); + }); + + it("Check swapExternal with minReturn > 0 should revert if minReturn is not valid (higher)", async () => { + const assetBalance = await loanToken.assetBalanceOf(lender); + await SUSD.approve( + sovryn.address, + assetBalance.add(new BN(wei("10", "ether"))).toString() + ); + await expectRevert( + sovryn.swapExternal( + SUSD.address, + WRBTC.address, + accounts[0], + accounts[0], + wei("1", "ether"), + 0, + wei("10", "ether"), + "0x" + ), + "destTokenAmountReceived too low" + ); + }); + + it("Check swapExternal with minReturn > 0", async () => { + const assetBalance = await loanToken.assetBalanceOf(lender); + await SUSD.approve( + sovryn.address, + assetBalance.add(new BN(wei("10", "ether"))).toString() + ); + // feeds price is set 0.01, so test minReturn with 0.01 as well for the 1 ether swap + const tx = await sovryn.swapExternal( + SUSD.address, + WRBTC.address, + accounts[0], + accounts[0], + wei("1", "ether"), + 0, + wei("0.01", "ether"), + "0x" + ); + const fields = await sovryn.swapExternal.call( + SUSD.address, + WRBTC.address, + accounts[0], + accounts[0], + wei("1", "ether"), + 0, + wei("0.01", "ether"), + "0x" + ); + expectEvent(tx, "ExternalSwap", { + user: lender, + sourceToken: SUSD.address, + destToken: WRBTC.address, + sourceAmount: wei("1", "ether"), + destAmount: fields.destTokenAmountReceived.toString(), + }); + + expectEvent(tx, "PayTradingFee", { + amount: new BN(wei("0.3", "ether")) + .mul(new BN(wei("10", "ether"))) + .div(new BN(wei("100", "ether"))) + .toString(), + }); + + let destTokenAmount = await sovryn.getSwapExpectedReturn( + SUSD.address, + WRBTC.address, + wei("1", "ether") + ); + const trading_fee_percent = await sovryn.getSwapExternalFeePercent(); + const trading_fee = destTokenAmount.mul(trading_fee_percent).div(hunEth); + let desTokenAmountAfterFee = destTokenAmount - trading_fee; + assert.equal(desTokenAmountAfterFee, fields.destTokenAmountReceived.toString()); + }); + + it("Should be able to withdraw fees", async () => { + const maxDisagreement = new BN(wei("5", "ether")); + await sovryn.setMaxDisagreement(maxDisagreement); + const assetBalance = await loanToken.assetBalanceOf(lender); + await SUSD.approve( + sovryn.address, + assetBalance.add(new BN(wei("10", "ether"))).toString() + ); + // feeds price is set 0.01, so test minReturn with 0.01 as well for the 1 ether swap + await sovryn.swapExternal( + SUSD.address, + WRBTC.address, + accounts[0], + accounts[0], + wei("1", "ether"), + 0, + wei("0.01", "ether"), + "0x" + ); + + const fields = await sovryn.swapExternal.call( + SUSD.address, + WRBTC.address, + accounts[0], + accounts[0], + wei("1", "ether"), + 0, + wei("0.01", "ether"), + "0x" + ); + + let destTokenAmount = await sovryn.getSwapExpectedReturn( + SUSD.address, + WRBTC.address, + wei("1", "ether") + ); + const trading_fee_percent = await sovryn.getSwapExternalFeePercent(); + const trading_fee = destTokenAmount.mul(trading_fee_percent).div(hunEth); + await SUSD.transfer(sovryn.address, wei("1", "ether")); + + // stake - getPriorTotalVotingPower + let amount = trading_fee; + // await SUSD.transfer(lender, amount); + await SUSD.approve(staking.address, amount, { from: lender }); + let kickoffTS = await staking.kickoffTS.call(); + await staking.stake(amount, kickoffTS.add(new BN(TWO_WEEKS)), lender, lender, { + from: lender, + }); + + const tx = await feeSharingProxy.withdrawFees([SUSD.address]); + + let swapFee = amount.mul(trading_fee_percent).div(new BN(wei("100", "ether"))); + + // need to sub by swap fee because at this point, protocol will received the trading fee again. + loanTokenWRBTCBalanceShouldBe = amount.mul(new BN(1)).sub(swapFee); + + expectEvent(tx, "FeeWithdrawn", { + sender: lender, + token: loanTokenWrbtc.address, + amount: loanTokenWRBTCBalanceShouldBe, + }); + }); + + it("Check swapExternal with minReturn > 0 should revert if minReturn is invalid", async () => { + await expectRevert( + sovryn.checkPriceDivergence( + SUSD.address, + WRBTC.address, + wei("1", "ether"), + wei("2", "ether") + ), + "destTokenAmountReceived too low" + ); + }); + + it("Swap external using RBTC", async () => { + const swapper = accounts[2]; + const underlyingBalancePrev = await SUSD.balanceOf(swapper); + const rbtcBalancePrev = new BN(await web3.eth.getBalance(swapper)); + const assetBalance = await loanToken.assetBalanceOf(swapper); + const rbtcValueBeingSent = 1e14; + await SUSD.approve( + sovryn.address, + assetBalance.add(new BN(wei("10", "ether"))).toString() + ); + + const tx = await sovryn.swapExternal( + WRBTC.address, // source token must be wrbtc + SUSD.address, // dest token + swapper, // receiver + swapper, // return to sender address + rbtcValueBeingSent, // sourceTokenAmount + 0, // requiredDestTokenAmount + 0, // minReturn (slippage) + "0x", + { value: rbtcValueBeingSent, from: swapper } + ); + + const underlyingBalanceAfter = await SUSD.balanceOf(swapper); + const rbtcBalanceAfter = new BN(await web3.eth.getBalance(swapper)); + + let event_name = "ExternalSwap"; + let decode = decodeLogs(tx.receipt.rawLogs, SwapsExternal, event_name); + if (!decode.length) { + throw "Event ExternalSwap is not fired properly"; + } + + const user = decode[0].args["user"]; + const sourceToken = decode[0].args["sourceToken"]; + const destToken = decode[0].args["destToken"]; + const sourceAmount = decode[0].args["sourceAmount"]; + const destAmount = decode[0].args["destAmount"]; + const txFee = new BN((await etherGasCost(tx.receipt)).toString()); + + const finalUnderlyingBalance = underlyingBalanceAfter.sub(underlyingBalancePrev); + const finalRbtcBalance = rbtcBalancePrev.sub(rbtcBalanceAfter); + + expect(user).to.be.equal(swapper); + expect(sourceToken).to.be.equal(WRBTC.address); + expect(destToken).to.be.equal(SUSD.address); + expect(destAmount.toString()).to.be.equal(finalUnderlyingBalance.toString()); + expect(sourceAmount.toString()).to.be.equal(finalRbtcBalance.sub(txFee).toString()); + expect(sourceAmount.toString()).to.be.equal(rbtcValueBeingSent.toString()); + }); + + it("Swap external using RBTC should failed if source token amount is not matched with rbtc being sent", async () => { + const assetBalance = await loanToken.assetBalanceOf(lender); + const rbtcValueBeingSent = 1e14; + await SUSD.approve( + sovryn.address, + assetBalance.add(new BN(wei("10", "ether"))).toString() + ); + + await expectRevert( + sovryn.swapExternal( + constants.ZERO_ADDRESS, // source token must be wrbtc + SUSD.address, // dest token + lender, // receiver + lender, // return to sender address + rbtcValueBeingSent, // sourceTokenAmount + 0, // requiredDestTokenAmount + 0, // minReturn (slippage) + "0x", + { value: 2e14 } + ), + "sourceTokenAmount mismatch" + ); + }); + + // Should fail to change swap external fee percent by invalid value (more than 100%) + it("Test set swapExternalFeePercent with invalid value", async () => { + await expectRevert( + sovryn.setSwapExternalFeePercent(wei("101", "ether")), + "value too high" + ); + }); + }); }); diff --git a/tests/test_BProPriceFeed.js b/tests/test_BProPriceFeed.js index bed5078c5..0c6c1aff5 100644 --- a/tests/test_BProPriceFeed.js +++ b/tests/test_BProPriceFeed.js @@ -1,8 +1,8 @@ require("@openzeppelin/test-helpers/configure")({ - provider: web3.currentProvider, - singletons: { - abstraction: "truffle", - }, + provider: web3.currentProvider, + singletons: { + abstraction: "truffle", + }, }); const { expect } = require("chai"); @@ -14,29 +14,29 @@ const BProPriceFeed = artifacts.require("BProPriceFeed"); const BProPriceFeedMockup = artifacts.require("BProPriceFeedMockup"); contract("BProPriceFeed", () => { - let bproPriceFeed; + let bproPriceFeed; - beforeEach(async () => { - bProPriceFeedMockup = await BProPriceFeedMockup.new(); - await bProPriceFeedMockup.setValue(1); - bproPriceFeed = await BProPriceFeed.new(bProPriceFeedMockup.address); - }); + beforeEach(async () => { + bProPriceFeedMockup = await BProPriceFeedMockup.new(); + await bProPriceFeedMockup.setValue(1); + bproPriceFeed = await BProPriceFeed.new(bProPriceFeedMockup.address); + }); - it("should always return BPro USD Price for latestAnswer", async () => { - const bproUSDPrice = await bproPriceFeed.latestAnswer.call(); + it("should always return BPro USD Price for latestAnswer", async () => { + const bproUSDPrice = await bproPriceFeed.latestAnswer.call(); - expect(bproUSDPrice.toNumber()).to.be.above(0, "The Bpro USD Price must be larger than 0"); + expect(bproUSDPrice.toNumber()).to.be.above(0, "The Bpro USD Price must be larger than 0"); - if (bproUSDPrice > 0) { - console.log("The BPro USD Price is:", bproUSDPrice); - } - }); + if (bproUSDPrice > 0) { + console.log("The BPro USD Price is:", bproUSDPrice); + } + }); - it("should always return the current time for latestTimestamp", async () => { - expect(await bproPriceFeed.latestTimestamp.call()).to.be.bignumber.equal(await latest()); + it("should always return the current time for latestTimestamp", async () => { + expect(await bproPriceFeed.latestTimestamp.call()).to.be.bignumber.equal(await latest()); - await increase(duration.days(1)); + await increase(duration.days(1)); - expect(await bproPriceFeed.latestTimestamp.call()).to.be.bignumber.equal(await latest()); - }); + expect(await bproPriceFeed.latestTimestamp.call()).to.be.bignumber.equal(await latest()); + }); }); diff --git a/tests/test_PriceFeedRSKOracle.js b/tests/test_PriceFeedRSKOracle.js index 28fab2847..eb5926ec7 100644 --- a/tests/test_PriceFeedRSKOracle.js +++ b/tests/test_PriceFeedRSKOracle.js @@ -1,8 +1,8 @@ require("@openzeppelin/test-helpers/configure")({ - provider: web3.currentProvider, - singletons: { - abstraction: "truffle", - }, + provider: web3.currentProvider, + singletons: { + abstraction: "truffle", + }, }); const { expect } = require("chai"); @@ -14,29 +14,33 @@ const PriceFeedRSKOracle = artifacts.require("PriceFeedRSKOracle"); const PriceFeedRSKOracleMockup = artifacts.require("PriceFeedRSKOracleMockup"); contract("PriceFeedRSKOracle", () => { - let priceFeedRSKOracle; + let priceFeedRSKOracle; - beforeEach(async () => { - priceFeedRSKOracleMockup = await PriceFeedRSKOracleMockup.new(); - await priceFeedRSKOracleMockup.setValue(1); - priceFeedRSKOracle = await PriceFeedRSKOracle.new(priceFeedRSKOracleMockup.address); - }); + beforeEach(async () => { + priceFeedRSKOracleMockup = await PriceFeedRSKOracleMockup.new(); + await priceFeedRSKOracleMockup.setValue(1); + priceFeedRSKOracle = await PriceFeedRSKOracle.new(priceFeedRSKOracleMockup.address); + }); - it("should always return Price for latestAnswer", async () => { - const price = (await priceFeedRSKOracle.latestAnswer.call()).toNumber(); + it("should always return Price for latestAnswer", async () => { + const price = (await priceFeedRSKOracle.latestAnswer.call()).toNumber(); - expect(price).to.be.above(0, "The price must be larger than 0"); + expect(price).to.be.above(0, "The price must be larger than 0"); - if (price > 0) { - console.log("The price is:", price); - } - }); + if (price > 0) { + console.log("The price is:", price); + } + }); - it("should always return the current time for latestTimestamp", async () => { - expect(await priceFeedRSKOracle.latestTimestamp.call()).to.be.bignumber.equal(await latest()); + it("should always return the current time for latestTimestamp", async () => { + expect(await priceFeedRSKOracle.latestTimestamp.call()).to.be.bignumber.equal( + await latest() + ); - await increase(duration.days(1)); + await increase(duration.days(1)); - expect(await priceFeedRSKOracle.latestTimestamp.call()).to.be.bignumber.equal(await latest()); - }); + expect(await priceFeedRSKOracle.latestTimestamp.call()).to.be.bignumber.equal( + await latest() + ); + }); }); diff --git a/tests/testsFixtureBoilerplate.js b/tests/testsFixtureBoilerplate.js index 2fe1f9bf8..33117d844 100644 --- a/tests/testsFixtureBoilerplate.js +++ b/tests/testsFixtureBoilerplate.js @@ -9,53 +9,53 @@ const { waffle } = require("hardhat"); const { loadFixture } = waffle; const { BN, constants, expectEvent, expectRevert } = require("@openzeppelin/test-helpers"); const { - getSUSD, - getRBTC, - getWRBTC, - getBZRX, - getLoanTokenLogic, - getLoanToken, - getLoanTokenLogicWrbtc, - getLoanTokenWRBTC, - loan_pool_setup, - set_demand_curve, - getPriceFeeds, - getSovryn, - decodeLogs, - getSOV, + getSUSD, + getRBTC, + getWRBTC, + getBZRX, + getLoanTokenLogic, + getLoanToken, + getLoanTokenLogicWrbtc, + getLoanTokenWRBTC, + loan_pool_setup, + set_demand_curve, + getPriceFeeds, + getSovryn, + decodeLogs, + getSOV, } = require("./Utils/initializer.js"); contract("ContractName", (accounts) => { - let root, account1, account2, account3, account4; - let sovryn, SUSD, WRBTC, RBTC, BZRX, priceFeeds, SOV; + let root, account1, account2, account3, account4; + let sovryn, SUSD, WRBTC, RBTC, BZRX, priceFeeds, SOV; - async function deploymentAndInitFixture(_wallets, _provider) { - // Deploying sovrynProtocol w/ generic function from initializer.js - SUSD = await getSUSD(); - RBTC = await getRBTC(); - WRBTC = await getWRBTC(); - BZRX = await getBZRX(); - priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); - sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); - await sovryn.setSovrynProtocolAddress(sovryn.address); + async function deploymentAndInitFixture(_wallets, _provider) { + // Deploying sovrynProtocol w/ generic function from initializer.js + SUSD = await getSUSD(); + RBTC = await getRBTC(); + WRBTC = await getWRBTC(); + BZRX = await getBZRX(); + priceFeeds = await getPriceFeeds(WRBTC, SUSD, RBTC, BZRX); + sovryn = await getSovryn(WRBTC, SUSD, RBTC, priceFeeds); + await sovryn.setSovrynProtocolAddress(sovryn.address); - // Protocol token - SOV = await getSOV(sovryn, priceFeeds, SUSD, accounts); - } + // Protocol token + SOV = await getSOV(sovryn, priceFeeds, SUSD, accounts); + } - before(async () => { - [root, account1, account2, account3, account4, ...accounts] = accounts; - }); + before(async () => { + [root, account1, account2, account3, account4, ...accounts] = accounts; + }); - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); - describe("Boilerplate", () => { - it("SOV total supply", async () => { - let totalSupply = await SOV.totalSupply(); - // console.log("SOV totalSupply: ", totalSupply.toString()); - expect(totalSupply).to.be.bignumber.equal(new BN(10).pow(new BN(50))); - }); - }); + describe("Boilerplate", () => { + it("SOV total supply", async () => { + let totalSupply = await SOV.totalSupply(); + // console.log("SOV totalSupply: ", totalSupply.toString()); + expect(totalSupply).to.be.bignumber.equal(new BN(10).pow(new BN(50))); + }); + }); }); diff --git a/tests/token/SOVTest.js b/tests/token/SOVTest.js index bb0d2cf28..be6ff0127 100644 --- a/tests/token/SOVTest.js +++ b/tests/token/SOVTest.js @@ -30,140 +30,149 @@ const SYMBOL = "SOV"; const DECIMALS = 18; contract("SOV:", (accounts) => { - let root, account1, account2; - let tokenSOV; - let amount, beforeBalance1; - - async function deploymentAndInitFixture(_wallets, _provider) { - tokenSOV = await SOV.new(TOTAL_SUPPLY); - - // Initial transfer to account 1 - amount = 1000; - beforeBalance1 = await tokenSOV.balanceOf.call(account1); - await tokenSOV.mint(account1, amount); - } - - before(async () => { - [root, account1, account2, ...accounts] = accounts; - }); - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - describe("constructor:", () => { - it("checks the deployment values", async () => { - expect(await tokenSOV.name.call()).to.be.equal(NAME); - expect(await tokenSOV.symbol.call()).to.be.equal(SYMBOL); - expect(await tokenSOV.decimals.call()).to.be.bignumber.equal(new BN(DECIMALS)); - - // Check whether deployer's token balance is equal to total supply. - expect(await tokenSOV.balanceOf.call(root)).to.be.bignumber.equal(TOTAL_SUPPLY); - - // Token contract balance is always zero - let balance = await tokenSOV.balanceOf.call(tokenSOV.address); - expect(balance.toNumber()).to.be.equal(0); - }); - - it("zero token balance if initial amount is zero", async () => { - // Redeployment w/ zero initial supply. - let tokenSOV = await SOV.new(ZERO); - - // Check whether deployer's token balance is zero. - expect(await tokenSOV.balanceOf.call(root)).to.be.bignumber.equal(ZERO); - - // Even though initial supply is zero, it is possible to mint new tokens w/o reverting. - await tokenSOV.mint(account1, amount); - - // Check mint has been effective, account1 holds the tokens. - expect(await tokenSOV.balanceOf.call(account1)).to.be.bignumber.equal(new BN(amount)); - }); - }); - - describe("mint:", () => { - it("should be able to mint SOV tokens", async () => { - let afterBalance1 = await tokenSOV.balanceOf.call(account1); - expect(afterBalance1.sub(beforeBalance1).toNumber()).to.be.equal(amount); - }); - - it("revert if mint on behalf of zero address", async () => { - await expectRevert(tokenSOV.mint(zeroAddress, amount), "revert ERC20: mint to the zero address"); - }); - }); - - describe("transfer:", () => { - it("should be able to transfer SOV tokens", async () => { - let afterBalance1 = await tokenSOV.balanceOf.call(account1); - expect(afterBalance1.sub(beforeBalance1).toNumber()).to.be.equal(amount); - - // Transfer whole amount to account2 - let beforeBalance2 = await tokenSOV.balanceOf.call(account2); - await tokenSOV.transfer(account2, amount, { from: account1 }); - let afterBalance2 = await tokenSOV.balanceOf.call(account2); - expect(afterBalance2.sub(beforeBalance2).toNumber()).to.be.equal(amount); - }); - - it("shouldn't be able to transfer more SOV tokens than available on balance", async () => { - // Try to transfer double amount to account2 - await expectRevert( - tokenSOV.transfer(account2, amount * 2, { from: account1 }), - "revert ERC20: transfer amount exceeds balance" - ); - }); - - it("shouldn't be able to transfer SOV tokens to zero address", async () => { - // Try to transfer amount to zero address - await expectRevert(tokenSOV.transfer(zeroAddress, amount, { from: account1 }), "revert ERC20: transfer to the zero address"); - }); - - /// @dev Instead of throwing the expected error from ERC20.sol contract - /// it is throwing : "unknown account 0x0000000000000000000000000000000000000000" - // it("shouldn't be able to transfer SOV tokens from zero address", async () => { - // // Try to transfer amount from zero address - // await expectRevert(tokenSOV.transfer(account2, amount, { from: zeroAddress }), "revert ERC20: transfer from the zero address"); - // }); - }); - - describe("approve:", () => { - it("should be able to approve a SOV token transfer", async () => { - // Approve whole amount to be spent by account2 from account1 - let beforeAllowance2 = await tokenSOV.allowance.call(account1, account2); - await tokenSOV.approve(account2, amount, { from: account1 }); - let afterAllowance2 = await tokenSOV.allowance.call(account1, account2); - expect(afterAllowance2.sub(beforeAllowance2).toNumber()).to.be.equal(amount); - }); - - it("shouldn't be able to approve SOV tokens to be spent by zero address", async () => { - // Try to approve amount for zero address to spend - await expectRevert(tokenSOV.approve(zeroAddress, amount, { from: account1 }), "revert ERC20: approve to the zero address"); - }); - - it("should be able to increase the allowance for a spender", async () => { - // Increase allowance by amount to be spent by account2 from account1 - let beforeAllowance2 = await tokenSOV.allowance.call(account1, account2); - await tokenSOV.increaseAllowance(account2, amount, { from: account1 }); - let afterAllowance2 = await tokenSOV.allowance.call(account1, account2); - expect(afterAllowance2.sub(beforeAllowance2).toNumber()).to.be.equal(amount); - }); - - it("shouldn't be able to decrease the allowance below zero", async () => { - // Try to decrease an allowance below zero - await expectRevert( - tokenSOV.decreaseAllowance(account2, amount, { from: account1 }), - "revert ERC20: decreased allowance below zero" - ); - }); - - it("should be able to decrease the allowance for a spender", async () => { - // Approve double amount to be spent by account2 from account1 - await tokenSOV.approve(account2, amount * 2, { from: account1 }); - - // Decrease allowance by amount to be spent by account2 from account1 - await tokenSOV.decreaseAllowance(account2, amount, { from: account1 }); - let afterAllowance2 = await tokenSOV.allowance.call(account1, account2); - - // Allowance should be equal to amount - expect(afterAllowance2.toNumber()).to.be.equal(amount); - }); - }); + let root, account1, account2; + let tokenSOV; + let amount, beforeBalance1; + + async function deploymentAndInitFixture(_wallets, _provider) { + tokenSOV = await SOV.new(TOTAL_SUPPLY); + + // Initial transfer to account 1 + amount = 1000; + beforeBalance1 = await tokenSOV.balanceOf.call(account1); + await tokenSOV.mint(account1, amount); + } + + before(async () => { + [root, account1, account2, ...accounts] = accounts; + }); + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + describe("constructor:", () => { + it("checks the deployment values", async () => { + expect(await tokenSOV.name.call()).to.be.equal(NAME); + expect(await tokenSOV.symbol.call()).to.be.equal(SYMBOL); + expect(await tokenSOV.decimals.call()).to.be.bignumber.equal(new BN(DECIMALS)); + + // Check whether deployer's token balance is equal to total supply. + expect(await tokenSOV.balanceOf.call(root)).to.be.bignumber.equal(TOTAL_SUPPLY); + + // Token contract balance is always zero + let balance = await tokenSOV.balanceOf.call(tokenSOV.address); + expect(balance.toNumber()).to.be.equal(0); + }); + + it("zero token balance if initial amount is zero", async () => { + // Redeployment w/ zero initial supply. + let tokenSOV = await SOV.new(ZERO); + + // Check whether deployer's token balance is zero. + expect(await tokenSOV.balanceOf.call(root)).to.be.bignumber.equal(ZERO); + + // Even though initial supply is zero, it is possible to mint new tokens w/o reverting. + await tokenSOV.mint(account1, amount); + + // Check mint has been effective, account1 holds the tokens. + expect(await tokenSOV.balanceOf.call(account1)).to.be.bignumber.equal(new BN(amount)); + }); + }); + + describe("mint:", () => { + it("should be able to mint SOV tokens", async () => { + let afterBalance1 = await tokenSOV.balanceOf.call(account1); + expect(afterBalance1.sub(beforeBalance1).toNumber()).to.be.equal(amount); + }); + + it("revert if mint on behalf of zero address", async () => { + await expectRevert( + tokenSOV.mint(zeroAddress, amount), + "revert ERC20: mint to the zero address" + ); + }); + }); + + describe("transfer:", () => { + it("should be able to transfer SOV tokens", async () => { + let afterBalance1 = await tokenSOV.balanceOf.call(account1); + expect(afterBalance1.sub(beforeBalance1).toNumber()).to.be.equal(amount); + + // Transfer whole amount to account2 + let beforeBalance2 = await tokenSOV.balanceOf.call(account2); + await tokenSOV.transfer(account2, amount, { from: account1 }); + let afterBalance2 = await tokenSOV.balanceOf.call(account2); + expect(afterBalance2.sub(beforeBalance2).toNumber()).to.be.equal(amount); + }); + + it("shouldn't be able to transfer more SOV tokens than available on balance", async () => { + // Try to transfer double amount to account2 + await expectRevert( + tokenSOV.transfer(account2, amount * 2, { from: account1 }), + "revert ERC20: transfer amount exceeds balance" + ); + }); + + it("shouldn't be able to transfer SOV tokens to zero address", async () => { + // Try to transfer amount to zero address + await expectRevert( + tokenSOV.transfer(zeroAddress, amount, { from: account1 }), + "revert ERC20: transfer to the zero address" + ); + }); + + /// @dev Instead of throwing the expected error from ERC20.sol contract + /// it is throwing : "unknown account 0x0000000000000000000000000000000000000000" + // it("shouldn't be able to transfer SOV tokens from zero address", async () => { + // // Try to transfer amount from zero address + // await expectRevert(tokenSOV.transfer(account2, amount, { from: zeroAddress }), "revert ERC20: transfer from the zero address"); + // }); + }); + + describe("approve:", () => { + it("should be able to approve a SOV token transfer", async () => { + // Approve whole amount to be spent by account2 from account1 + let beforeAllowance2 = await tokenSOV.allowance.call(account1, account2); + await tokenSOV.approve(account2, amount, { from: account1 }); + let afterAllowance2 = await tokenSOV.allowance.call(account1, account2); + expect(afterAllowance2.sub(beforeAllowance2).toNumber()).to.be.equal(amount); + }); + + it("shouldn't be able to approve SOV tokens to be spent by zero address", async () => { + // Try to approve amount for zero address to spend + await expectRevert( + tokenSOV.approve(zeroAddress, amount, { from: account1 }), + "revert ERC20: approve to the zero address" + ); + }); + + it("should be able to increase the allowance for a spender", async () => { + // Increase allowance by amount to be spent by account2 from account1 + let beforeAllowance2 = await tokenSOV.allowance.call(account1, account2); + await tokenSOV.increaseAllowance(account2, amount, { from: account1 }); + let afterAllowance2 = await tokenSOV.allowance.call(account1, account2); + expect(afterAllowance2.sub(beforeAllowance2).toNumber()).to.be.equal(amount); + }); + + it("shouldn't be able to decrease the allowance below zero", async () => { + // Try to decrease an allowance below zero + await expectRevert( + tokenSOV.decreaseAllowance(account2, amount, { from: account1 }), + "revert ERC20: decreased allowance below zero" + ); + }); + + it("should be able to decrease the allowance for a spender", async () => { + // Approve double amount to be spent by account2 from account1 + await tokenSOV.approve(account2, amount * 2, { from: account1 }); + + // Decrease allowance by amount to be spent by account2 from account1 + await tokenSOV.decreaseAllowance(account2, amount, { from: account1 }); + let afterAllowance2 = await tokenSOV.allowance.call(account1, account2); + + // Allowance should be equal to amount + expect(afterAllowance2.toNumber()).to.be.equal(amount); + }); + }); }); diff --git a/tests/utilities/ethereum.js b/tests/utilities/ethereum.js index c6afc4b39..4d8d4508e 100644 --- a/tests/utilities/ethereum.js +++ b/tests/utilities/ethereum.js @@ -4,159 +4,165 @@ const BigNumber = require("bignumber.js"); const ethers = require("ethers"); function UInt256Max() { - return ethers.constants.MaxUint256; + return ethers.constants.MaxUint256; } function address(n) { - return `0x${n.toString(16).padStart(40, "0")}`; + return `0x${n.toString(16).padStart(40, "0")}`; } function encodeParameters(types, values) { - const abi = new ethers.utils.AbiCoder(); - return abi.encode(types, values); + const abi = new ethers.utils.AbiCoder(); + return abi.encode(types, values); } async function etherBalance(addr) { - return new BigNumber(await web3.eth.getBalance(addr)); + return new BigNumber(await web3.eth.getBalance(addr)); } async function etherGasCost(receipt) { - const tx = await web3.eth.getTransaction(receipt.transactionHash); - const gasUsed = new BigNumber(receipt.gasUsed); - const gasPrice = new BigNumber(tx.gasPrice); - return gasUsed.times(gasPrice); + const tx = await web3.eth.getTransaction(receipt.transactionHash); + const gasUsed = new BigNumber(receipt.gasUsed); + const gasPrice = new BigNumber(tx.gasPrice); + return gasUsed.times(gasPrice); } function etherExp(num) { - return etherMantissa(num, 1e18); + return etherMantissa(num, 1e18); } function etherDouble(num) { - return etherMantissa(num, 1e36); + return etherMantissa(num, 1e36); } function etherMantissa(num, scale = 1e18) { - if (num < 0) return new BigNumber(2).pow(256).plus(num); - return new BigNumber(num).times(scale); + if (num < 0) return new BigNumber(2).pow(256).plus(num); + return new BigNumber(num).times(scale); } function etherUnsigned(num) { - return new BigNumber(num); + return new BigNumber(num); } function mergeInterface(into, from) { - const key = (item) => (item.inputs ? `${item.name}/${item.inputs.length}` : item.name); - const existing = into.options.jsonInterface.reduce((acc, item) => { - acc[key(item)] = true; - return acc; - }, {}); - const extended = from.options.jsonInterface.reduce((acc, item) => { - if (!(key(item) in existing)) acc.push(item); - return acc; - }, into.options.jsonInterface.slice()); - into.options.jsonInterface = into.options.jsonInterface.concat(from.options.jsonInterface); - return into; + const key = (item) => (item.inputs ? `${item.name}/${item.inputs.length}` : item.name); + const existing = into.options.jsonInterface.reduce((acc, item) => { + acc[key(item)] = true; + return acc; + }, {}); + const extended = from.options.jsonInterface.reduce((acc, item) => { + if (!(key(item) in existing)) acc.push(item); + return acc; + }, into.options.jsonInterface.slice()); + into.options.jsonInterface = into.options.jsonInterface.concat(from.options.jsonInterface); + return into; } function getContractDefaults() { - return { gas: 20000000, gasPrice: 20000 }; + return { gas: 20000000, gasPrice: 20000 }; } function keccak256(values) { - return ethers.utils.keccak256(values); + return ethers.utils.keccak256(values); } function unlockedAccounts() { - let provider = web3.currentProvider; - if (provider._providers) provider = provider._providers.find((p) => p._ganacheProvider)._ganacheProvider; - return provider.manager.state.unlocked_accounts; + let provider = web3.currentProvider; + if (provider._providers) + provider = provider._providers.find((p) => p._ganacheProvider)._ganacheProvider; + return provider.manager.state.unlocked_accounts; } function unlockedAccount(a) { - return unlockedAccounts()[a.toLowerCase()]; + return unlockedAccounts()[a.toLowerCase()]; } async function mineBlockNumber(blockNumber) { - return rpc({ method: "evm_mineBlockNumber", params: [blockNumber] }); + return rpc({ method: "evm_mineBlockNumber", params: [blockNumber] }); } async function mineBlock() { - return rpc({ method: "evm_mine" }); + return rpc({ method: "evm_mine" }); } async function increaseTime(seconds) { - await rpc({ method: "evm_increaseTime", params: [seconds] }); - return rpc({ method: "evm_mine" }); + await rpc({ method: "evm_increaseTime", params: [seconds] }); + return rpc({ method: "evm_mine" }); } async function setTime(seconds) { - await rpc({ method: "evm_setTime", params: [new Date(seconds * 1000)] }); + await rpc({ method: "evm_setTime", params: [new Date(seconds * 1000)] }); } async function freezeTime(seconds) { - await rpc({ method: "evm_freezeTime", params: [seconds] }); - return rpc({ method: "evm_mine" }); + await rpc({ method: "evm_freezeTime", params: [seconds] }); + return rpc({ method: "evm_mine" }); } async function advanceBlocks(blocks) { - let { result: num } = await rpc({ method: "eth_blockNumber" }); - await rpc({ method: "evm_mineBlockNumber", params: [blocks + parseInt(num)] }); + let { result: num } = await rpc({ method: "eth_blockNumber" }); + await rpc({ method: "evm_mineBlockNumber", params: [blocks + parseInt(num)] }); } async function blockNumber() { - let { result: num } = await rpc({ method: "eth_blockNumber" }); - return parseInt(num); + let { result: num } = await rpc({ method: "eth_blockNumber" }); + return parseInt(num); } async function minerStart() { - return rpc({ method: "miner_start" }); + return rpc({ method: "miner_start" }); } async function minerStop() { - return rpc({ method: "miner_stop" }); + return rpc({ method: "miner_stop" }); } async function rpc(request) { - return new Promise((okay, fail) => web3.currentProvider.send(request, (err, res) => (err ? fail(err) : okay(res)))); + return new Promise((okay, fail) => + web3.currentProvider.send(request, (err, res) => (err ? fail(err) : okay(res))) + ); } async function both(contract, method, args = [], opts = {}) { - const reply = await call(contract, method, args, opts); - const receipt = await send(contract, method, args, opts); - return { reply, receipt }; + const reply = await call(contract, method, args, opts); + const receipt = await send(contract, method, args, opts); + return { reply, receipt }; } async function sendFallback(contract, opts = {}) { - const receipt = await web3.eth.sendTransaction({ to: contract._address, ...Object.assign(getContractDefaults(), opts) }); - return Object.assign(receipt, { events: receipt.logs }); + const receipt = await web3.eth.sendTransaction({ + to: contract._address, + ...Object.assign(getContractDefaults(), opts), + }); + return Object.assign(receipt, { events: receipt.logs }); } module.exports = { - address, - encodeParameters, - etherBalance, - etherGasCost, - etherExp, - etherDouble, - etherMantissa, - etherUnsigned, - mergeInterface, - keccak256, - unlockedAccounts, - unlockedAccount, - - advanceBlocks, - blockNumber, - freezeTime, - increaseTime, - mineBlock, - mineBlockNumber, - minerStart, - minerStop, - rpc, - setTime, - - both, - sendFallback, - UInt256Max, + address, + encodeParameters, + etherBalance, + etherGasCost, + etherExp, + etherDouble, + etherMantissa, + etherUnsigned, + mergeInterface, + keccak256, + unlockedAccounts, + unlockedAccount, + + advanceBlocks, + blockNumber, + freezeTime, + increaseTime, + mineBlock, + mineBlockNumber, + minerStart, + minerStop, + rpc, + setTime, + + both, + sendFallback, + UInt256Max, }; diff --git a/tests/vesting/DevelopmentFund/anyone.test.js b/tests/vesting/DevelopmentFund/anyone.test.js index e99255330..923e21e4a 100644 --- a/tests/vesting/DevelopmentFund/anyone.test.js +++ b/tests/vesting/DevelopmentFund/anyone.test.js @@ -15,8 +15,8 @@ const DevelopmentFund = artifacts.require("DevelopmentFund"); const TestToken = artifacts.require("TestToken"); const { - BN, // Big Number support. - expectRevert, // Assertions for transactions that should fail. + BN, // Big Number support. + expectRevert, // Assertions for transactions that should fail. } = require("@openzeppelin/test-helpers"); const { assert } = require("chai"); @@ -38,7 +38,7 @@ let totalReleaseTokenAmount = 0; * @return {number} Random Value. */ function randomValue() { - return Math.floor(Math.random() * 1000); + return Math.floor(Math.random() * 1000); } /** @@ -47,14 +47,14 @@ function randomValue() { * @returns releaseTokenAmounts The release token amount array. */ function createReleaseTokenAmount() { - let balance = totalSupply; - let releaseTokenAmounts = []; - for (let times = 0; times < 60; times++) { - let newValue = randomValue() * 10; // Get's a number between 0 to 10000. - balance -= newValue; - releaseTokenAmounts.push(newValue); - } - return releaseTokenAmounts; + let balance = totalSupply; + let releaseTokenAmounts = []; + for (let times = 0; times < 60; times++) { + let newValue = randomValue() * 10; // Get's a number between 0 to 10000. + balance -= newValue; + releaseTokenAmounts.push(newValue); + } + return releaseTokenAmounts; } /** @@ -64,115 +64,129 @@ function createReleaseTokenAmount() { * @returns totalTokenAmounts The total number of tokens for the release. */ function calculateTotalTokenAmount(releaseTokenAmounts) { - return releaseTokenAmounts.reduce((a, b) => a + b, 0); + return releaseTokenAmounts.reduce((a, b) => a + b, 0); } contract("DevelopmentFund (Any User Functions)", (accounts) => { - let developmentFund, testToken; - let creator, governance, newGovernance, multisig, newMultisig, safeVault, userOne; - - async function deploymentAndInitFixture(_wallets, _provider) { - // Creating a new release schedule. - releaseDuration = []; - - // This is run 60 times for mimicking 5 years (12 months * 5), though the interval is small. - for (let times = 0; times < 60; times++) { - releaseDuration.push(releaseInterval); - } - - // Creating a new release token schedule. - releaseTokenAmount = createReleaseTokenAmount(); - - // Calculating the total tokens in the release schedule. - totalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); - - // Minting new Tokens. - await testToken.mint(creator, totalSupply, { from: creator }); - await testToken.mint(userOne, totalReleaseTokenAmount); - - // Anyone should be able to fund the initial token release schedule amount to make contract active. - developmentFund = await DevelopmentFund.new( - testToken.address, - governance, - safeVault, - multisig, - zero, - releaseDuration, - releaseTokenAmount, - { from: creator } - ); - - await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { from: userOne }); - await developmentFund.init({ from: userOne }); - } - - before("Initiating Accounts & Creating Test Token Instance.", async () => { - // Checking if we have enough accounts to test. - assert.isAtLeast(accounts.length, 7, "At least 7 accounts are required to test the contracts."); - [creator, governance, newGovernance, multisig, newMultisig, safeVault, userOne] = accounts; - - // Creating the instance of Test Token. - testToken = await TestToken.new("TestToken", "TST", 18, zero); - }); - - beforeEach("Creating New Development Fund Instance.", async () => { - await loadFixture(deploymentAndInitFixture); - }); - - it("No one should be able to call the init() more than once.", async () => { - await expectRevert(developmentFund.init({ from: userOne }), "The contract is not in the right state."); - }); - - it("Except Locked Token Owner, no one should be able to add new Locked Token Owner.", async () => { - await expectRevert( - developmentFund.updateLockedTokenOwner(newGovernance, { from: userOne }), - "Only Locked Token Owner can call this." - ); - }); - - it("Except current Unlocked Token Owner, no one should be able to approve Locked Token Owner.", async () => { - await expectRevert(developmentFund.approveLockedTokenOwner({ from: userOne }), "Only Unlocked Token Owner can call this."); - }); - - it("Except Locked Token Owner, no one should be able to update Unlocked Token Owner.", async () => { - await expectRevert( - developmentFund.updateUnlockedTokenOwner(newMultisig, { from: userOne }), - "Only Locked Token Owner can call this." - ); - }); - - it("Anyone could deposit Tokens.", async () => { - let value = randomValue() + 1; - await testToken.mint(userOne, value); - await testToken.approve(developmentFund.address, value, { from: userOne }); - await developmentFund.depositTokens(value, { from: userOne }); - }); - - it("Except Locked Token Owner, no one should be able to change the release schedule.", async () => { - await expectRevert( - developmentFund.changeTokenReleaseSchedule(zero, releaseDuration, releaseTokenAmount, { from: userOne }), - "Only Locked Token Owner can call this." - ); - }); - - it("Except Unlocked Token Owner, no one should be able to transfer all token to safeVault.", async () => { - await expectRevert( - developmentFund.transferTokensByUnlockedTokenOwner({ from: userOne }), - "Only Unlocked Token Owner can call this." - ); - }); - - it("Except Unlocked Token Owner, no one should be able to withdraw tokens from schedule.", async () => { - await expectRevert( - developmentFund.withdrawTokensByUnlockedTokenOwner(zero, { from: userOne }), - "Only Unlocked Token Owner can call this." - ); - }); - - it("Except Locked Token Owner, no one should be able to transfer all tokens to a receiver.", async () => { - await expectRevert( - developmentFund.transferTokensByLockedTokenOwner(creator, { from: userOne }), - "Only Locked Token Owner can call this." - ); - }); + let developmentFund, testToken; + let creator, governance, newGovernance, multisig, newMultisig, safeVault, userOne; + + async function deploymentAndInitFixture(_wallets, _provider) { + // Creating a new release schedule. + releaseDuration = []; + + // This is run 60 times for mimicking 5 years (12 months * 5), though the interval is small. + for (let times = 0; times < 60; times++) { + releaseDuration.push(releaseInterval); + } + + // Creating a new release token schedule. + releaseTokenAmount = createReleaseTokenAmount(); + + // Calculating the total tokens in the release schedule. + totalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); + + // Minting new Tokens. + await testToken.mint(creator, totalSupply, { from: creator }); + await testToken.mint(userOne, totalReleaseTokenAmount); + + // Anyone should be able to fund the initial token release schedule amount to make contract active. + developmentFund = await DevelopmentFund.new( + testToken.address, + governance, + safeVault, + multisig, + zero, + releaseDuration, + releaseTokenAmount, + { from: creator } + ); + + await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { + from: userOne, + }); + await developmentFund.init({ from: userOne }); + } + + before("Initiating Accounts & Creating Test Token Instance.", async () => { + // Checking if we have enough accounts to test. + assert.isAtLeast( + accounts.length, + 7, + "At least 7 accounts are required to test the contracts." + ); + [creator, governance, newGovernance, multisig, newMultisig, safeVault, userOne] = accounts; + + // Creating the instance of Test Token. + testToken = await TestToken.new("TestToken", "TST", 18, zero); + }); + + beforeEach("Creating New Development Fund Instance.", async () => { + await loadFixture(deploymentAndInitFixture); + }); + + it("No one should be able to call the init() more than once.", async () => { + await expectRevert( + developmentFund.init({ from: userOne }), + "The contract is not in the right state." + ); + }); + + it("Except Locked Token Owner, no one should be able to add new Locked Token Owner.", async () => { + await expectRevert( + developmentFund.updateLockedTokenOwner(newGovernance, { from: userOne }), + "Only Locked Token Owner can call this." + ); + }); + + it("Except current Unlocked Token Owner, no one should be able to approve Locked Token Owner.", async () => { + await expectRevert( + developmentFund.approveLockedTokenOwner({ from: userOne }), + "Only Unlocked Token Owner can call this." + ); + }); + + it("Except Locked Token Owner, no one should be able to update Unlocked Token Owner.", async () => { + await expectRevert( + developmentFund.updateUnlockedTokenOwner(newMultisig, { from: userOne }), + "Only Locked Token Owner can call this." + ); + }); + + it("Anyone could deposit Tokens.", async () => { + let value = randomValue() + 1; + await testToken.mint(userOne, value); + await testToken.approve(developmentFund.address, value, { from: userOne }); + await developmentFund.depositTokens(value, { from: userOne }); + }); + + it("Except Locked Token Owner, no one should be able to change the release schedule.", async () => { + await expectRevert( + developmentFund.changeTokenReleaseSchedule(zero, releaseDuration, releaseTokenAmount, { + from: userOne, + }), + "Only Locked Token Owner can call this." + ); + }); + + it("Except Unlocked Token Owner, no one should be able to transfer all token to safeVault.", async () => { + await expectRevert( + developmentFund.transferTokensByUnlockedTokenOwner({ from: userOne }), + "Only Unlocked Token Owner can call this." + ); + }); + + it("Except Unlocked Token Owner, no one should be able to withdraw tokens from schedule.", async () => { + await expectRevert( + developmentFund.withdrawTokensByUnlockedTokenOwner(zero, { from: userOne }), + "Only Unlocked Token Owner can call this." + ); + }); + + it("Except Locked Token Owner, no one should be able to transfer all tokens to a receiver.", async () => { + await expectRevert( + developmentFund.transferTokensByLockedTokenOwner(creator, { from: userOne }), + "Only Locked Token Owner can call this." + ); + }); }); diff --git a/tests/vesting/DevelopmentFund/creator.test.js b/tests/vesting/DevelopmentFund/creator.test.js index 48913ad03..26a737574 100644 --- a/tests/vesting/DevelopmentFund/creator.test.js +++ b/tests/vesting/DevelopmentFund/creator.test.js @@ -14,8 +14,8 @@ const DevelopmentFund = artifacts.require("DevelopmentFund"); const TestToken = artifacts.require("TestToken"); const { - BN, // Big Number support. - expectRevert, // Assertions for transactions that should fail. + BN, // Big Number support. + expectRevert, // Assertions for transactions that should fail. } = require("@openzeppelin/test-helpers"); const { assert } = require("chai"); @@ -37,7 +37,7 @@ let totalReleaseTokenAmount = 0; * @return {number} Random Value. */ function randomValue() { - return Math.floor(Math.random() * 1000); + return Math.floor(Math.random() * 1000); } /** @@ -46,14 +46,14 @@ function randomValue() { * @returns releaseTokenAmounts The release token amount array. */ function createReleaseTokenAmount() { - let balance = totalSupply; - let releaseTokenAmounts = []; - for (let times = 0; times < 60; times++) { - let newValue = randomValue() * 10; // Get's a number between 0 to 10000. - balance -= newValue; - releaseTokenAmounts.push(newValue); - } - return releaseTokenAmounts; + let balance = totalSupply; + let releaseTokenAmounts = []; + for (let times = 0; times < 60; times++) { + let newValue = randomValue() * 10; // Get's a number between 0 to 10000. + balance -= newValue; + releaseTokenAmounts.push(newValue); + } + return releaseTokenAmounts; } /** @@ -63,97 +63,123 @@ function createReleaseTokenAmount() { * @returns totalTokenAmounts The total number of tokens for the release. */ function calculateTotalTokenAmount(releaseTokenAmounts) { - return releaseTokenAmounts.reduce((a, b) => a + b, 0); + return releaseTokenAmounts.reduce((a, b) => a + b, 0); } contract("DevelopmentFund (Contract Creator Functions)", (accounts) => { - let developmentFund, testToken; - let creator, governance, newGovernance, multisig, newMultisig, safeVault, userOne; - - async function deploymentAndInitFixture(_wallets, _provider) { - // Creating a new release schedule. - releaseDuration = []; - // This is run 60 times for mimicking 5 years (12 months * 5), though the interval is small. - for (let times = 0; times < 60; times++) { - releaseDuration.push(releaseInterval); - } - - // Creating a new release token schedule. - releaseTokenAmount = createReleaseTokenAmount(); - - // Creating the contract instance. - developmentFund = await DevelopmentFund.new( - testToken.address, - governance, - safeVault, - multisig, - zero, - releaseDuration, - releaseTokenAmount, - { from: creator } - ); - - // Calculating the total tokens in the release schedule. - totalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); - - // Minting new Tokens. - await testToken.mint(creator, totalSupply, { from: creator }); - - // Approving the development fund to do a transfer on behalf of governance. - await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { from: creator }); - - // Marking the contract as active. - await developmentFund.init({ from: creator }); - } - - before("Initiating Accounts & Creating Test Token Instance.", async () => { - // Checking if we have enough accounts to test. - assert.isAtLeast(accounts.length, 7, "At least 7 accounts are required to test the contracts."); - [creator, governance, newGovernance, multisig, newMultisig, safeVault, userOne] = accounts; - - // Creating the instance of Test Token. - testToken = await TestToken.new("TestToken", "TST", 18, zero); - }); - - beforeEach("Creating New Development Fund Instance.", async () => { - await loadFixture(deploymentAndInitFixture); - }); - - it("Contract Creator should not be able to call the init() more than once.", async () => { - await expectRevert(developmentFund.init({ from: creator }), "The contract is not in the right state."); - }); - - it("Contract Creator should not be able to add Locked Token Owner.", async () => { - await expectRevert(developmentFund.updateLockedTokenOwner(newGovernance), "Only Locked Token Owner can call this."); - }); - - it("Contract Creator should not be able to approve a Locked Token Owner.", async () => { - await expectRevert(developmentFund.approveLockedTokenOwner(), "Only Unlocked Token Owner can call this."); - }); - - it("Contract Creator should not be able to update Unlocked Token Owner.", async () => { - await expectRevert(developmentFund.updateUnlockedTokenOwner(newMultisig), "Only Locked Token Owner can call this."); - }); - - it("Contract Creator should not be able to change the release schedule.", async () => { - await expectRevert( - developmentFund.changeTokenReleaseSchedule(zero, releaseDuration, releaseTokenAmount), - "Only Locked Token Owner can call this." - ); - }); - - it("Contract Creator should not be able to transfer all tokens to safeVault.", async () => { - await expectRevert(developmentFund.transferTokensByUnlockedTokenOwner(), "Only Unlocked Token Owner can call this."); - }); - - it("Contract Creator should not be able to withdraw tokens after schedule.", async () => { - await expectRevert( - developmentFund.withdrawTokensByUnlockedTokenOwner(releaseTokenAmount[releaseTokenAmount.length - 1]), - "Only Unlocked Token Owner can call this." - ); - }); - - it("Contract Creator should not be able to transfer all tokens to a receiver.", async () => { - await expectRevert(developmentFund.transferTokensByLockedTokenOwner(creator), "Only Locked Token Owner can call this."); - }); + let developmentFund, testToken; + let creator, governance, newGovernance, multisig, newMultisig, safeVault, userOne; + + async function deploymentAndInitFixture(_wallets, _provider) { + // Creating a new release schedule. + releaseDuration = []; + // This is run 60 times for mimicking 5 years (12 months * 5), though the interval is small. + for (let times = 0; times < 60; times++) { + releaseDuration.push(releaseInterval); + } + + // Creating a new release token schedule. + releaseTokenAmount = createReleaseTokenAmount(); + + // Creating the contract instance. + developmentFund = await DevelopmentFund.new( + testToken.address, + governance, + safeVault, + multisig, + zero, + releaseDuration, + releaseTokenAmount, + { from: creator } + ); + + // Calculating the total tokens in the release schedule. + totalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); + + // Minting new Tokens. + await testToken.mint(creator, totalSupply, { from: creator }); + + // Approving the development fund to do a transfer on behalf of governance. + await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { + from: creator, + }); + + // Marking the contract as active. + await developmentFund.init({ from: creator }); + } + + before("Initiating Accounts & Creating Test Token Instance.", async () => { + // Checking if we have enough accounts to test. + assert.isAtLeast( + accounts.length, + 7, + "At least 7 accounts are required to test the contracts." + ); + [creator, governance, newGovernance, multisig, newMultisig, safeVault, userOne] = accounts; + + // Creating the instance of Test Token. + testToken = await TestToken.new("TestToken", "TST", 18, zero); + }); + + beforeEach("Creating New Development Fund Instance.", async () => { + await loadFixture(deploymentAndInitFixture); + }); + + it("Contract Creator should not be able to call the init() more than once.", async () => { + await expectRevert( + developmentFund.init({ from: creator }), + "The contract is not in the right state." + ); + }); + + it("Contract Creator should not be able to add Locked Token Owner.", async () => { + await expectRevert( + developmentFund.updateLockedTokenOwner(newGovernance), + "Only Locked Token Owner can call this." + ); + }); + + it("Contract Creator should not be able to approve a Locked Token Owner.", async () => { + await expectRevert( + developmentFund.approveLockedTokenOwner(), + "Only Unlocked Token Owner can call this." + ); + }); + + it("Contract Creator should not be able to update Unlocked Token Owner.", async () => { + await expectRevert( + developmentFund.updateUnlockedTokenOwner(newMultisig), + "Only Locked Token Owner can call this." + ); + }); + + it("Contract Creator should not be able to change the release schedule.", async () => { + await expectRevert( + developmentFund.changeTokenReleaseSchedule(zero, releaseDuration, releaseTokenAmount), + "Only Locked Token Owner can call this." + ); + }); + + it("Contract Creator should not be able to transfer all tokens to safeVault.", async () => { + await expectRevert( + developmentFund.transferTokensByUnlockedTokenOwner(), + "Only Unlocked Token Owner can call this." + ); + }); + + it("Contract Creator should not be able to withdraw tokens after schedule.", async () => { + await expectRevert( + developmentFund.withdrawTokensByUnlockedTokenOwner( + releaseTokenAmount[releaseTokenAmount.length - 1] + ), + "Only Unlocked Token Owner can call this." + ); + }); + + it("Contract Creator should not be able to transfer all tokens to a receiver.", async () => { + await expectRevert( + developmentFund.transferTokensByLockedTokenOwner(creator), + "Only Locked Token Owner can call this." + ); + }); }); diff --git a/tests/vesting/DevelopmentFund/event.test.js b/tests/vesting/DevelopmentFund/event.test.js index d9b5b4110..edfb7af2b 100644 --- a/tests/vesting/DevelopmentFund/event.test.js +++ b/tests/vesting/DevelopmentFund/event.test.js @@ -15,9 +15,9 @@ const DevelopmentFund = artifacts.require("DevelopmentFund"); const TestToken = artifacts.require("TestToken"); const { - time, // Convert different time units to seconds. Available helpers are: seconds, minutes, hours, days, weeks and years. - BN, // Big Number support. - expectEvent, // Assertions for emitted events. + time, // Convert different time units to seconds. Available helpers are: seconds, minutes, hours, days, weeks and years. + BN, // Big Number support. + expectEvent, // Assertions for emitted events. } = require("@openzeppelin/test-helpers"); const { assert } = require("chai"); @@ -39,7 +39,7 @@ let totalReleaseTokenAmount = 0; * @return {number} Random Value. */ function randomValue() { - return Math.floor(Math.random() * 1000); + return Math.floor(Math.random() * 1000); } /** @@ -48,14 +48,14 @@ function randomValue() { * @returns releaseTokenAmounts The release token amount array. */ function createReleaseTokenAmount() { - let balance = totalSupply; - let releaseTokenAmounts = []; - for (let times = 0; times < 60; times++) { - let newValue = randomValue() * 10; // Get's a number between 0 to 10000. - balance -= newValue; - releaseTokenAmounts.push(newValue); - } - return releaseTokenAmounts; + let balance = totalSupply; + let releaseTokenAmounts = []; + for (let times = 0; times < 60; times++) { + let newValue = randomValue() * 10; // Get's a number between 0 to 10000. + balance -= newValue; + releaseTokenAmounts.push(newValue); + } + return releaseTokenAmounts; } /** @@ -65,144 +65,175 @@ function createReleaseTokenAmount() { * @returns totalTokenAmounts The total number of tokens for the release. */ function calculateTotalTokenAmount(releaseTokenAmounts) { - return releaseTokenAmounts.reduce((a, b) => a + b, 0); + return releaseTokenAmounts.reduce((a, b) => a + b, 0); } contract("DevelopmentFund (Events)", (accounts) => { - let developmentFund, testToken; - let creator, governance, newGovernance, multisig, newMultisig, safeVault, userOne; - - async function deploymentAndInitFixture(_wallets, _provider) { - // Creating a new release schedule. - releaseDuration = []; - // This is run 60 times for mimicking 5 years (12 months * 5), though the interval is small. - for (let times = 0; times < 60; times++) { - releaseDuration.push(releaseInterval); - } - - // Creating a new release token schedule. - releaseTokenAmount = createReleaseTokenAmount(); - - // Creating the contract instance. - developmentFund = await DevelopmentFund.new( - testToken.address, - governance, - safeVault, - multisig, - zero, - releaseDuration, - releaseTokenAmount, - { from: creator } - ); - - // Calculating the total tokens in the release schedule. - totalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); - - // Minting new Tokens. - await testToken.mint(creator, totalSupply, { from: creator }); - - // Approving the development fund to do a transfer on behalf of governance. - await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { from: creator }); - - // Marking the contract as active. - await developmentFund.init({ from: creator }); - } - - before("Initiating Accounts & Creating Test Token Instance.", async () => { - // Checking if we have enough accounts to test. - assert.isAtLeast(accounts.length, 7, "At least 7 accounts are required to test the contracts."); - [creator, governance, newGovernance, multisig, newMultisig, safeVault, userOne] = accounts; - - // Creating the instance of Test Token. - testToken = await TestToken.new("TestToken", "TST", 18, zero); - }); - - beforeEach("Creating New Development Fund Instance.", async () => { - await loadFixture(deploymentAndInitFixture); - }); - - it("Adding a new locked owner should emit NewLockedOwnerAdded event.", async () => { - let txReceipt = await developmentFund.updateLockedTokenOwner(newGovernance, { from: governance }); - expectEvent(txReceipt, "NewLockedOwnerAdded", { - _initiator: governance, - _newLockedOwner: newGovernance, - }); - }); - - it("Approving a new Locked Token Owner should emit NewLockedOwnerApproved event.", async () => { - await developmentFund.updateLockedTokenOwner(newGovernance, { from: governance }); - let txReceipt = await developmentFund.approveLockedTokenOwner({ from: multisig }); - expectEvent(txReceipt, "NewLockedOwnerApproved", { - _initiator: multisig, - _oldLockedOwner: governance, - _newLockedOwner: newGovernance, - }); - }); - - it("Updating the Unlocked Token Owner should emit UnlockedOwnerUpdated event.", async () => { - let txReceipt = await developmentFund.updateUnlockedTokenOwner(newMultisig, { from: governance }); - expectEvent(txReceipt, "UnlockedOwnerUpdated", { - _initiator: governance, - _newUnlockedOwner: newMultisig, - }); - }); - - it("Depositing Token should emit the TokenDeposit event.", async () => { - let value = randomValue() + 1; - await testToken.mint(userOne, value); - await testToken.approve(developmentFund.address, value, { from: userOne }); - let txReceipt = await developmentFund.depositTokens(value, { from: userOne }); - expectEvent(txReceipt, "TokenDeposit", { - _initiator: userOne, - _amount: new BN(value), - }); - }); - - it("Updating the Release Schedule should emit TokenReleaseChanged event.", async () => { - await testToken.mint(governance, totalReleaseTokenAmount); - await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { from: governance }); - let txReceipt = await developmentFund.changeTokenReleaseSchedule(zero, releaseDuration, releaseTokenAmount, { from: governance }); - expectEvent(txReceipt, "TokenReleaseChanged", { - _initiator: governance, - _releaseCount: new BN(60), - }); - }); - - it("Transferring all tokens to safeVault by Unlocked Token Owner should emit LockedTokenTransferByUnlockedOwner event.", async () => { - let txReceipt = await developmentFund.transferTokensByUnlockedTokenOwner({ from: multisig }); - expectEvent(txReceipt, "DevelopmentFundExpired"); - expectEvent(txReceipt, "LockedTokenTransferByUnlockedOwner", { - _initiator: multisig, - _receiver: safeVault, - _amount: new BN(totalReleaseTokenAmount), - }); - }); - - it("Withdrawing tokens based on schedule should emit UnlockedTokenWithdrawalByUnlockedOwner event.", async () => { - await testToken.mint(governance, totalReleaseTokenAmount); - await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { from: governance }); - await developmentFund.changeTokenReleaseSchedule(zero, releaseDuration, releaseTokenAmount, { from: governance }); - - // Increasing the time to pass atleast one duration. - await time.increase(releaseDuration[releaseDuration.length - 1] + 1); - - let txReceipt = await developmentFund.withdrawTokensByUnlockedTokenOwner(releaseTokenAmount[releaseTokenAmount.length - 1], { - from: multisig, - }); - expectEvent(txReceipt, "UnlockedTokenWithdrawalByUnlockedOwner", { - _initiator: multisig, - _amount: new BN(releaseTokenAmount[releaseTokenAmount.length - 1]), - _releaseCount: new BN(1), - }); - }); - - it("Transferring all tokens to a receiver by Locked Token Owner should emit LockedTokenTransferByLockedOwner event.", async () => { - let txReceipt = await developmentFund.transferTokensByLockedTokenOwner(creator, { from: governance }); - expectEvent(txReceipt, "DevelopmentFundExpired"); - expectEvent(txReceipt, "LockedTokenTransferByLockedOwner", { - _initiator: governance, - _receiver: creator, - _amount: new BN(totalReleaseTokenAmount), - }); - }); + let developmentFund, testToken; + let creator, governance, newGovernance, multisig, newMultisig, safeVault, userOne; + + async function deploymentAndInitFixture(_wallets, _provider) { + // Creating a new release schedule. + releaseDuration = []; + // This is run 60 times for mimicking 5 years (12 months * 5), though the interval is small. + for (let times = 0; times < 60; times++) { + releaseDuration.push(releaseInterval); + } + + // Creating a new release token schedule. + releaseTokenAmount = createReleaseTokenAmount(); + + // Creating the contract instance. + developmentFund = await DevelopmentFund.new( + testToken.address, + governance, + safeVault, + multisig, + zero, + releaseDuration, + releaseTokenAmount, + { from: creator } + ); + + // Calculating the total tokens in the release schedule. + totalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); + + // Minting new Tokens. + await testToken.mint(creator, totalSupply, { from: creator }); + + // Approving the development fund to do a transfer on behalf of governance. + await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { + from: creator, + }); + + // Marking the contract as active. + await developmentFund.init({ from: creator }); + } + + before("Initiating Accounts & Creating Test Token Instance.", async () => { + // Checking if we have enough accounts to test. + assert.isAtLeast( + accounts.length, + 7, + "At least 7 accounts are required to test the contracts." + ); + [creator, governance, newGovernance, multisig, newMultisig, safeVault, userOne] = accounts; + + // Creating the instance of Test Token. + testToken = await TestToken.new("TestToken", "TST", 18, zero); + }); + + beforeEach("Creating New Development Fund Instance.", async () => { + await loadFixture(deploymentAndInitFixture); + }); + + it("Adding a new locked owner should emit NewLockedOwnerAdded event.", async () => { + let txReceipt = await developmentFund.updateLockedTokenOwner(newGovernance, { + from: governance, + }); + expectEvent(txReceipt, "NewLockedOwnerAdded", { + _initiator: governance, + _newLockedOwner: newGovernance, + }); + }); + + it("Approving a new Locked Token Owner should emit NewLockedOwnerApproved event.", async () => { + await developmentFund.updateLockedTokenOwner(newGovernance, { from: governance }); + let txReceipt = await developmentFund.approveLockedTokenOwner({ from: multisig }); + expectEvent(txReceipt, "NewLockedOwnerApproved", { + _initiator: multisig, + _oldLockedOwner: governance, + _newLockedOwner: newGovernance, + }); + }); + + it("Updating the Unlocked Token Owner should emit UnlockedOwnerUpdated event.", async () => { + let txReceipt = await developmentFund.updateUnlockedTokenOwner(newMultisig, { + from: governance, + }); + expectEvent(txReceipt, "UnlockedOwnerUpdated", { + _initiator: governance, + _newUnlockedOwner: newMultisig, + }); + }); + + it("Depositing Token should emit the TokenDeposit event.", async () => { + let value = randomValue() + 1; + await testToken.mint(userOne, value); + await testToken.approve(developmentFund.address, value, { from: userOne }); + let txReceipt = await developmentFund.depositTokens(value, { from: userOne }); + expectEvent(txReceipt, "TokenDeposit", { + _initiator: userOne, + _amount: new BN(value), + }); + }); + + it("Updating the Release Schedule should emit TokenReleaseChanged event.", async () => { + await testToken.mint(governance, totalReleaseTokenAmount); + await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { + from: governance, + }); + let txReceipt = await developmentFund.changeTokenReleaseSchedule( + zero, + releaseDuration, + releaseTokenAmount, + { from: governance } + ); + expectEvent(txReceipt, "TokenReleaseChanged", { + _initiator: governance, + _releaseCount: new BN(60), + }); + }); + + it("Transferring all tokens to safeVault by Unlocked Token Owner should emit LockedTokenTransferByUnlockedOwner event.", async () => { + let txReceipt = await developmentFund.transferTokensByUnlockedTokenOwner({ + from: multisig, + }); + expectEvent(txReceipt, "DevelopmentFundExpired"); + expectEvent(txReceipt, "LockedTokenTransferByUnlockedOwner", { + _initiator: multisig, + _receiver: safeVault, + _amount: new BN(totalReleaseTokenAmount), + }); + }); + + it("Withdrawing tokens based on schedule should emit UnlockedTokenWithdrawalByUnlockedOwner event.", async () => { + await testToken.mint(governance, totalReleaseTokenAmount); + await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { + from: governance, + }); + await developmentFund.changeTokenReleaseSchedule( + zero, + releaseDuration, + releaseTokenAmount, + { from: governance } + ); + + // Increasing the time to pass atleast one duration. + await time.increase(releaseDuration[releaseDuration.length - 1] + 1); + + let txReceipt = await developmentFund.withdrawTokensByUnlockedTokenOwner( + releaseTokenAmount[releaseTokenAmount.length - 1], + { + from: multisig, + } + ); + expectEvent(txReceipt, "UnlockedTokenWithdrawalByUnlockedOwner", { + _initiator: multisig, + _amount: new BN(releaseTokenAmount[releaseTokenAmount.length - 1]), + _releaseCount: new BN(1), + }); + }); + + it("Transferring all tokens to a receiver by Locked Token Owner should emit LockedTokenTransferByLockedOwner event.", async () => { + let txReceipt = await developmentFund.transferTokensByLockedTokenOwner(creator, { + from: governance, + }); + expectEvent(txReceipt, "DevelopmentFundExpired"); + expectEvent(txReceipt, "LockedTokenTransferByLockedOwner", { + _initiator: governance, + _receiver: creator, + _amount: new BN(totalReleaseTokenAmount), + }); + }); }); diff --git a/tests/vesting/DevelopmentFund/governance.test.js b/tests/vesting/DevelopmentFund/governance.test.js index 52c59e7dd..6b9693eac 100644 --- a/tests/vesting/DevelopmentFund/governance.test.js +++ b/tests/vesting/DevelopmentFund/governance.test.js @@ -15,8 +15,8 @@ const DevelopmentFund = artifacts.require("DevelopmentFund"); const TestToken = artifacts.require("TestToken"); const { - BN, // Big Number support. - expectRevert, // Assertions for transactions that should fail. + BN, // Big Number support. + expectRevert, // Assertions for transactions that should fail. } = require("@openzeppelin/test-helpers"); const { assert } = require("chai"); @@ -38,7 +38,7 @@ let totalReleaseTokenAmount = 0; * @return {number} Random Value. */ function randomValue() { - return Math.floor(Math.random() * 1000); + return Math.floor(Math.random() * 1000); } /** @@ -47,14 +47,14 @@ function randomValue() { * @returns releaseTokenAmounts The release token amount array. */ function createReleaseTokenAmount() { - let balance = totalSupply; - let releaseTokenAmounts = []; - for (let times = 0; times < 60; times++) { - let newValue = randomValue() * 10; // Get's a number between 0 to 10000. - balance -= newValue; - releaseTokenAmounts.push(newValue); - } - return releaseTokenAmounts; + let balance = totalSupply; + let releaseTokenAmounts = []; + for (let times = 0; times < 60; times++) { + let newValue = randomValue() * 10; // Get's a number between 0 to 10000. + balance -= newValue; + releaseTokenAmounts.push(newValue); + } + return releaseTokenAmounts; } /** @@ -64,127 +64,162 @@ function createReleaseTokenAmount() { * @returns totalTokenAmounts The total number of tokens for the release. */ function calculateTotalTokenAmount(releaseTokenAmounts) { - return releaseTokenAmounts.reduce((a, b) => a + b, 0); + return releaseTokenAmounts.reduce((a, b) => a + b, 0); } contract("DevelopmentFund (Governance Functions)", (accounts) => { - let developmentFund, testToken; - let creator, governance, newGovernance, multisig, newMultisig, safeVault, userOne; - - async function deploymentAndInitFixture(_wallets, _provider) { - // Creating a new release schedule. - releaseDuration = []; - // This is run 60 times for mimicking 5 years (12 months * 5), though the interval is small. - for (let times = 0; times < 60; times++) { - releaseDuration.push(releaseInterval); - } - - // Creating a new release token schedule. - releaseTokenAmount = createReleaseTokenAmount(); - - // Creating the contract instance. - developmentFund = await DevelopmentFund.new( - testToken.address, - governance, - safeVault, - multisig, - zero, - releaseDuration, - releaseTokenAmount, - { from: creator } - ); - - // Calculating the total tokens in the release schedule. - totalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); - - // Minting new Tokens. - await testToken.mint(creator, totalSupply, { from: creator }); - - // Approving the development fund to do a transfer on behalf of governance. - await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { from: creator }); - - // Marking the contract as active. - await developmentFund.init({ from: creator }); - - /// @dev Last tests require another deployment - developmentFundGov = await DevelopmentFund.new(testToken.address, governance, safeVault, multisig, zero, [0], [0], { - from: governance, - }); - await developmentFundGov.init({ from: governance }); - } - - before("Initiating Accounts & Creating Test Token Instance.", async () => { - // Checking if we have enough accounts to test. - assert.isAtLeast(accounts.length, 7, "Alteast 7 accounts are required to test the contracts."); - [creator, governance, newGovernance, multisig, newMultisig, safeVault, userOne] = accounts; - - // Creating the instance of Test Token. - testToken = await TestToken.new("TestToken", "TST", 18, zero); - }); - - beforeEach("Creating New Development Fund Instance.", async () => { - await loadFixture(deploymentAndInitFixture); - }); - - it("Locked Token Owner should not be able to call the init() more than once.", async () => { - await expectRevert(developmentFund.init({ from: governance }), "The contract is not in the right state."); - }); - - it("Instance Locked Token Owner should be governance.", async () => { - let lockedTokenOwner = await developmentFund.lockedTokenOwner(); - assert.strictEqual(lockedTokenOwner, governance, "The locked owner does not match."); - }); - - it("Should be able to add new Locked Token Owner.", async () => { - await developmentFund.updateLockedTokenOwner(newGovernance, { from: governance }); - }); - - it("Should not be able to approve the Locked Token Owner.", async () => { - await expectRevert(developmentFund.approveLockedTokenOwner({ from: governance }), "Only Unlocked Token Owner can call this."); - }); - - it("Should be able to update the Unlocked Token Owner.", async () => { - await developmentFund.updateUnlockedTokenOwner(newMultisig, { from: governance }); - }); - - it("Only Locked Token Owner should be able to update the Release Schedule.", async () => { - releaseTokenAmount = createReleaseTokenAmount(); - totalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); - await testToken.mint(governance, totalReleaseTokenAmount); - await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { from: governance }); - await developmentFund.changeTokenReleaseSchedule(zero, releaseDuration, releaseTokenAmount, { from: governance }); - }); - - /// @dev TODO: Misleading test found while optimizing has been splitted into 2. - /// Please review this test makes sense. - it("Locked Token Owner should approve the contract to send tokens for the Release Schedule.", async () => { - await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { from: governance }); - }); - - /// @dev TODO: Misleading test found while optimizing has been splitted into 2. - /// Please review this test makes sense. - it("Shouldn't be able to change the release schedule of a vesting contract.", async () => { - await expectRevert( - developmentFundGov.changeTokenReleaseSchedule(zero, releaseDuration, releaseTokenAmount, { from: governance }), - "invalid transfer" - ); - }); - - it("Locked Token Owner should not be able to transfer all tokens to safeVault.", async () => { - await expectRevert( - developmentFundGov.transferTokensByUnlockedTokenOwner({ from: governance }), - "Only Unlocked Token Owner can call this." - ); - }); - - it("Locked Token Owner should not be able to withdraw tokens after schedule duration passed.", async () => { - await expectRevert( - developmentFundGov.withdrawTokensByUnlockedTokenOwner(zero, { from: governance }), - "Only Unlocked Token Owner can call this." - ); - }); - - it("Locked Token Owner should be able to transfer all tokens to a receiver.", async () => { - await developmentFundGov.transferTokensByLockedTokenOwner(creator, { from: governance }); - }); + let developmentFund, testToken; + let creator, governance, newGovernance, multisig, newMultisig, safeVault, userOne; + + async function deploymentAndInitFixture(_wallets, _provider) { + // Creating a new release schedule. + releaseDuration = []; + // This is run 60 times for mimicking 5 years (12 months * 5), though the interval is small. + for (let times = 0; times < 60; times++) { + releaseDuration.push(releaseInterval); + } + + // Creating a new release token schedule. + releaseTokenAmount = createReleaseTokenAmount(); + + // Creating the contract instance. + developmentFund = await DevelopmentFund.new( + testToken.address, + governance, + safeVault, + multisig, + zero, + releaseDuration, + releaseTokenAmount, + { from: creator } + ); + + // Calculating the total tokens in the release schedule. + totalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); + + // Minting new Tokens. + await testToken.mint(creator, totalSupply, { from: creator }); + + // Approving the development fund to do a transfer on behalf of governance. + await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { + from: creator, + }); + + // Marking the contract as active. + await developmentFund.init({ from: creator }); + + /// @dev Last tests require another deployment + developmentFundGov = await DevelopmentFund.new( + testToken.address, + governance, + safeVault, + multisig, + zero, + [0], + [0], + { + from: governance, + } + ); + await developmentFundGov.init({ from: governance }); + } + + before("Initiating Accounts & Creating Test Token Instance.", async () => { + // Checking if we have enough accounts to test. + assert.isAtLeast( + accounts.length, + 7, + "Alteast 7 accounts are required to test the contracts." + ); + [creator, governance, newGovernance, multisig, newMultisig, safeVault, userOne] = accounts; + + // Creating the instance of Test Token. + testToken = await TestToken.new("TestToken", "TST", 18, zero); + }); + + beforeEach("Creating New Development Fund Instance.", async () => { + await loadFixture(deploymentAndInitFixture); + }); + + it("Locked Token Owner should not be able to call the init() more than once.", async () => { + await expectRevert( + developmentFund.init({ from: governance }), + "The contract is not in the right state." + ); + }); + + it("Instance Locked Token Owner should be governance.", async () => { + let lockedTokenOwner = await developmentFund.lockedTokenOwner(); + assert.strictEqual(lockedTokenOwner, governance, "The locked owner does not match."); + }); + + it("Should be able to add new Locked Token Owner.", async () => { + await developmentFund.updateLockedTokenOwner(newGovernance, { from: governance }); + }); + + it("Should not be able to approve the Locked Token Owner.", async () => { + await expectRevert( + developmentFund.approveLockedTokenOwner({ from: governance }), + "Only Unlocked Token Owner can call this." + ); + }); + + it("Should be able to update the Unlocked Token Owner.", async () => { + await developmentFund.updateUnlockedTokenOwner(newMultisig, { from: governance }); + }); + + it("Only Locked Token Owner should be able to update the Release Schedule.", async () => { + releaseTokenAmount = createReleaseTokenAmount(); + totalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); + await testToken.mint(governance, totalReleaseTokenAmount); + await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { + from: governance, + }); + await developmentFund.changeTokenReleaseSchedule( + zero, + releaseDuration, + releaseTokenAmount, + { from: governance } + ); + }); + + /// @dev TODO: Misleading test found while optimizing has been splitted into 2. + /// Please review this test makes sense. + it("Locked Token Owner should approve the contract to send tokens for the Release Schedule.", async () => { + await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { + from: governance, + }); + }); + + /// @dev TODO: Misleading test found while optimizing has been splitted into 2. + /// Please review this test makes sense. + it("Shouldn't be able to change the release schedule of a vesting contract.", async () => { + await expectRevert( + developmentFundGov.changeTokenReleaseSchedule( + zero, + releaseDuration, + releaseTokenAmount, + { from: governance } + ), + "invalid transfer" + ); + }); + + it("Locked Token Owner should not be able to transfer all tokens to safeVault.", async () => { + await expectRevert( + developmentFundGov.transferTokensByUnlockedTokenOwner({ from: governance }), + "Only Unlocked Token Owner can call this." + ); + }); + + it("Locked Token Owner should not be able to withdraw tokens after schedule duration passed.", async () => { + await expectRevert( + developmentFundGov.withdrawTokensByUnlockedTokenOwner(zero, { from: governance }), + "Only Unlocked Token Owner can call this." + ); + }); + + it("Locked Token Owner should be able to transfer all tokens to a receiver.", async () => { + await developmentFundGov.transferTokensByLockedTokenOwner(creator, { from: governance }); + }); }); diff --git a/tests/vesting/DevelopmentFund/multisig.test.js b/tests/vesting/DevelopmentFund/multisig.test.js index 94ee289c2..0f990811d 100644 --- a/tests/vesting/DevelopmentFund/multisig.test.js +++ b/tests/vesting/DevelopmentFund/multisig.test.js @@ -15,9 +15,9 @@ const DevelopmentFund = artifacts.require("DevelopmentFund"); const TestToken = artifacts.require("TestToken"); const { - time, // Convert different time units to seconds. Available helpers are: seconds, minutes, hours, days, weeks and years. - BN, // Big Number support. - expectRevert, // Assertions for transactions that should fail. + time, // Convert different time units to seconds. Available helpers are: seconds, minutes, hours, days, weeks and years. + BN, // Big Number support. + expectRevert, // Assertions for transactions that should fail. } = require("@openzeppelin/test-helpers"); const { assert } = require("chai"); @@ -39,7 +39,7 @@ let totalReleaseTokenAmount = 0; * @return {number} Random Value. */ function randomValue() { - return Math.floor(Math.random() * 1000); + return Math.floor(Math.random() * 1000); } /** @@ -48,14 +48,14 @@ function randomValue() { * @returns releaseTokenAmounts The release token amount array. */ function createReleaseTokenAmount() { - let balance = totalSupply; - let releaseTokenAmounts = []; - for (let times = 0; times < 60; times++) { - let newValue = randomValue() * 10; // Get's a number between 0 to 10000. - balance -= newValue; - releaseTokenAmounts.push(newValue); - } - return releaseTokenAmounts; + let balance = totalSupply; + let releaseTokenAmounts = []; + for (let times = 0; times < 60; times++) { + let newValue = randomValue() * 10; // Get's a number between 0 to 10000. + balance -= newValue; + releaseTokenAmounts.push(newValue); + } + return releaseTokenAmounts; } /** @@ -65,174 +65,230 @@ function createReleaseTokenAmount() { * @returns totalTokenAmounts The total number of tokens for the release. */ function calculateTotalTokenAmount(releaseTokenAmounts) { - return releaseTokenAmounts.reduce((a, b) => a + b, 0); + return releaseTokenAmounts.reduce((a, b) => a + b, 0); } contract("DevelopmentFund (Multisig Functions)", (accounts) => { - let developmentFund, testToken; - let creator, governance, newGovernance, multisig, newMultisig, safeVault, userOne; - - async function deploymentAndInitFixture(_wallets, _provider) { - // Creating a new release schedule. - releaseDuration = []; - // This is run 60 times for mimicking 5 years (12 months * 5), though the interval is small. - for (let times = 0; times < 60; times++) { - releaseDuration.push(releaseInterval); - } - - // Creating a new release token schedule. - releaseTokenAmount = createReleaseTokenAmount(); - - // Creating the contract instance. - developmentFund = await DevelopmentFund.new( - testToken.address, - governance, - safeVault, - multisig, - zero, - releaseDuration, - releaseTokenAmount, - { from: creator } - ); - - // Calculating the total tokens in the release schedule. - totalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); - - // Minting new Tokens. - await testToken.mint(creator, totalSupply, { from: creator }); - - // Approving the development fund to do a transfer on behalf of governance. - await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { from: creator }); - - // Marking the contract as active. - await developmentFund.init({ from: creator }); - - /// @dev Moved from tests - await testToken.mint(governance, totalReleaseTokenAmount); - await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { from: governance }); - } - - before("Initiating Accounts & Creating Test Token Instance.", async () => { - // Checking if we have enough accounts to test. - assert.isAtLeast(accounts.length, 7, "At least 7 accounts are required to test the contracts."); - [creator, governance, newGovernance, multisig, newMultisig, safeVault, userOne] = accounts; - - // Creating the instance of Test Token. - testToken = await TestToken.new("TestToken", "TST", 18, zero); - }); - - beforeEach("Creating New Development Fund Instance.", async () => { - await loadFixture(deploymentAndInitFixture); - }); - - it("Unlocked Token Owner should not be able to call the init() more than once.", async () => { - await expectRevert(developmentFund.init({ from: multisig }), "The contract is not in the right state."); - }); - - it("Unlocked Token Owner should not be able to add Locked Token Owner.", async () => { - await expectRevert( - developmentFund.updateLockedTokenOwner(newGovernance, { from: multisig }), - "Only Locked Token Owner can call this." - ); - }); - - it("Unlocked Token Owner should be able to approve a Locked Token Owner.", async () => { - await developmentFund.updateLockedTokenOwner(newGovernance, { from: governance }); - await developmentFund.approveLockedTokenOwner({ from: multisig }); - }); - - it("Unlocked Token Owner should not be able to approve a Locked Token Owner, if not added by governance.", async () => { - await expectRevert(developmentFund.approveLockedTokenOwner({ from: multisig }), "No new locked owner added."); - }); - - it("Unlocked Token Owner should not be able to update Unlocked Token Owner.", async () => { - await expectRevert( - developmentFund.updateUnlockedTokenOwner(newMultisig, { from: multisig }), - "Only Locked Token Owner can call this." - ); - }); - - it("Unlocked Token Owner should not be able to change the release schedule.", async () => { - await expectRevert( - developmentFund.changeTokenReleaseSchedule(zero, releaseDuration, releaseTokenAmount, { from: multisig }), - "Only Locked Token Owner can call this." - ); - }); - - it("Unlocked Token Owner should be able to transfer all tokens to safeVault.", async () => { - let value = randomValue() + 1; - await testToken.mint(userOne, value); - await testToken.approve(developmentFund.address, value, { from: userOne }); - await developmentFund.depositTokens(value, { from: userOne }); - await developmentFund.transferTokensByUnlockedTokenOwner({ from: multisig }); - }); - - it("Unlocked Token Owner should be able to withdraw tokens after schedule.", async () => { - await developmentFund.changeTokenReleaseSchedule(zero, releaseDuration, releaseTokenAmount, { from: governance }); - - // Increasing the time to pass atleast one duration. - await time.increase(releaseDuration[releaseDuration.length - 1] + 1); - - await developmentFund.withdrawTokensByUnlockedTokenOwner(releaseTokenAmount[releaseTokenAmount.length - 1], { from: multisig }); - }); - - it("Unlocked Token Owner should be able to withdraw part of the tokens after schedule.", async () => { - await developmentFund.changeTokenReleaseSchedule(zero, releaseDuration, releaseTokenAmount, { from: governance }); - - // Increasing the time to pass atleast one duration. - await time.increase(releaseDuration[releaseDuration.length - 1] + 1); - - await developmentFund.withdrawTokensByUnlockedTokenOwner(Math.floor(releaseTokenAmount[releaseTokenAmount.length - 1] / 2), { - from: multisig, - }); - }); - - it("Unlocked Token Owner should be able to withdraw tokens after multiple schedule is passed.", async () => { - await developmentFund.changeTokenReleaseSchedule(zero, releaseDuration, releaseTokenAmount, { from: governance }); - - // Increasing the time to pass atleast one duration. - await time.increase(releaseDuration[releaseDuration.length - 1] + releaseDuration[releaseDuration.length - 2] + 1); - - await developmentFund.withdrawTokensByUnlockedTokenOwner( - releaseTokenAmount[releaseTokenAmount.length - 1] + Math.floor(releaseTokenAmount[releaseTokenAmount.length - 2] / 2), - { from: multisig } - ); - }); - - it("Unlocked Token Owner should not be able to withdraw tokens higher than the schedule.", async () => { - await developmentFund.changeTokenReleaseSchedule(zero, releaseDuration, releaseTokenAmount, { from: governance }); - - // Increasing the time to pass atleast one duration. - await time.increase(releaseDuration[releaseDuration.length - 1] + 1); - - // Checking token balance of multisig contract before tx - let beforeTokenBalance = await testToken.balanceOf(multisig); - - await developmentFund.withdrawTokensByUnlockedTokenOwner( - releaseTokenAmount[releaseTokenAmount.length - 1] + Math.floor(releaseTokenAmount[releaseTokenAmount.length - 2] / 2), - { from: multisig } - ); - - // Checking token balance of multisig contract after tx - let afterTokenBalance = await testToken.balanceOf(multisig); - - assert.strictEqual( - beforeTokenBalance.toNumber(), - afterTokenBalance.toNumber() - releaseTokenAmount[releaseTokenAmount.length - 1], - "Token amount not correct." - ); - }); - - it("Unlocked Token Owner should not be able to withdraw tokens without any duration is complete.", async () => { - await developmentFund.changeTokenReleaseSchedule(zero, releaseDuration, releaseTokenAmount, { from: governance }); - let value = randomValue() + 1; - await expectRevert(developmentFund.withdrawTokensByUnlockedTokenOwner(value, { from: multisig }), "No release schedule reached."); - }); - - it("Unlocked Token Owner should not be able to transfer all tokens to a receiver.", async () => { - await expectRevert( - developmentFund.transferTokensByLockedTokenOwner(creator, { from: multisig }), - "Only Locked Token Owner can call this." - ); - }); + let developmentFund, testToken; + let creator, governance, newGovernance, multisig, newMultisig, safeVault, userOne; + + async function deploymentAndInitFixture(_wallets, _provider) { + // Creating a new release schedule. + releaseDuration = []; + // This is run 60 times for mimicking 5 years (12 months * 5), though the interval is small. + for (let times = 0; times < 60; times++) { + releaseDuration.push(releaseInterval); + } + + // Creating a new release token schedule. + releaseTokenAmount = createReleaseTokenAmount(); + + // Creating the contract instance. + developmentFund = await DevelopmentFund.new( + testToken.address, + governance, + safeVault, + multisig, + zero, + releaseDuration, + releaseTokenAmount, + { from: creator } + ); + + // Calculating the total tokens in the release schedule. + totalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); + + // Minting new Tokens. + await testToken.mint(creator, totalSupply, { from: creator }); + + // Approving the development fund to do a transfer on behalf of governance. + await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { + from: creator, + }); + + // Marking the contract as active. + await developmentFund.init({ from: creator }); + + /// @dev Moved from tests + await testToken.mint(governance, totalReleaseTokenAmount); + await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { + from: governance, + }); + } + + before("Initiating Accounts & Creating Test Token Instance.", async () => { + // Checking if we have enough accounts to test. + assert.isAtLeast( + accounts.length, + 7, + "At least 7 accounts are required to test the contracts." + ); + [creator, governance, newGovernance, multisig, newMultisig, safeVault, userOne] = accounts; + + // Creating the instance of Test Token. + testToken = await TestToken.new("TestToken", "TST", 18, zero); + }); + + beforeEach("Creating New Development Fund Instance.", async () => { + await loadFixture(deploymentAndInitFixture); + }); + + it("Unlocked Token Owner should not be able to call the init() more than once.", async () => { + await expectRevert( + developmentFund.init({ from: multisig }), + "The contract is not in the right state." + ); + }); + + it("Unlocked Token Owner should not be able to add Locked Token Owner.", async () => { + await expectRevert( + developmentFund.updateLockedTokenOwner(newGovernance, { from: multisig }), + "Only Locked Token Owner can call this." + ); + }); + + it("Unlocked Token Owner should be able to approve a Locked Token Owner.", async () => { + await developmentFund.updateLockedTokenOwner(newGovernance, { from: governance }); + await developmentFund.approveLockedTokenOwner({ from: multisig }); + }); + + it("Unlocked Token Owner should not be able to approve a Locked Token Owner, if not added by governance.", async () => { + await expectRevert( + developmentFund.approveLockedTokenOwner({ from: multisig }), + "No new locked owner added." + ); + }); + + it("Unlocked Token Owner should not be able to update Unlocked Token Owner.", async () => { + await expectRevert( + developmentFund.updateUnlockedTokenOwner(newMultisig, { from: multisig }), + "Only Locked Token Owner can call this." + ); + }); + + it("Unlocked Token Owner should not be able to change the release schedule.", async () => { + await expectRevert( + developmentFund.changeTokenReleaseSchedule(zero, releaseDuration, releaseTokenAmount, { + from: multisig, + }), + "Only Locked Token Owner can call this." + ); + }); + + it("Unlocked Token Owner should be able to transfer all tokens to safeVault.", async () => { + let value = randomValue() + 1; + await testToken.mint(userOne, value); + await testToken.approve(developmentFund.address, value, { from: userOne }); + await developmentFund.depositTokens(value, { from: userOne }); + await developmentFund.transferTokensByUnlockedTokenOwner({ from: multisig }); + }); + + it("Unlocked Token Owner should be able to withdraw tokens after schedule.", async () => { + await developmentFund.changeTokenReleaseSchedule( + zero, + releaseDuration, + releaseTokenAmount, + { from: governance } + ); + + // Increasing the time to pass atleast one duration. + await time.increase(releaseDuration[releaseDuration.length - 1] + 1); + + await developmentFund.withdrawTokensByUnlockedTokenOwner( + releaseTokenAmount[releaseTokenAmount.length - 1], + { from: multisig } + ); + }); + + it("Unlocked Token Owner should be able to withdraw part of the tokens after schedule.", async () => { + await developmentFund.changeTokenReleaseSchedule( + zero, + releaseDuration, + releaseTokenAmount, + { from: governance } + ); + + // Increasing the time to pass atleast one duration. + await time.increase(releaseDuration[releaseDuration.length - 1] + 1); + + await developmentFund.withdrawTokensByUnlockedTokenOwner( + Math.floor(releaseTokenAmount[releaseTokenAmount.length - 1] / 2), + { + from: multisig, + } + ); + }); + + it("Unlocked Token Owner should be able to withdraw tokens after multiple schedule is passed.", async () => { + await developmentFund.changeTokenReleaseSchedule( + zero, + releaseDuration, + releaseTokenAmount, + { from: governance } + ); + + // Increasing the time to pass atleast one duration. + await time.increase( + releaseDuration[releaseDuration.length - 1] + + releaseDuration[releaseDuration.length - 2] + + 1 + ); + + await developmentFund.withdrawTokensByUnlockedTokenOwner( + releaseTokenAmount[releaseTokenAmount.length - 1] + + Math.floor(releaseTokenAmount[releaseTokenAmount.length - 2] / 2), + { from: multisig } + ); + }); + + it("Unlocked Token Owner should not be able to withdraw tokens higher than the schedule.", async () => { + await developmentFund.changeTokenReleaseSchedule( + zero, + releaseDuration, + releaseTokenAmount, + { from: governance } + ); + + // Increasing the time to pass atleast one duration. + await time.increase(releaseDuration[releaseDuration.length - 1] + 1); + + // Checking token balance of multisig contract before tx + let beforeTokenBalance = await testToken.balanceOf(multisig); + + await developmentFund.withdrawTokensByUnlockedTokenOwner( + releaseTokenAmount[releaseTokenAmount.length - 1] + + Math.floor(releaseTokenAmount[releaseTokenAmount.length - 2] / 2), + { from: multisig } + ); + + // Checking token balance of multisig contract after tx + let afterTokenBalance = await testToken.balanceOf(multisig); + + assert.strictEqual( + beforeTokenBalance.toNumber(), + afterTokenBalance.toNumber() - releaseTokenAmount[releaseTokenAmount.length - 1], + "Token amount not correct." + ); + }); + + it("Unlocked Token Owner should not be able to withdraw tokens without any duration is complete.", async () => { + await developmentFund.changeTokenReleaseSchedule( + zero, + releaseDuration, + releaseTokenAmount, + { from: governance } + ); + let value = randomValue() + 1; + await expectRevert( + developmentFund.withdrawTokensByUnlockedTokenOwner(value, { from: multisig }), + "No release schedule reached." + ); + }); + + it("Unlocked Token Owner should not be able to transfer all tokens to a receiver.", async () => { + await expectRevert( + developmentFund.transferTokensByLockedTokenOwner(creator, { from: multisig }), + "Only Locked Token Owner can call this." + ); + }); }); diff --git a/tests/vesting/DevelopmentFund/state.test.js b/tests/vesting/DevelopmentFund/state.test.js index 1f57b4bf4..18925ae6f 100644 --- a/tests/vesting/DevelopmentFund/state.test.js +++ b/tests/vesting/DevelopmentFund/state.test.js @@ -19,10 +19,10 @@ const DevelopmentFund = artifacts.require("DevelopmentFund"); const TestToken = artifacts.require("TestToken"); const { - time, // Convert different time units to seconds. Available helpers are: seconds, minutes, hours, days, weeks and years. - BN, // Big Number support. - constants, // Common constants, like the zero address and largest integers. - expectRevert, // Assertions for transactions that should fail. + time, // Convert different time units to seconds. Available helpers are: seconds, minutes, hours, days, weeks and years. + BN, // Big Number support. + constants, // Common constants, like the zero address and largest integers. + expectRevert, // Assertions for transactions that should fail. } = require("@openzeppelin/test-helpers"); const { assert } = require("chai"); @@ -47,7 +47,7 @@ let statusExpired = 2; * @return {number} Random Value. */ function randomValue() { - return Math.floor(Math.random() * 1000); + return Math.floor(Math.random() * 1000); } /** @@ -57,7 +57,7 @@ function randomValue() { * @return numArray The array with numbers. */ function convertBNArrayToNumArray(bnArray) { - return bnArray.map((a) => a.toNumber()); + return bnArray.map((a) => a.toNumber()); } /** @@ -74,60 +74,66 @@ function convertBNArrayToNumArray(bnArray) { * @param status The current contract status. */ async function checkStatus( - contractInstance, - checkArray, - lockedTokenOwner, - unlockedTokenOwner, - newLockedTokenOwner, - lastReleaseTime, - releaseDuration, - releaseTokenAmount, - status + contractInstance, + checkArray, + lockedTokenOwner, + unlockedTokenOwner, + newLockedTokenOwner, + lastReleaseTime, + releaseDuration, + releaseTokenAmount, + status ) { - if (checkArray[0] == 1) { - let cValue = await contractInstance.lockedTokenOwner(); - assert.strictEqual(lockedTokenOwner, cValue, "The locked owner does not match."); - } - if (checkArray[1] == 1) { - let cValue = await contractInstance.unlockedTokenOwner(); - assert.strictEqual(unlockedTokenOwner, cValue, "The unlocked owner does not match."); - } - if (checkArray[2] == 1) { - let cValue = await contractInstance.newLockedTokenOwner(); - assert.strictEqual(newLockedTokenOwner, cValue, "The new locked owner does not match."); - } - if (checkArray[3] == 1) { - let cValue = await contractInstance.lastReleaseTime(); - assert.equal(lastReleaseTime, cValue.toNumber(), "The last release time does not match."); - } - if (checkArray[4] == 1) { - let cValue = []; - await contractInstance.getReleaseDuration().then((data) => { - cValue = data; - }); - cValue = convertBNArrayToNumArray(cValue); - assert(cValue.length == releaseDuration.length, "The release duration length does not match."); - assert( - cValue.every((value, index) => value === releaseDuration[index]), - "The release duration does not match." - ); - } - if (checkArray[5] == 1) { - let cValue = []; - await contractInstance.getReleaseTokenAmount().then((data) => { - cValue = data; - }); - cValue = convertBNArrayToNumArray(cValue); - assert(cValue.length == releaseTokenAmount.length, "The release token length does not match."); - assert( - cValue.every((value, index) => value === releaseTokenAmount[index]), - "The release token amount does not match." - ); - } - if (checkArray[6] == 1) { - let cValue = await contractInstance.status(); - assert.equal(status, cValue.toNumber(), "The contract status does not match."); - } + if (checkArray[0] == 1) { + let cValue = await contractInstance.lockedTokenOwner(); + assert.strictEqual(lockedTokenOwner, cValue, "The locked owner does not match."); + } + if (checkArray[1] == 1) { + let cValue = await contractInstance.unlockedTokenOwner(); + assert.strictEqual(unlockedTokenOwner, cValue, "The unlocked owner does not match."); + } + if (checkArray[2] == 1) { + let cValue = await contractInstance.newLockedTokenOwner(); + assert.strictEqual(newLockedTokenOwner, cValue, "The new locked owner does not match."); + } + if (checkArray[3] == 1) { + let cValue = await contractInstance.lastReleaseTime(); + assert.equal(lastReleaseTime, cValue.toNumber(), "The last release time does not match."); + } + if (checkArray[4] == 1) { + let cValue = []; + await contractInstance.getReleaseDuration().then((data) => { + cValue = data; + }); + cValue = convertBNArrayToNumArray(cValue); + assert( + cValue.length == releaseDuration.length, + "The release duration length does not match." + ); + assert( + cValue.every((value, index) => value === releaseDuration[index]), + "The release duration does not match." + ); + } + if (checkArray[5] == 1) { + let cValue = []; + await contractInstance.getReleaseTokenAmount().then((data) => { + cValue = data; + }); + cValue = convertBNArrayToNumArray(cValue); + assert( + cValue.length == releaseTokenAmount.length, + "The release token length does not match." + ); + assert( + cValue.every((value, index) => value === releaseTokenAmount[index]), + "The release token amount does not match." + ); + } + if (checkArray[6] == 1) { + let cValue = await contractInstance.status(); + assert.equal(status, cValue.toNumber(), "The contract status does not match."); + } } /** @@ -136,14 +142,14 @@ async function checkStatus( * @returns releaseTokenAmounts The release token amount array. */ function createReleaseTokenAmount() { - let balance = totalSupply; - let releaseTokenAmounts = []; - for (let times = 0; times < 60; times++) { - let newValue = randomValue() * 10; // Get's a number between 0 to 10000. - balance -= newValue; - releaseTokenAmounts.push(newValue); - } - return releaseTokenAmounts; + let balance = totalSupply; + let releaseTokenAmounts = []; + for (let times = 0; times < 60; times++) { + let newValue = randomValue() * 10; // Get's a number between 0 to 10000. + balance -= newValue; + releaseTokenAmounts.push(newValue); + } + return releaseTokenAmounts; } /** @@ -153,459 +159,697 @@ function createReleaseTokenAmount() { * @returns totalTokenAmounts The total number of tokens for the release. */ function calculateTotalTokenAmount(releaseTokenAmounts) { - return releaseTokenAmounts.reduce((a, b) => a + b, 0); + return releaseTokenAmounts.reduce((a, b) => a + b, 0); } contract("DevelopmentFund (State)", (accounts) => { - let developmentFund, testToken; - let creator, governance, newGovernance, multisig, newMultisig, safeVault, userOne; - - async function deploymentAndInitFixture(_wallets, _provider) { - // Creating a new release schedule. - releaseDuration = []; - // This is run 60 times for mimicking 5 years (12 months * 5), though the interval is small. - for (let times = 0; times < 60; times++) { - releaseDuration.push(releaseInterval); - } - - // Creating a new release token schedule. - releaseTokenAmount = createReleaseTokenAmount(); - - // Creating the contract instance. - developmentFund = await DevelopmentFund.new( - testToken.address, - governance, - safeVault, - multisig, - zero, - releaseDuration, - releaseTokenAmount, - { from: creator } - ); - - // Calculating the total tokens in the release schedule. - totalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); - - // Minting new Tokens. - await testToken.mint(creator, totalSupply, { from: creator }); - - // Approving the development fund to do a transfer on behalf of governance. - await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { from: creator }); - - // Marking the contract as active. - await developmentFund.init({ from: creator }); - } - - before("Initiating Accounts & Creating Test Token Instance.", async () => { - // Checking if we have enough accounts to test. - assert.isAtLeast(accounts.length, 7, "At least 7 accounts are required to test the contracts."); - [creator, governance, newGovernance, multisig, newMultisig, safeVault, userOne] = accounts; - - // Creating the instance of Test Token. - testToken = await TestToken.new("TestToken", "TST", 18, zero); - }); - - beforeEach("Creating New Development Fund Instance.", async () => { - // await loadFixture(deploymentAndInitFixture); - }); - - it("Successfully created an instance without any error.", async () => { - /// @dev Explicit call to use fixture snapshot as initial status - await loadFixture(deploymentAndInitFixture); - - /// @dev The re-deployment is only valid on this local scope (variable developmentFund) - /// fixture removes it for following tests - let developmentFund = await DevelopmentFund.new( - testToken.address, - governance, - safeVault, - multisig, - zero, - releaseDuration, - releaseTokenAmount, - { from: creator } - ); - - let currentTime = await time.latest(); - - // Checking if the contract is in deployed state. - await checkStatus(developmentFund, [0, 0, 0, 0, 0, 0, 1], zero, zero, zero, zero, zero, zero, statusDeployed); - - // Minting new Tokens. - await testToken.mint(creator, totalSupply, { from: creator }); - - // Approving the development fund to do a transfer on behalf of governance. - await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { from: creator }); - - // Marking the contract as active. - await developmentFund.init({ from: creator }); - - await checkStatus( - developmentFund, - [1, 1, 0, 1, 1, 1, 1], - governance, - multisig, - zero, - currentTime, - releaseDuration, - releaseTokenAmount, - statusActive - ); - }); - - it("Adding new Locked Token Owner should update the new Locked Token Owner storage.", async () => { - /// @dev Explicit call to use fixture snapshot as initial status - await loadFixture(deploymentAndInitFixture); - - await developmentFund.updateLockedTokenOwner(newGovernance, { from: governance }); - await checkStatus(developmentFund, [0, 0, 1, 0, 0, 0, 0], zero, zero, newGovernance, zero, zero, zero, zero); - }); - - it("Approving new Locked Token Owner should update the Locked Token Owner.", async () => { - /// @dev Explicit call to use fixture snapshot as initial status - await loadFixture(deploymentAndInitFixture); - - await developmentFund.updateLockedTokenOwner(newGovernance, { from: governance }); - await developmentFund.approveLockedTokenOwner({ from: multisig }); - await checkStatus(developmentFund, [1, 0, 0, 0, 0, 0, 0], newGovernance, zero, zero, zero, zero, zero, zero); - }); - - it("After approval of new Locked Token Owner, newLockedTokenOwner should be zero address.", async () => { - /// @dev Explicit call to use fixture snapshot as initial status - await loadFixture(deploymentAndInitFixture); - - await developmentFund.updateLockedTokenOwner(newGovernance, { from: governance }); - await developmentFund.approveLockedTokenOwner({ from: multisig }); - await checkStatus(developmentFund, [0, 0, 1, 0, 0, 0, 0], zero, zero, constants.ZERO_ADDRESS, zero, zero, zero, zero); - }); - - it("Adding new Deposit should update the remaining token and contract token balance.", async () => { - /// @dev Explicit call to use fixture snapshot as initial status - await loadFixture(deploymentAndInitFixture); - - let value = randomValue() + 1; - await testToken.mint(userOne, value); - await testToken.approve(developmentFund.address, value, { from: userOne }); - await developmentFund.depositTokens(value, { from: userOne }); - let tokenBalance = await testToken.balanceOf(developmentFund.address); - assert.equal( - tokenBalance.toNumber(), - value + totalReleaseTokenAmount, - "Token Balance in contract does not match the deposited amount." - ); - }); - - it("The contract should be approved to make token transfer for deposit.", async () => { - /// @dev Explicit call to use fixture snapshot as initial status - await loadFixture(deploymentAndInitFixture); - - let value = randomValue() + 1; - await expectRevert(developmentFund.depositTokens(value, { from: userOne }), "invalid transfer"); - }); - - it("Zero Tokens could not be deposited.", async () => { - /// @dev Explicit call to use fixture snapshot as initial status - await loadFixture(deploymentAndInitFixture); - - await expectRevert(developmentFund.depositTokens(0, { from: userOne }), "Amount needs to be bigger than zero."); - }); - - it("Updating the release schedule should update the lastReleaseTime, releaseDuration and releaseTokenAmount.", async () => { - /// @dev Explicit call to use fixture snapshot as initial status - await loadFixture(deploymentAndInitFixture); - - let newReleaseTime = randomValue() + 1; - releaseTokenAmount = createReleaseTokenAmount(); - totalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); - await testToken.mint(governance, totalReleaseTokenAmount); - await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { from: governance }); - await developmentFund.changeTokenReleaseSchedule(newReleaseTime, releaseDuration, releaseTokenAmount, { from: governance }); - await checkStatus( - developmentFund, - [0, 0, 0, 1, 1, 1, 0], - zero, - zero, - zero, - newReleaseTime, - releaseDuration, - releaseTokenAmount, - zero - ); - let tokenBalance = await testToken.balanceOf(developmentFund.address); - assert.equal(tokenBalance.toNumber(), totalReleaseTokenAmount, "Token Balance in contract does not match the correct amount."); - }); - - it("Updating the release schedule twice should update the lastReleaseTime, releaseDuration and releaseTokenAmount accordingly.", async () => { - /// @dev Explicit call to use fixture snapshot as initial status - await loadFixture(deploymentAndInitFixture); - - // First Time - let newReleaseTime = randomValue() + 1; - releaseTokenAmount = createReleaseTokenAmount(); - totalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); - await testToken.mint(governance, totalReleaseTokenAmount); - await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { from: governance }); - await developmentFund.changeTokenReleaseSchedule(newReleaseTime, releaseDuration, releaseTokenAmount, { from: governance }); - await checkStatus( - developmentFund, - [0, 0, 0, 1, 1, 1, 0], - zero, - zero, - zero, - newReleaseTime, - releaseDuration, - releaseTokenAmount, - zero - ); - let tokenBalance = await testToken.balanceOf(developmentFund.address); - assert.equal(tokenBalance.toNumber(), totalReleaseTokenAmount, "Token Balance in contract does not match the correct amount."); - - // Second Time - newReleaseTime = randomValue() + 1; - releaseTokenAmount = createReleaseTokenAmount(); - totalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); - await testToken.mint(governance, totalReleaseTokenAmount); - await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { from: governance }); - await developmentFund.changeTokenReleaseSchedule(newReleaseTime, releaseDuration, releaseTokenAmount, { from: governance }); - await checkStatus( - developmentFund, - [0, 0, 0, 1, 1, 1, 0], - zero, - zero, - zero, - newReleaseTime, - releaseDuration, - releaseTokenAmount, - zero - ); - tokenBalance = await testToken.balanceOf(developmentFund.address); - assert.equal(tokenBalance.toNumber(), totalReleaseTokenAmount, "Token Balance in contract does not match the correct amount."); - }); - - it("While updating release schedule, extra tokens should be sent back.", async () => { - let previousReleaseTokenAmount = totalReleaseTokenAmount; - let newReleaseTime = randomValue() + 1; - releaseTokenAmount = createReleaseTokenAmount(); - totalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); - - await testToken.mint(governance, totalReleaseTokenAmount); - await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { from: governance }); - await developmentFund.depositTokens(totalReleaseTokenAmount, { from: governance }); - - // Checking token balance of governance contract before tx - let beforeTokenBalance = await testToken.balanceOf(governance); - - await developmentFund.changeTokenReleaseSchedule(newReleaseTime, releaseDuration, releaseTokenAmount, { from: governance }); - - // Checking token balance of governance contract after tx - let afterTokenBalance = await testToken.balanceOf(governance); - - assert.strictEqual( - afterTokenBalance.toNumber(), - beforeTokenBalance.toNumber() + previousReleaseTokenAmount, - "Extra tokens not sent back." - ); - await checkStatus( - developmentFund, - [0, 0, 0, 1, 1, 1, 0], - zero, - zero, - zero, - newReleaseTime, - releaseDuration, - releaseTokenAmount, - zero - ); - let tokenBalance = await testToken.balanceOf(developmentFund.address); - assert.equal(tokenBalance.toNumber(), totalReleaseTokenAmount, "Token Balance in contract does not match the correct amount."); - }); - - it("While updating release schedule, deficient tokens should be sent to contract.", async () => { - let newReleaseTime = randomValue() + 1; - releaseTokenAmount = createReleaseTokenAmount(); - let newTotalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); - if (newTotalReleaseTokenAmount <= totalReleaseTokenAmount) { - let difference = totalReleaseTokenAmount - newTotalReleaseTokenAmount + randomValue(); - releaseTokenAmount[releaseTokenAmount.length - 1] += difference; - } - newTotalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); - let deficient = newTotalReleaseTokenAmount - totalReleaseTokenAmount; - - await testToken.mint(governance, deficient); - await testToken.approve(developmentFund.address, deficient, { from: governance }); - - // Checking token balance of governance contract before tx - let beforeTokenBalance = await testToken.balanceOf(governance); - - await developmentFund.changeTokenReleaseSchedule(newReleaseTime, releaseDuration, releaseTokenAmount, { from: governance }); - - // Checking token balance of governance contract after tx - let afterTokenBalance = await testToken.balanceOf(governance); - - assert.strictEqual(afterTokenBalance.toNumber(), beforeTokenBalance.toNumber() - deficient, "Extra tokens not sent to contract."); - await checkStatus( - developmentFund, - [0, 0, 0, 1, 1, 1, 0], - zero, - zero, - zero, - newReleaseTime, - releaseDuration, - releaseTokenAmount, - zero - ); - let tokenBalance = await testToken.balanceOf(developmentFund.address); - assert.equal(tokenBalance.toNumber(), newTotalReleaseTokenAmount, "Token Balance in contract does not match the correct amount."); - }); - - it("Unequal array for duration and tokens should not be accepted for token release schedule.", async () => { - let newReleaseTime = randomValue() + 1; - releaseTokenAmount = createReleaseTokenAmount(); - releaseTokenAmount.pop(); - await expectRevert( - developmentFund.changeTokenReleaseSchedule(newReleaseTime, releaseDuration, releaseTokenAmount, { from: governance }), - "Release Schedule does not match." - ); - }); - - it("Transferring all tokens to a safeVault by Unlocked Token Owner should update the remainingToken.", async () => { - let value = randomValue() + 1; - await testToken.mint(userOne, value); - await testToken.approve(developmentFund.address, value, { from: userOne }); - await developmentFund.depositTokens(value, { from: userOne }); - await developmentFund.transferTokensByUnlockedTokenOwner({ from: multisig }); - let tokenBalance = await testToken.balanceOf(developmentFund.address); - assert.equal(tokenBalance.toNumber(), 0, "Token Balance in contract does not match the correct amount."); - await checkStatus(developmentFund, [0, 0, 0, 0, 0, 0, 1], zero, zero, zero, zero, zero, zero, statusExpired); - }); - - it("After withdrawing all tokens after a particular schedule, the release array size should decrease.", async () => { - /// @dev This test requires a hard reset of init fixture - await deploymentAndInitFixture(); - - releaseTokenAmount = createReleaseTokenAmount(); - totalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); - await testToken.mint(governance, totalReleaseTokenAmount); - await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { from: governance }); - await developmentFund.changeTokenReleaseSchedule(zero, releaseDuration, releaseTokenAmount, { from: governance }); - - // Increasing the time to pass atleast one duration. - await time.increase(releaseDuration[releaseDuration.length - 1] + 1); - - await developmentFund.withdrawTokensByUnlockedTokenOwner(releaseTokenAmount[releaseTokenAmount.length - 1], { from: multisig }); - - releaseDuration.pop(); - releaseTokenAmount.pop(); - await checkStatus(developmentFund, [0, 0, 0, 0, 1, 1, 0], zero, zero, zero, zero, releaseDuration, releaseTokenAmount, zero); - }); - - it("After withdrawing all tokens after 2 particular schedule, the release array size should decrease.", async () => { - /// @dev This test requires a hard reset of init fixture - await deploymentAndInitFixture(); - - releaseTokenAmount = createReleaseTokenAmount(); - totalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); - await testToken.mint(governance, totalReleaseTokenAmount); - await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { from: governance }); - await developmentFund.changeTokenReleaseSchedule(zero, releaseDuration, releaseTokenAmount, { from: governance }); - - // Increasing the time to pass atleast one duration. - await time.increase(releaseDuration[releaseDuration.length - 1] + releaseDuration[releaseDuration.length - 2] + 1); - - await developmentFund.withdrawTokensByUnlockedTokenOwner( - releaseTokenAmount[releaseTokenAmount.length - 1] + releaseTokenAmount[releaseTokenAmount.length - 2], - { from: multisig } - ); - - releaseDuration.pop(); - releaseDuration.pop(); - releaseTokenAmount.pop(); - releaseTokenAmount.pop(); - await checkStatus(developmentFund, [0, 0, 0, 0, 1, 1, 0], zero, zero, zero, zero, releaseDuration, releaseTokenAmount, zero); - }); - - it("After withdrawing part of tokens after a particular schedule, the release token amount should decrease.", async () => { - /// @dev This test requires a hard reset of init fixture - await deploymentAndInitFixture(); - - releaseTokenAmount = createReleaseTokenAmount(); - totalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); - await testToken.mint(governance, totalReleaseTokenAmount); - await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { from: governance }); - await developmentFund.changeTokenReleaseSchedule(zero, releaseDuration, releaseTokenAmount, { from: governance }); - - // Increasing the time to pass atleast one duration. - await time.increase(releaseDuration[releaseDuration.length - 1] + 1); - - let withdrawAmount = Math.floor(releaseTokenAmount[releaseTokenAmount.length - 1] / 2); - await developmentFund.withdrawTokensByUnlockedTokenOwner(withdrawAmount, { from: multisig }); - releaseTokenAmount[releaseTokenAmount.length - 1] = releaseTokenAmount[releaseTokenAmount.length - 1] - withdrawAmount; - await checkStatus(developmentFund, [0, 0, 0, 0, 1, 1, 0], zero, zero, zero, zero, releaseDuration, releaseTokenAmount, zero); - }); - - it("After withdrawing all tokens after a particular schedule, the release time should be updated based on duration.", async () => { - /// @dev This test requires a hard reset of init fixture - await deploymentAndInitFixture(); - - releaseTokenAmount = createReleaseTokenAmount(); - totalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); - await testToken.mint(governance, totalReleaseTokenAmount); - await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { from: governance }); - await developmentFund.changeTokenReleaseSchedule(zero, releaseDuration, releaseTokenAmount, { from: governance }); - - // Increasing the time to pass atleast one duration. - await time.increase(releaseDuration[releaseDuration.length - 1] + 1); - - let lastReleaseTime = await developmentFund.lastReleaseTime(); - - await developmentFund.withdrawTokensByUnlockedTokenOwner(releaseTokenAmount[releaseTokenAmount.length - 1], { from: multisig }); - - await checkStatus( - developmentFund, - [0, 0, 0, 1, 0, 0, 0], - zero, - zero, - zero, - lastReleaseTime.toNumber() + releaseDuration[releaseDuration.length - 1], - zero, - zero, - zero - ); - }); - - it("After withdrawing part of tokens after a particular schedule, the release time should not be updated based on duration.", async () => { - /// @dev This test requires a hard reset of init fixture - await deploymentAndInitFixture(); - - releaseTokenAmount = createReleaseTokenAmount(); - totalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); - await testToken.mint(governance, totalReleaseTokenAmount); - await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { from: governance }); - - // This time we will be changing the last release time. - let currentTime = await time.latest(); - await developmentFund.changeTokenReleaseSchedule(currentTime, releaseDuration, releaseTokenAmount, { from: governance }); - - // Increasing the time to pass atleast one duration. - await time.increase(releaseDuration[releaseDuration.length - 1] + 1); - - let withdrawAmount = Math.floor(releaseTokenAmount[releaseTokenAmount.length - 1] / 2); - await developmentFund.withdrawTokensByUnlockedTokenOwner(withdrawAmount, { from: multisig }); - await checkStatus(developmentFund, [0, 0, 0, 1, 0, 0, 0], zero, zero, zero, currentTime, zero, zero, zero); - }); - - it("Zero Tokens could not be withdrawed from release schedule.", async () => { - await expectRevert(developmentFund.withdrawTokensByUnlockedTokenOwner(zero, { from: multisig }), "Zero can't be withdrawn."); - }); - - it("Transferring all tokens to a receiver by Locked Token should update the remainingToken.", async () => { - let value = randomValue() + 1; - await testToken.mint(userOne, value); - await testToken.approve(developmentFund.address, value, { from: userOne }); - await developmentFund.depositTokens(value, { from: userOne }); - await developmentFund.transferTokensByLockedTokenOwner(creator, { from: governance }); - let tokenBalance = await testToken.balanceOf(developmentFund.address); - assert.equal(tokenBalance.toNumber(), 0, "Token Balance in contract does not match the correct amount."); - await checkStatus(developmentFund, [0, 0, 0, 0, 0, 0, 1], zero, zero, zero, zero, zero, zero, statusExpired); - }); + let developmentFund, testToken; + let creator, governance, newGovernance, multisig, newMultisig, safeVault, userOne; + + async function deploymentAndInitFixture(_wallets, _provider) { + // Creating a new release schedule. + releaseDuration = []; + // This is run 60 times for mimicking 5 years (12 months * 5), though the interval is small. + for (let times = 0; times < 60; times++) { + releaseDuration.push(releaseInterval); + } + + // Creating a new release token schedule. + releaseTokenAmount = createReleaseTokenAmount(); + + // Creating the contract instance. + developmentFund = await DevelopmentFund.new( + testToken.address, + governance, + safeVault, + multisig, + zero, + releaseDuration, + releaseTokenAmount, + { from: creator } + ); + + // Calculating the total tokens in the release schedule. + totalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); + + // Minting new Tokens. + await testToken.mint(creator, totalSupply, { from: creator }); + + // Approving the development fund to do a transfer on behalf of governance. + await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { + from: creator, + }); + + // Marking the contract as active. + await developmentFund.init({ from: creator }); + } + + before("Initiating Accounts & Creating Test Token Instance.", async () => { + // Checking if we have enough accounts to test. + assert.isAtLeast( + accounts.length, + 7, + "At least 7 accounts are required to test the contracts." + ); + [creator, governance, newGovernance, multisig, newMultisig, safeVault, userOne] = accounts; + + // Creating the instance of Test Token. + testToken = await TestToken.new("TestToken", "TST", 18, zero); + }); + + beforeEach("Creating New Development Fund Instance.", async () => { + // await loadFixture(deploymentAndInitFixture); + }); + + it("Successfully created an instance without any error.", async () => { + /// @dev Explicit call to use fixture snapshot as initial status + await loadFixture(deploymentAndInitFixture); + + /// @dev The re-deployment is only valid on this local scope (variable developmentFund) + /// fixture removes it for following tests + let developmentFund = await DevelopmentFund.new( + testToken.address, + governance, + safeVault, + multisig, + zero, + releaseDuration, + releaseTokenAmount, + { from: creator } + ); + + let currentTime = await time.latest(); + + // Checking if the contract is in deployed state. + await checkStatus( + developmentFund, + [0, 0, 0, 0, 0, 0, 1], + zero, + zero, + zero, + zero, + zero, + zero, + statusDeployed + ); + + // Minting new Tokens. + await testToken.mint(creator, totalSupply, { from: creator }); + + // Approving the development fund to do a transfer on behalf of governance. + await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { + from: creator, + }); + + // Marking the contract as active. + await developmentFund.init({ from: creator }); + + await checkStatus( + developmentFund, + [1, 1, 0, 1, 1, 1, 1], + governance, + multisig, + zero, + currentTime, + releaseDuration, + releaseTokenAmount, + statusActive + ); + }); + + it("Adding new Locked Token Owner should update the new Locked Token Owner storage.", async () => { + /// @dev Explicit call to use fixture snapshot as initial status + await loadFixture(deploymentAndInitFixture); + + await developmentFund.updateLockedTokenOwner(newGovernance, { from: governance }); + await checkStatus( + developmentFund, + [0, 0, 1, 0, 0, 0, 0], + zero, + zero, + newGovernance, + zero, + zero, + zero, + zero + ); + }); + + it("Approving new Locked Token Owner should update the Locked Token Owner.", async () => { + /// @dev Explicit call to use fixture snapshot as initial status + await loadFixture(deploymentAndInitFixture); + + await developmentFund.updateLockedTokenOwner(newGovernance, { from: governance }); + await developmentFund.approveLockedTokenOwner({ from: multisig }); + await checkStatus( + developmentFund, + [1, 0, 0, 0, 0, 0, 0], + newGovernance, + zero, + zero, + zero, + zero, + zero, + zero + ); + }); + + it("After approval of new Locked Token Owner, newLockedTokenOwner should be zero address.", async () => { + /// @dev Explicit call to use fixture snapshot as initial status + await loadFixture(deploymentAndInitFixture); + + await developmentFund.updateLockedTokenOwner(newGovernance, { from: governance }); + await developmentFund.approveLockedTokenOwner({ from: multisig }); + await checkStatus( + developmentFund, + [0, 0, 1, 0, 0, 0, 0], + zero, + zero, + constants.ZERO_ADDRESS, + zero, + zero, + zero, + zero + ); + }); + + it("Adding new Deposit should update the remaining token and contract token balance.", async () => { + /// @dev Explicit call to use fixture snapshot as initial status + await loadFixture(deploymentAndInitFixture); + + let value = randomValue() + 1; + await testToken.mint(userOne, value); + await testToken.approve(developmentFund.address, value, { from: userOne }); + await developmentFund.depositTokens(value, { from: userOne }); + let tokenBalance = await testToken.balanceOf(developmentFund.address); + assert.equal( + tokenBalance.toNumber(), + value + totalReleaseTokenAmount, + "Token Balance in contract does not match the deposited amount." + ); + }); + + it("The contract should be approved to make token transfer for deposit.", async () => { + /// @dev Explicit call to use fixture snapshot as initial status + await loadFixture(deploymentAndInitFixture); + + let value = randomValue() + 1; + await expectRevert( + developmentFund.depositTokens(value, { from: userOne }), + "invalid transfer" + ); + }); + + it("Zero Tokens could not be deposited.", async () => { + /// @dev Explicit call to use fixture snapshot as initial status + await loadFixture(deploymentAndInitFixture); + + await expectRevert( + developmentFund.depositTokens(0, { from: userOne }), + "Amount needs to be bigger than zero." + ); + }); + + it("Updating the release schedule should update the lastReleaseTime, releaseDuration and releaseTokenAmount.", async () => { + /// @dev Explicit call to use fixture snapshot as initial status + await loadFixture(deploymentAndInitFixture); + + let newReleaseTime = randomValue() + 1; + releaseTokenAmount = createReleaseTokenAmount(); + totalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); + await testToken.mint(governance, totalReleaseTokenAmount); + await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { + from: governance, + }); + await developmentFund.changeTokenReleaseSchedule( + newReleaseTime, + releaseDuration, + releaseTokenAmount, + { from: governance } + ); + await checkStatus( + developmentFund, + [0, 0, 0, 1, 1, 1, 0], + zero, + zero, + zero, + newReleaseTime, + releaseDuration, + releaseTokenAmount, + zero + ); + let tokenBalance = await testToken.balanceOf(developmentFund.address); + assert.equal( + tokenBalance.toNumber(), + totalReleaseTokenAmount, + "Token Balance in contract does not match the correct amount." + ); + }); + + it("Updating the release schedule twice should update the lastReleaseTime, releaseDuration and releaseTokenAmount accordingly.", async () => { + /// @dev Explicit call to use fixture snapshot as initial status + await loadFixture(deploymentAndInitFixture); + + // First Time + let newReleaseTime = randomValue() + 1; + releaseTokenAmount = createReleaseTokenAmount(); + totalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); + await testToken.mint(governance, totalReleaseTokenAmount); + await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { + from: governance, + }); + await developmentFund.changeTokenReleaseSchedule( + newReleaseTime, + releaseDuration, + releaseTokenAmount, + { from: governance } + ); + await checkStatus( + developmentFund, + [0, 0, 0, 1, 1, 1, 0], + zero, + zero, + zero, + newReleaseTime, + releaseDuration, + releaseTokenAmount, + zero + ); + let tokenBalance = await testToken.balanceOf(developmentFund.address); + assert.equal( + tokenBalance.toNumber(), + totalReleaseTokenAmount, + "Token Balance in contract does not match the correct amount." + ); + + // Second Time + newReleaseTime = randomValue() + 1; + releaseTokenAmount = createReleaseTokenAmount(); + totalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); + await testToken.mint(governance, totalReleaseTokenAmount); + await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { + from: governance, + }); + await developmentFund.changeTokenReleaseSchedule( + newReleaseTime, + releaseDuration, + releaseTokenAmount, + { from: governance } + ); + await checkStatus( + developmentFund, + [0, 0, 0, 1, 1, 1, 0], + zero, + zero, + zero, + newReleaseTime, + releaseDuration, + releaseTokenAmount, + zero + ); + tokenBalance = await testToken.balanceOf(developmentFund.address); + assert.equal( + tokenBalance.toNumber(), + totalReleaseTokenAmount, + "Token Balance in contract does not match the correct amount." + ); + }); + + it("While updating release schedule, extra tokens should be sent back.", async () => { + let previousReleaseTokenAmount = totalReleaseTokenAmount; + let newReleaseTime = randomValue() + 1; + releaseTokenAmount = createReleaseTokenAmount(); + totalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); + + await testToken.mint(governance, totalReleaseTokenAmount); + await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { + from: governance, + }); + await developmentFund.depositTokens(totalReleaseTokenAmount, { from: governance }); + + // Checking token balance of governance contract before tx + let beforeTokenBalance = await testToken.balanceOf(governance); + + await developmentFund.changeTokenReleaseSchedule( + newReleaseTime, + releaseDuration, + releaseTokenAmount, + { from: governance } + ); + + // Checking token balance of governance contract after tx + let afterTokenBalance = await testToken.balanceOf(governance); + + assert.strictEqual( + afterTokenBalance.toNumber(), + beforeTokenBalance.toNumber() + previousReleaseTokenAmount, + "Extra tokens not sent back." + ); + await checkStatus( + developmentFund, + [0, 0, 0, 1, 1, 1, 0], + zero, + zero, + zero, + newReleaseTime, + releaseDuration, + releaseTokenAmount, + zero + ); + let tokenBalance = await testToken.balanceOf(developmentFund.address); + assert.equal( + tokenBalance.toNumber(), + totalReleaseTokenAmount, + "Token Balance in contract does not match the correct amount." + ); + }); + + it("While updating release schedule, deficient tokens should be sent to contract.", async () => { + let newReleaseTime = randomValue() + 1; + releaseTokenAmount = createReleaseTokenAmount(); + let newTotalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); + if (newTotalReleaseTokenAmount <= totalReleaseTokenAmount) { + let difference = totalReleaseTokenAmount - newTotalReleaseTokenAmount + randomValue(); + releaseTokenAmount[releaseTokenAmount.length - 1] += difference; + } + newTotalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); + let deficient = newTotalReleaseTokenAmount - totalReleaseTokenAmount; + + await testToken.mint(governance, deficient); + await testToken.approve(developmentFund.address, deficient, { from: governance }); + + // Checking token balance of governance contract before tx + let beforeTokenBalance = await testToken.balanceOf(governance); + + await developmentFund.changeTokenReleaseSchedule( + newReleaseTime, + releaseDuration, + releaseTokenAmount, + { from: governance } + ); + + // Checking token balance of governance contract after tx + let afterTokenBalance = await testToken.balanceOf(governance); + + assert.strictEqual( + afterTokenBalance.toNumber(), + beforeTokenBalance.toNumber() - deficient, + "Extra tokens not sent to contract." + ); + await checkStatus( + developmentFund, + [0, 0, 0, 1, 1, 1, 0], + zero, + zero, + zero, + newReleaseTime, + releaseDuration, + releaseTokenAmount, + zero + ); + let tokenBalance = await testToken.balanceOf(developmentFund.address); + assert.equal( + tokenBalance.toNumber(), + newTotalReleaseTokenAmount, + "Token Balance in contract does not match the correct amount." + ); + }); + + it("Unequal array for duration and tokens should not be accepted for token release schedule.", async () => { + let newReleaseTime = randomValue() + 1; + releaseTokenAmount = createReleaseTokenAmount(); + releaseTokenAmount.pop(); + await expectRevert( + developmentFund.changeTokenReleaseSchedule( + newReleaseTime, + releaseDuration, + releaseTokenAmount, + { from: governance } + ), + "Release Schedule does not match." + ); + }); + + it("Transferring all tokens to a safeVault by Unlocked Token Owner should update the remainingToken.", async () => { + let value = randomValue() + 1; + await testToken.mint(userOne, value); + await testToken.approve(developmentFund.address, value, { from: userOne }); + await developmentFund.depositTokens(value, { from: userOne }); + await developmentFund.transferTokensByUnlockedTokenOwner({ from: multisig }); + let tokenBalance = await testToken.balanceOf(developmentFund.address); + assert.equal( + tokenBalance.toNumber(), + 0, + "Token Balance in contract does not match the correct amount." + ); + await checkStatus( + developmentFund, + [0, 0, 0, 0, 0, 0, 1], + zero, + zero, + zero, + zero, + zero, + zero, + statusExpired + ); + }); + + it("After withdrawing all tokens after a particular schedule, the release array size should decrease.", async () => { + /// @dev This test requires a hard reset of init fixture + await deploymentAndInitFixture(); + + releaseTokenAmount = createReleaseTokenAmount(); + totalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); + await testToken.mint(governance, totalReleaseTokenAmount); + await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { + from: governance, + }); + await developmentFund.changeTokenReleaseSchedule( + zero, + releaseDuration, + releaseTokenAmount, + { from: governance } + ); + + // Increasing the time to pass atleast one duration. + await time.increase(releaseDuration[releaseDuration.length - 1] + 1); + + await developmentFund.withdrawTokensByUnlockedTokenOwner( + releaseTokenAmount[releaseTokenAmount.length - 1], + { from: multisig } + ); + + releaseDuration.pop(); + releaseTokenAmount.pop(); + await checkStatus( + developmentFund, + [0, 0, 0, 0, 1, 1, 0], + zero, + zero, + zero, + zero, + releaseDuration, + releaseTokenAmount, + zero + ); + }); + + it("After withdrawing all tokens after 2 particular schedule, the release array size should decrease.", async () => { + /// @dev This test requires a hard reset of init fixture + await deploymentAndInitFixture(); + + releaseTokenAmount = createReleaseTokenAmount(); + totalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); + await testToken.mint(governance, totalReleaseTokenAmount); + await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { + from: governance, + }); + await developmentFund.changeTokenReleaseSchedule( + zero, + releaseDuration, + releaseTokenAmount, + { from: governance } + ); + + // Increasing the time to pass atleast one duration. + await time.increase( + releaseDuration[releaseDuration.length - 1] + + releaseDuration[releaseDuration.length - 2] + + 1 + ); + + await developmentFund.withdrawTokensByUnlockedTokenOwner( + releaseTokenAmount[releaseTokenAmount.length - 1] + + releaseTokenAmount[releaseTokenAmount.length - 2], + { from: multisig } + ); + + releaseDuration.pop(); + releaseDuration.pop(); + releaseTokenAmount.pop(); + releaseTokenAmount.pop(); + await checkStatus( + developmentFund, + [0, 0, 0, 0, 1, 1, 0], + zero, + zero, + zero, + zero, + releaseDuration, + releaseTokenAmount, + zero + ); + }); + + it("After withdrawing part of tokens after a particular schedule, the release token amount should decrease.", async () => { + /// @dev This test requires a hard reset of init fixture + await deploymentAndInitFixture(); + + releaseTokenAmount = createReleaseTokenAmount(); + totalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); + await testToken.mint(governance, totalReleaseTokenAmount); + await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { + from: governance, + }); + await developmentFund.changeTokenReleaseSchedule( + zero, + releaseDuration, + releaseTokenAmount, + { from: governance } + ); + + // Increasing the time to pass atleast one duration. + await time.increase(releaseDuration[releaseDuration.length - 1] + 1); + + let withdrawAmount = Math.floor(releaseTokenAmount[releaseTokenAmount.length - 1] / 2); + await developmentFund.withdrawTokensByUnlockedTokenOwner(withdrawAmount, { + from: multisig, + }); + releaseTokenAmount[releaseTokenAmount.length - 1] = + releaseTokenAmount[releaseTokenAmount.length - 1] - withdrawAmount; + await checkStatus( + developmentFund, + [0, 0, 0, 0, 1, 1, 0], + zero, + zero, + zero, + zero, + releaseDuration, + releaseTokenAmount, + zero + ); + }); + + it("After withdrawing all tokens after a particular schedule, the release time should be updated based on duration.", async () => { + /// @dev This test requires a hard reset of init fixture + await deploymentAndInitFixture(); + + releaseTokenAmount = createReleaseTokenAmount(); + totalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); + await testToken.mint(governance, totalReleaseTokenAmount); + await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { + from: governance, + }); + await developmentFund.changeTokenReleaseSchedule( + zero, + releaseDuration, + releaseTokenAmount, + { from: governance } + ); + + // Increasing the time to pass atleast one duration. + await time.increase(releaseDuration[releaseDuration.length - 1] + 1); + + let lastReleaseTime = await developmentFund.lastReleaseTime(); + + await developmentFund.withdrawTokensByUnlockedTokenOwner( + releaseTokenAmount[releaseTokenAmount.length - 1], + { from: multisig } + ); + + await checkStatus( + developmentFund, + [0, 0, 0, 1, 0, 0, 0], + zero, + zero, + zero, + lastReleaseTime.toNumber() + releaseDuration[releaseDuration.length - 1], + zero, + zero, + zero + ); + }); + + it("After withdrawing part of tokens after a particular schedule, the release time should not be updated based on duration.", async () => { + /// @dev This test requires a hard reset of init fixture + await deploymentAndInitFixture(); + + releaseTokenAmount = createReleaseTokenAmount(); + totalReleaseTokenAmount = calculateTotalTokenAmount(releaseTokenAmount); + await testToken.mint(governance, totalReleaseTokenAmount); + await testToken.approve(developmentFund.address, totalReleaseTokenAmount, { + from: governance, + }); + + // This time we will be changing the last release time. + let currentTime = await time.latest(); + await developmentFund.changeTokenReleaseSchedule( + currentTime, + releaseDuration, + releaseTokenAmount, + { from: governance } + ); + + // Increasing the time to pass atleast one duration. + await time.increase(releaseDuration[releaseDuration.length - 1] + 1); + + let withdrawAmount = Math.floor(releaseTokenAmount[releaseTokenAmount.length - 1] / 2); + await developmentFund.withdrawTokensByUnlockedTokenOwner(withdrawAmount, { + from: multisig, + }); + await checkStatus( + developmentFund, + [0, 0, 0, 1, 0, 0, 0], + zero, + zero, + zero, + currentTime, + zero, + zero, + zero + ); + }); + + it("Zero Tokens could not be withdrawed from release schedule.", async () => { + await expectRevert( + developmentFund.withdrawTokensByUnlockedTokenOwner(zero, { from: multisig }), + "Zero can't be withdrawn." + ); + }); + + it("Transferring all tokens to a receiver by Locked Token should update the remainingToken.", async () => { + let value = randomValue() + 1; + await testToken.mint(userOne, value); + await testToken.approve(developmentFund.address, value, { from: userOne }); + await developmentFund.depositTokens(value, { from: userOne }); + await developmentFund.transferTokensByLockedTokenOwner(creator, { from: governance }); + let tokenBalance = await testToken.balanceOf(developmentFund.address); + assert.equal( + tokenBalance.toNumber(), + 0, + "Token Balance in contract does not match the correct amount." + ); + await checkStatus( + developmentFund, + [0, 0, 0, 0, 0, 0, 1], + zero, + zero, + zero, + zero, + zero, + zero, + statusExpired + ); + }); }); diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index 5c0e611c0..6c339be0a 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -22,877 +22,1167 @@ const ONE_MILLON = "1000000000000000000000000"; const ONE_ETHER = "1000000000000000000"; contract("FourYearVesting", (accounts) => { - let root, a1, a2, a3; - let token, staking, stakingLogic, feeSharingProxy; - let vestingLogic; - let vestingFactory; - let kickoffTS; - - let cliff = 4 * WEEK; - let duration = 156 * WEEK; - - before(async () => { - [root, a1, a2, a3, ...accounts] = accounts; - token = await SOV.new(TOTAL_SUPPLY); - wrbtc = await TestWrbtc.new(); - - vestingLogic = await VestingLogic.new(); - vestingFactory = await VestingFactory.new(); - - feeSharingProxy = await FeeSharingProxy.new(constants.ZERO_ADDRESS, constants.ZERO_ADDRESS); - - stakingLogic = await StakingLogic.new(token.address); - staking = await StakingProxy.new(token.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingLogic.at(staking.address); - //Upgradable Vesting Registry - vestingRegistryLogic = await VestingRegistryLogic.new(); - vestingReg = await VestingRegistryProxy.new(); - await vestingReg.setImplementation(vestingRegistryLogic.address); - vestingReg = await VestingRegistryLogic.at(vestingReg.address); - await staking.setVestingRegistry(vestingReg.address); - - await token.transfer(a2, "1000"); - await token.approve(staking.address, "1000", { from: a2 }); - - kickoffTS = await staking.kickoffTS.call(); - }); - - describe("vestingfactory", () => { - it("sets the expected values", async () => { - let vestingInstance = await vestingFactory.deployFourYearVesting( - token.address, - staking.address, - a1, - feeSharingProxy.address, - root, - vestingLogic.address - ); - vestingInstance = await VestingLogic.at(vestingInstance.logs[0].address); - - // Check data - let _sov = await vestingInstance.SOV(); - let _stackingAddress = await vestingInstance.staking(); - let _tokenOwner = await vestingInstance.tokenOwner(); - let _cliff = await vestingInstance.CLIFF(); - let _duration = await vestingInstance.DURATION(); - let _feeSharingProxy = await vestingInstance.feeSharingProxy(); - - assert.equal(_sov, token.address); - assert.equal(_stackingAddress, staking.address); - assert.equal(_tokenOwner, a1); - assert.equal(_cliff.toString(), cliff); - assert.equal(_duration.toString(), duration); - assert.equal(_feeSharingProxy, feeSharingProxy.address); - }); - }); - - describe("constructor", () => { - it("sets the expected values", async () => { - let vestingInstance = await Vesting.new(vestingLogic.address, token.address, staking.address, root, feeSharingProxy.address); - vestingInstance = await VestingLogic.at(vestingInstance.address); - - // Check data - let _sov = await vestingInstance.SOV(); - let _stackingAddress = await vestingInstance.staking(); - let _tokenOwner = await vestingInstance.tokenOwner(); - let _cliff = await vestingInstance.CLIFF(); - let _duration = await vestingInstance.DURATION(); - let _feeSharingProxy = await vestingInstance.feeSharingProxy(); - - assert.equal(_sov, token.address); - assert.equal(_stackingAddress, staking.address); - assert.equal(_tokenOwner, root); - assert.equal(_cliff.toString(), cliff); - assert.equal(_duration.toString(), duration); - assert.equal(_feeSharingProxy, feeSharingProxy.address); - }); - - it("fails if the 0 address is passed as SOV address", async () => { - await expectRevert( - Vesting.new(vestingLogic.address, constants.ZERO_ADDRESS, staking.address, root, feeSharingProxy.address), - "SOV address invalid" - ); - }); - - it("fails if the 0 address is passed as token owner address", async () => { - await expectRevert( - Vesting.new(vestingLogic.address, token.address, staking.address, constants.ZERO_ADDRESS, feeSharingProxy.address), - "token owner address invalid" - ); - }); - - it("fails if the 0 address is passed as staking address", async () => { - await expectRevert( - Vesting.new(vestingLogic.address, token.address, constants.ZERO_ADDRESS, root, feeSharingProxy.address), - "staking address invalid" - ); - }); - - it("fails if the 0 address is passed as feeSharingProxy address", async () => { - await expectRevert( - Vesting.new(vestingLogic.address, token.address, staking.address, root, constants.ZERO_ADDRESS), - "feeSharingProxy address invalid" - ); - }); - - it("fails if logic is not a contract address", async () => { - await expectRevert(Vesting.new(a1, token.address, staking.address, a1, feeSharingProxy.address), "_logic not a contract"); - }); - - it("fails if SOV is not a contract address", async () => { - await expectRevert(Vesting.new(vestingLogic.address, a1, staking.address, a1, feeSharingProxy.address), "_SOV not a contract"); - }); - - it("fails if staking address is not a contract address", async () => { - await expectRevert( - Vesting.new(vestingLogic.address, token.address, a1, a1, feeSharingProxy.address), - "_stakingAddress not a contract" - ); - }); - - it("fails if fee sharing is not a contract address", async () => { - await expectRevert( - Vesting.new(vestingLogic.address, token.address, staking.address, a1, a1), - "_feeSharingProxy not a contract" - ); - }); - }); - - describe("delegate", () => { - let vesting; - it("should stake tokens and delegate voting power", async () => { - let toStake = ONE_MILLON; - vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, a2, feeSharingProxy.address); - vesting = await VestingLogic.at(vesting.address); - - await token.approve(vesting.address, toStake); - let remainingStakeAmount = ONE_MILLON; - let lastStakingSchedule = 0; - while (remainingStakeAmount > 0) { - await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); - lastStakingSchedule = await vesting.lastStakingSchedule(); - remainingStakeAmount = await vesting.remainingStakeAmount(); - } - - // check delegatee - let data = await staking.getStakes.call(vesting.address); - /// @dev Optimization: This loop through 40 steps is a bottleneck - for (let i = 0; i < data.dates.length; i++) { - let delegatee = await staking.delegates(vesting.address, data.dates[i]); - expect(delegatee).equal(a2); - } - - // delegate - let tx = await vesting.delegate(a1, { from: a2 }); - - expectEvent(tx, "VotesDelegated", { - caller: a2, - delegatee: a1, - }); - - // check new delegatee - data = await staking.getStakes.call(vesting.address); - /// @dev Optimization: This loop through 40 steps is a bottleneck - for (let i = 0; i < data.dates.length; i++) { - let delegatee = await staking.delegates(vesting.address, data.dates[i]); - expect(delegatee).equal(a1); - } - }); - - it("fails if delegatee is zero address", async () => { - await expectRevert(vesting.delegate(constants.ZERO_ADDRESS, { from: a2 }), "delegatee address invalid"); - }); - - it("fails if not a token owner", async () => { - await expectRevert(vesting.delegate(a1, { from: a1 }), "unauthorized"); - }); - }); - - describe("stakeTokens; using Ganache", () => { - // Check random scenarios - let vesting; - it("should stake 1,000,000 SOV with a duration of 156 weeks and a 4 week cliff", async () => { - vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, root, feeSharingProxy.address); - vesting = await VestingLogic.at(vesting.address); - await token.approve(vesting.address, ONE_MILLON); - let remainingStakeAmount = ONE_MILLON; - let lastStakingSchedule = 0; - while (remainingStakeAmount > 0) { - let tx = await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); - expectEvent(tx, "TokensStaked"); - lastStakingSchedule = await vesting.lastStakingSchedule(); - remainingStakeAmount = await vesting.remainingStakeAmount(); - } - - // check delegatee - let data = await staking.getStakes.call(vesting.address); - for (let i = 0; i < data.dates.length; i++) { - let delegatee = await staking.delegates(vesting.address, data.dates[i]); - expect(delegatee).equal(root); - } - }); - - it("should stake 1,000,000 SOV with a duration of 156 weeks and a 4 week cliff", async () => { - let block = await lastBlock(); - let timestamp = parseInt(block.timestamp); - - let kickoffTS = await staking.kickoffTS(); - - let start = timestamp + 4 * WEEK; - let end = timestamp + 156 * WEEK; - - let numIntervals = Math.floor((end - start) / (4 * WEEK)) + 1; - let stakedPerInterval = ONE_MILLON / numIntervals; - - // positive case - for (let i = start; i <= end; i += 4 * WEEK) { - let periodFromKickoff = Math.floor((i - kickoffTS.toNumber()) / (2 * WEEK)); - let startBuf = periodFromKickoff * 2 * WEEK + kickoffTS.toNumber(); - let userStakingCheckpoints = await staking.userStakingCheckpoints(vesting.address, startBuf, 0); - - assert.equal(userStakingCheckpoints.stake.toString(), stakedPerInterval); - - let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints(vesting.address, startBuf); - assert.equal(numUserStakingCheckpoints.toString(), "1"); - } - - // negative cases - - // start-10 to avoid coming to active checkpoint - let periodFromKickoff = Math.floor((start - 10 - kickoffTS.toNumber()) / (2 * WEEK)); - let startBuf = periodFromKickoff * 2 * WEEK + kickoffTS.toNumber(); - let userStakingCheckpoints = await staking.userStakingCheckpoints(vesting.address, startBuf, 0); - - assert.equal(userStakingCheckpoints.fromBlock.toNumber(), 0); - assert.equal(userStakingCheckpoints.stake.toString(), 0); - - let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints(vesting.address, startBuf); - assert.equal(numUserStakingCheckpoints.toString(), "0"); - periodFromKickoff = Math.floor((end + 3 * WEEK - kickoffTS.toNumber()) / (2 * WEEK)); - startBuf = periodFromKickoff * 2 * WEEK + kickoffTS.toNumber(); - userStakingCheckpoints = await staking.userStakingCheckpoints(vesting.address, startBuf, 0); - - assert.equal(userStakingCheckpoints.fromBlock.toNumber(), 0); - assert.equal(userStakingCheckpoints.stake.toString(), 0); - - numUserStakingCheckpoints = await staking.numUserStakingCheckpoints(vesting.address, startBuf); - assert.equal(numUserStakingCheckpoints.toString(), "0"); - }); - - it("should not allow to stake 2 times 1,000,000 SOV with a duration of 156 weeks and a 4 week cliff", async () => { - let amount = ONE_MILLON; - let cliff = 4 * WEEK; - let duration = 156 * WEEK; - vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, root, feeSharingProxy.address); - vesting = await VestingLogic.at(vesting.address); - - await token.approve(vesting.address, amount); - let remainingStakeAmount = amount; - let lastStakingSchedule = 0; - while (remainingStakeAmount > 0) { - await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); - lastStakingSchedule = await vesting.lastStakingSchedule(); - remainingStakeAmount = await vesting.remainingStakeAmount(); - } - - await increaseTime(52 * WEEK); - await token.approve(vesting.address, amount); - await expectRevert(vesting.stakeTokens(amount, 0), "create new vesting address"); - }); - }); - - describe("stakeTokensWithApproval", () => { - let vesting; - - it("fails if invoked directly", async () => { - let amount = 1000; - let cliff = 4 * WEEK; - let duration = 39 * 4 * WEEK; - vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, root, feeSharingProxy.address); - vesting = await VestingLogic.at(vesting.address); - await expectRevert(vesting.stakeTokensWithApproval(root, amount, 0), "unauthorized"); - }); - - it("fails if pass wrong method in data", async () => { - let amount = 1000; - let cliff = 4 * WEEK; - let duration = 39 * 4 * WEEK; - vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, root, feeSharingProxy.address); - vesting = await VestingLogic.at(vesting.address); - - let contract = new web3.eth.Contract(vesting.abi, vesting.address); - let sender = root; - let data = contract.methods.stakeTokens(amount, 0).encodeABI(); - - await expectRevert(token.approveAndCall(vesting.address, amount, data, { from: sender }), "method is not allowed"); - }); - - it("should stake ONE MILLION tokens with a duration of 156 weeks and a 4 week cliff", async () => { - let amount = ONE_MILLON; - let cliff = 4 * WEEK; - let duration = 39 * 4 * WEEK; - vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, root, feeSharingProxy.address); - vesting = await VestingLogic.at(vesting.address); - - let contract = new web3.eth.Contract(vesting.abi, vesting.address); - let sender = root; - let data = contract.methods.stakeTokensWithApproval(sender, amount, 0).encodeABI(); - let tx = await token.approveAndCall(vesting.address, amount, data, { from: sender }); - let lastStakingSchedule = await vesting.lastStakingSchedule(); - let remainingStakeAmount = await vesting.remainingStakeAmount(); - - data = contract.methods.stakeTokensWithApproval(sender, remainingStakeAmount, lastStakingSchedule).encodeABI(); - await token.approveAndCall(vesting.address, remainingStakeAmount, data, { from: sender }); - lastStakingSchedule = await vesting.lastStakingSchedule(); - remainingStakeAmount = await vesting.remainingStakeAmount(); - - data = contract.methods.stakeTokensWithApproval(sender, remainingStakeAmount, lastStakingSchedule).encodeABI(); - await token.approveAndCall(vesting.address, remainingStakeAmount, data, { from: sender }); - lastStakingSchedule = await vesting.lastStakingSchedule(); - remainingStakeAmount = await vesting.remainingStakeAmount(); - assert.equal(remainingStakeAmount, 0); - }); - - it("should stake 39000 tokens with a duration of 156 weeks and a 4 week cliff", async () => { - let amount = 39000; - let cliff = 4 * WEEK; - let duration = 156 * WEEK; - vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, root, feeSharingProxy.address); - vesting = await VestingLogic.at(vesting.address); - - let contract = new web3.eth.Contract(vesting.abi, vesting.address); - let sender = root; - let data = contract.methods.stakeTokensWithApproval(sender, amount, 0).encodeABI(); - await token.approveAndCall(vesting.address, amount, data, { from: sender }); - let lastStakingSchedule = await vesting.lastStakingSchedule(); - let remainingStakeAmount = await vesting.remainingStakeAmount(); - - data = contract.methods.stakeTokensWithApproval(sender, remainingStakeAmount, lastStakingSchedule).encodeABI(); - await token.approveAndCall(vesting.address, remainingStakeAmount, data, { from: sender }); - lastStakingSchedule = await vesting.lastStakingSchedule(); - remainingStakeAmount = await vesting.remainingStakeAmount(); - - data = contract.methods.stakeTokensWithApproval(sender, remainingStakeAmount, lastStakingSchedule).encodeABI(); - await token.approveAndCall(vesting.address, remainingStakeAmount, data, { from: sender }); - lastStakingSchedule = await vesting.lastStakingSchedule(); - remainingStakeAmount = await vesting.remainingStakeAmount(); - assert.equal(remainingStakeAmount, 0); - - let block = await web3.eth.getBlock("latest"); - let timestamp = block.timestamp; - - let start = timestamp + cliff; - let end = timestamp + duration; - - let numIntervals = Math.floor((end - start) / (4 * WEEK)) + 1; - let stakedPerInterval = Math.floor(amount / numIntervals); - - // positive case - for (let i = start; i <= end; i += 4 * WEEK) { - let periodFromKickoff = Math.floor((i - kickoffTS.toNumber()) / (2 * WEEK)); - let startBuf = periodFromKickoff * 2 * WEEK + kickoffTS.toNumber(); - let userStakingCheckpoints = await staking.userStakingCheckpoints(vesting.address, startBuf, 0); - - assert.equal(userStakingCheckpoints.stake.toString(), stakedPerInterval); - - let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints(vesting.address, startBuf); - assert.equal(numUserStakingCheckpoints.toString(), "1"); - } - }); - }); - - describe("withdrawTokens", () => { - let vesting; - it("should withdraw unlocked tokens", async () => { - // Save current amount - let previousAmount = await token.balanceOf(root); - let toStake = ONE_MILLON; - - // Stake - vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, root, feeSharingProxy.address); - vesting = await VestingLogic.at(vesting.address); - - await token.approve(vesting.address, toStake); - let remainingStakeAmount = ONE_MILLON; - let lastStakingSchedule = 0; - while (remainingStakeAmount > 0) { - await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); - lastStakingSchedule = await vesting.lastStakingSchedule(); - remainingStakeAmount = await vesting.remainingStakeAmount(); - } - - let amountAfterStake = await token.balanceOf(root); - - // time travel - await increaseTime(104 * WEEK); - - // withdraw - tx = await vesting.withdrawTokens(root); - - // check event - expectEvent(tx, "TokensWithdrawn", { - caller: root, - receiver: root, - }); - - // verify amount - let amount = await token.balanceOf(root); - - assert.equal(previousAmount.sub(new BN(toStake)).toString(), amountAfterStake.toString()); - expect(previousAmount).to.be.bignumber.greaterThan(amount); - expect(amount).to.be.bignumber.greaterThan(amountAfterStake); - }); - - it("should not withdraw unlocked tokens in the first year", async () => { - // Save current amount - let previousAmount = await token.balanceOf(root); - let toStake = ONE_MILLON; - - // Stake - vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, root, feeSharingProxy.address); - vesting = await VestingLogic.at(vesting.address); - - await token.approve(vesting.address, toStake); - let remainingStakeAmount = ONE_MILLON; - let lastStakingSchedule = 0; - while (remainingStakeAmount > 0) { - await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); - lastStakingSchedule = await vesting.lastStakingSchedule(); - remainingStakeAmount = await vesting.remainingStakeAmount(); - } - - let amountAfterStake = await token.balanceOf(root); - - // time travel - await increaseTime(34 * WEEK); - - // withdraw - tx = await vesting.withdrawTokens(root); - - // verify amount - let amount = await token.balanceOf(root); - - assert.equal(previousAmount.sub(new BN(toStake)).toString(), amountAfterStake.toString()); - expect(previousAmount).to.be.bignumber.greaterThan(amount); - assert.equal(amountAfterStake.toString(), amount.toString()); - }); - - it("should not allow for 2 stakes and withdrawal for the first year", async () => { - // Save current amount - let previousAmount = await token.balanceOf(root); - let toStake = ONE_ETHER; - - // Stake - vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, root, feeSharingProxy.address); - vesting = await VestingLogic.at(vesting.address); - - await token.approve(vesting.address, toStake); - await vesting.stakeTokens(toStake, 0); - let amountAfterStake = await token.balanceOf(root); - - // time travel - await increaseTime(20 * WEEK); - await token.approve(vesting.address, toStake); - await expectRevert(vesting.stakeTokens(toStake, 0), "create new vesting address"); - - // withdraw - tx = await vesting.withdrawTokens(root); - - // verify amount - let amount = await token.balanceOf(root); - - expect(previousAmount).to.be.bignumber.greaterThan(amount); - assert.equal(amountAfterStake.toString(), amount.toString()); - }); - - it("should do nothing if withdrawing a second time", async () => { - let amountOld = await token.balanceOf(root); - // withdraw - tx = await vesting.withdrawTokens(root); - - // verify amount - let amount = await token.balanceOf(root); - assert.equal(amountOld.toString(), amount.toString()); - }); - - it("should do nothing if withdrawing before reaching the cliff", async () => { - let toStake = ONE_MILLON; - - vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, a1, feeSharingProxy.address); - vesting = await VestingLogic.at(vesting.address); - - await token.approve(vesting.address, toStake); - let remainingStakeAmount = ONE_MILLON; - let lastStakingSchedule = 0; - while (remainingStakeAmount > 0) { - await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); - lastStakingSchedule = await vesting.lastStakingSchedule(); - remainingStakeAmount = await vesting.remainingStakeAmount(); - } - let amountOld = await token.balanceOf(root); - - // time travel - await increaseTime(2 * WEEK); - - // withdraw - tx = await vesting.withdrawTokens(a2, { from: a1 }); - - // verify amount - let amount = await token.balanceOf(root); - assert.equal(amountOld.toString(), amount.toString()); - }); - - it("should fail if the caller is not token owner", async () => { - await expectRevert(vesting.withdrawTokens(root, { from: a2 }), "unauthorized"); - await expectRevert(vesting.withdrawTokens(root, { from: a3 }), "unauthorized"); - - await expectRevert(vesting.withdrawTokens(root, { from: root }), "unauthorized"); - await increaseTime(30 * WEEK); - await expectRevert(vesting.withdrawTokens(root, { from: a2 }), "unauthorized"); - }); - - it("shouldn't be possible to use governanceWithdrawVesting by anyone but owner", async () => { - let toStake = ONE_MILLON; - - // Stake - vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, root, feeSharingProxy.address); - vesting = await VestingLogic.at(vesting.address); - - await token.approve(vesting.address, toStake); - let remainingStakeAmount = ONE_MILLON; - let lastStakingSchedule = 0; - while (remainingStakeAmount > 0) { - await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); - lastStakingSchedule = await vesting.lastStakingSchedule(); - remainingStakeAmount = await vesting.remainingStakeAmount(); - } - - await expectRevert(staking.governanceWithdrawVesting(vesting.address, root, { from: a1 }), "unauthorized"); - }); - - it("shouldn't be possible to use governanceWithdraw by user", async () => { - await expectRevert(staking.governanceWithdraw(100, kickoffTS.toNumber() + 52 * WEEK, root), "unauthorized"); - }); - }); - - describe("collectDividends", async () => { - let vesting; - it("should fail if the caller is neither owner nor token owner", async () => { - vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, a1, feeSharingProxy.address); - vesting = await VestingLogic.at(vesting.address); - await expectRevert(vesting.collectDividends(root, 10, a1, { from: a2 }), "unauthorized"); - await expectRevert(vesting.collectDividends(root, 10, a1, { from: a3 }), "unauthorized"); - }); - - it("should fail if receiver address is invalid", async () => { - let maxCheckpoints = new BN(10); - await expectRevert( - vesting.collectDividends(a1, maxCheckpoints, constants.ZERO_ADDRESS, { from: a1 }), - "receiver address invalid" - ); - }); - - it("should collect dividends", async () => { - let maxCheckpoints = new BN(10); - await expectRevert(vesting.collectDividends(a1, maxCheckpoints, a2), "unauthorized"); - let tx = await vesting.collectDividends(a1, maxCheckpoints, a2, { from: a1 }); - - let testData = await feeSharingProxy.testData.call(); - expect(testData.loanPoolToken).to.be.equal(a1); - expect(testData.maxCheckpoints).to.be.bignumber.equal(maxCheckpoints); - expect(testData.receiver).to.be.equal(a2); - - expectEvent(tx, "DividendsCollected", { - caller: a1, - loanPoolToken: a1, - receiver: a2, - maxCheckpoints: maxCheckpoints, - }); - }); - }); - - describe("migrateToNewStakingContract", async () => { - let vesting; - it("should set the new staking contract", async () => { - vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, a1, feeSharingProxy.address); - vesting = await VestingLogic.at(vesting.address); - // 1. set new staking contract address on staking contract - - let newStaking = await StakingProxy.new(token.address); - await newStaking.setImplementation(stakingLogic.address); - newStaking = await StakingLogic.at(newStaking.address); - - await staking.setNewStakingContract(newStaking.address); - - // 2. call migrateToNewStakingContract - let tx = await vesting.migrateToNewStakingContract(); - expectEvent(tx, "MigratedToNewStakingContract", { - caller: root, - newStakingContract: newStaking.address, - }); - let _staking = await vesting.staking(); - assert.equal(_staking, newStaking.address); - }); - - it("should fail if there is no new staking contract set", async () => { - let newStaking = await StakingProxy.new(token.address); - await newStaking.setImplementation(stakingLogic.address); - newStaking = await StakingLogic.at(newStaking.address); - - vesting = await Vesting.new(vestingLogic.address, token.address, newStaking.address, a1, feeSharingProxy.address); - vesting = await VestingLogic.at(vesting.address); - await expectRevert(vesting.migrateToNewStakingContract(), "there is no new staking contract set"); - }); - - it("should fail if the caller is neither owner nor token owner", async () => { - let newStaking = await StakingProxy.new(token.address); - await newStaking.setImplementation(stakingLogic.address); - newStaking = await StakingLogic.at(newStaking.address); - - vesting = await Vesting.new(vestingLogic.address, token.address, newStaking.address, a1, feeSharingProxy.address); - vesting = await VestingLogic.at(vesting.address); - - await newStaking.setNewStakingContract(newStaking.address); - - await expectRevert(vesting.migrateToNewStakingContract({ from: a2 }), "unauthorized"); - await expectRevert(vesting.migrateToNewStakingContract({ from: a3 }), "unauthorized"); - - await vesting.migrateToNewStakingContract(); - await vesting.migrateToNewStakingContract({ from: a1 }); - }); - }); - - describe("fouryearvesting", async () => { - let vesting, dates0, dates3, dates5; - it("staking schedule must fail if sufficient tokens aren't approved", async () => { - vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, root, feeSharingProxy.address); - vesting = await VestingLogic.at(vesting.address); - await token.approve(vesting.address, 1000); - await expectRevert(vesting.stakeTokens(ONE_MILLON, 0), "transfer amount exceeds allowance"); - }); - - it("staking schedule must fail for incorrect parameters", async () => { - vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, root, feeSharingProxy.address); - vesting = await VestingLogic.at(vesting.address); - await token.approve(vesting.address, ONE_MILLON); - - let remainingStakeAmount = ONE_MILLON; - let lastStakingSchedule = 0; - await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); - lastStakingSchedule = await vesting.lastStakingSchedule(); - remainingStakeAmount = await vesting.remainingStakeAmount(); - await expectRevert(vesting.stakeTokens(remainingStakeAmount + 100, lastStakingSchedule), "invalid params"); - await expectRevert(vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule + 100), "invalid params"); - }); - - it("staking schedule must run for max duration", async () => { - vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, root, feeSharingProxy.address); - vesting = await VestingLogic.at(vesting.address); - await token.approve(vesting.address, ONE_MILLON); - - let remainingStakeAmount = ONE_MILLON; - let lastStakingSchedule = 0; - while (remainingStakeAmount > 0) { - await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); - lastStakingSchedule = await vesting.lastStakingSchedule(); - remainingStakeAmount = await vesting.remainingStakeAmount(); - } - - let data = await staking.getStakes.call(vesting.address); - assert.equal(data.dates.length, 39); - assert.equal(data.stakes.length, 39); - expect(data.stakes[0]).to.be.bignumber.equal(data.stakes[15]); - dates0 = data.dates[0]; - dates5 = data.dates[5]; - }); - - it("should extend duration of first 5 staking periods", async () => { - await increaseTime(20 * WEEK); - tx = await vesting.extendStaking(); - data = await staking.getStakes.call(vesting.address); - expect(data.stakes[0]).to.be.bignumber.equal(data.stakes[15]); - expect(dates0).to.be.bignumber.not.equal(data.dates[0]); - expect(dates5).to.be.bignumber.equal(data.dates[0]); - dates0 = data.dates[0]; - dates5 = data.dates[5]; - }); - - it("should extend duration of next 5 staking periods", async () => { - await increaseTime(20 * WEEK); - tx = await vesting.extendStaking(); - data = await staking.getStakes.call(vesting.address); - expect(data.stakes[0]).to.be.bignumber.equal(data.stakes[15]); - expect(dates0).to.be.bignumber.not.equal(data.dates[0]); - expect(dates5).to.be.bignumber.equal(data.dates[0]); - dates0 = data.dates[0]; - dates3 = data.dates[3]; - }); - - it("should extend duration of next 3 staking periods only", async () => { - await increaseTime(20 * WEEK); - tx = await vesting.extendStaking(); - data = await staking.getStakes.call(vesting.address); - expect(data.stakes[0]).to.be.bignumber.equal(data.stakes[15]); - expect(dates0).to.be.bignumber.not.equal(data.dates[0]); - expect(dates3).to.be.bignumber.equal(data.dates[0]); - }); - - it("should not withdraw unlocked tokens if receiver address is 0", async () => { - // withdraw - await expectRevert(vesting.withdrawTokens(constants.ZERO_ADDRESS), "receiver address invalid"); - }); - - it("should withdraw unlocked tokens for four year vesting after first year", async () => { - // time travel - await increaseTime(104 * WEEK); - - // withdraw - tx = await vesting.withdrawTokens(root); - - // check event - expectEvent(tx, "TokensWithdrawn", { - caller: root, - receiver: root, - }); - }); - }); - - describe("setMaxInterval", async () => { - it("should set/alter maxInterval", async () => { - vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, a2, feeSharingProxy.address); - vesting = await VestingLogic.at(vesting.address); - let maxIntervalOld = await vesting.maxInterval(); - await vesting.setMaxInterval(60 * WEEK); - let maxIntervalNew = await vesting.maxInterval(); - expect(maxIntervalOld).to.be.bignumber.not.equal(maxIntervalNew); - }); - - it("should not set/alter maxInterval", async () => { - vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, a2, feeSharingProxy.address); - vesting = await VestingLogic.at(vesting.address); - await expectRevert(vesting.setMaxInterval(7 * WEEK), "invalid interval"); - }); - }); - - describe("extend duration and delegate", async () => { - it("must delegate for all intervals", async () => { - vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, root, feeSharingProxy.address); - vesting = await VestingLogic.at(vesting.address); - await token.approve(vesting.address, ONE_MILLON); - - let remainingStakeAmount = ONE_MILLON; - let lastStakingSchedule = 0; - while (remainingStakeAmount > 0) { - await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); - lastStakingSchedule = await vesting.lastStakingSchedule(); - remainingStakeAmount = await vesting.remainingStakeAmount(); - } - - let data = await staking.getStakes.call(vesting.address); - for (let i = 0; i < data.dates.length; i++) { - let delegatee = await staking.delegates(vesting.address, data.dates[i]); - expect(delegatee).equal(root); - } - - await increaseTime(80 * WEEK); - let tx = await vesting.extendStaking(); - // delegate - tx = await vesting.delegate(a1); - console.log("gasUsed: " + tx.receipt.gasUsed); - expectEvent(tx, "VotesDelegated", { - caller: root, - delegatee: a1, - }); - data = await staking.getStakes.call(vesting.address); - for (let i = 0; i < data.dates.length; i++) { - let delegatee = await staking.delegates(vesting.address, data.dates[i]); - expect(delegatee).equal(a1); - } - }); - }); - - describe("changeTokenOwner", async () => { - let vesting; - it("should not change token owner if vesting owner didn't approve", async () => { - vesting = await Vesting.new(vestingLogic.address, token.address, staking.address, a1, feeSharingProxy.address); - vesting = await VestingLogic.at(vesting.address); - await expectRevert(vesting.changeTokenOwner(a2, { from: a1 }), "unauthorized"); - }); - - it("changeTokenOwner should revert if address is zero", async () => { - await expectRevert(vesting.changeTokenOwner(constants.ZERO_ADDRESS), "invalid new token owner address"); - }); - - it("changeTokenOwner should revert if new owner is the same owner", async () => { - await expectRevert(vesting.changeTokenOwner(a1), "same owner not allowed"); - }); - - it("approveOwnershipTransfer should revert if new token address is zero", async () => { - await expectRevert(vesting.approveOwnershipTransfer({ from: a1 }), "invalid address"); - }); - - it("should not change token owner if token owner hasn't approved", async () => { - await vesting.changeTokenOwner(a2, { from: root }); - let newTokenOwner = await vesting.tokenOwner(); - expect(newTokenOwner).to.be.not.equal(a2); - }); - - it("approveOwnershipTransfer should revert if not signed by vesting owner", async () => { - await expectRevert(vesting.approveOwnershipTransfer({ from: a2 }), "unauthorized"); - }); - - it("should be able to change token owner", async () => { - let tx = await vesting.approveOwnershipTransfer({ from: a1 }); - // check event - expectEvent(tx, "TokenOwnerChanged", { - newOwner: a2, - oldOwner: a1, - }); - let newTokenOwner = await vesting.tokenOwner(); - assert.equal(newTokenOwner, a2); - }); - }); - - describe("setImplementation", async () => { - let vesting, newVestingLogic, vestingObject; - const NewVestingLogic = artifacts.require("MockFourYearVestingLogic"); - it("should not change implementation if token owner didn't sign", async () => { - vestingObject = await Vesting.new(vestingLogic.address, token.address, staking.address, a1, feeSharingProxy.address); - vesting = await VestingLogic.at(vestingObject.address); - newVestingLogic = await NewVestingLogic.new(); - await expectRevert(vesting.setImpl(newVestingLogic.address, { from: a3 }), "unauthorized"); - await expectRevert(vesting.setImpl(newVestingLogic.address), "unauthorized"); - await expectRevert(vesting.setImpl(constants.ZERO_ADDRESS, { from: a1 }), "invalid new implementation address"); - }); - - it("should not change implementation if still unauthorized by vesting owner", async () => { - await vesting.setImpl(newVestingLogic.address, { from: a1 }); - let newImplementation = await vestingObject.getImplementation(); - expect(newImplementation).to.not.equal(newVestingLogic.address); - }); - - it("setImplementation should revert if not signed by vesting owner", async () => { - await expectRevert(vestingObject.setImplementation(newVestingLogic.address, { from: a1 }), "Proxy:: access denied"); - }); - - it("setImplementation should revert if logic address is not a contract", async () => { - await expectRevert(vestingObject.setImplementation(a3, { from: root }), "_implementation not a contract"); - }); - - it("setImplementation should revert if address mismatch", async () => { - await expectRevert(vestingObject.setImplementation(vestingLogic.address, { from: root }), "address mismatch"); - }); - - it("should be able to change implementation", async () => { - await vestingObject.setImplementation(newVestingLogic.address); - vesting = await NewVestingLogic.at(vesting.address); - - let durationLeft = await vesting.getDurationLeft(); - await token.approve(vesting.address, ONE_MILLON); - await vesting.stakeTokens(ONE_MILLON, 0); - let durationLeftNew = await vesting.getDurationLeft(); - expect(durationLeft).to.be.bignumber.not.equal(durationLeftNew); - }); - }); + let root, a1, a2, a3; + let token, staking, stakingLogic, feeSharingProxy; + let vestingLogic; + let vestingFactory; + let kickoffTS; + + let cliff = 4 * WEEK; + let duration = 156 * WEEK; + + before(async () => { + [root, a1, a2, a3, ...accounts] = accounts; + token = await SOV.new(TOTAL_SUPPLY); + wrbtc = await TestWrbtc.new(); + + vestingLogic = await VestingLogic.new(); + vestingFactory = await VestingFactory.new(); + + feeSharingProxy = await FeeSharingProxy.new( + constants.ZERO_ADDRESS, + constants.ZERO_ADDRESS + ); + + stakingLogic = await StakingLogic.new(token.address); + staking = await StakingProxy.new(token.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); + //Upgradable Vesting Registry + vestingRegistryLogic = await VestingRegistryLogic.new(); + vestingReg = await VestingRegistryProxy.new(); + await vestingReg.setImplementation(vestingRegistryLogic.address); + vestingReg = await VestingRegistryLogic.at(vestingReg.address); + await staking.setVestingRegistry(vestingReg.address); + + await token.transfer(a2, "1000"); + await token.approve(staking.address, "1000", { from: a2 }); + + kickoffTS = await staking.kickoffTS.call(); + }); + + describe("vestingfactory", () => { + it("sets the expected values", async () => { + let vestingInstance = await vestingFactory.deployFourYearVesting( + token.address, + staking.address, + a1, + feeSharingProxy.address, + root, + vestingLogic.address + ); + vestingInstance = await VestingLogic.at(vestingInstance.logs[0].address); + + // Check data + let _sov = await vestingInstance.SOV(); + let _stackingAddress = await vestingInstance.staking(); + let _tokenOwner = await vestingInstance.tokenOwner(); + let _cliff = await vestingInstance.CLIFF(); + let _duration = await vestingInstance.DURATION(); + let _feeSharingProxy = await vestingInstance.feeSharingProxy(); + + assert.equal(_sov, token.address); + assert.equal(_stackingAddress, staking.address); + assert.equal(_tokenOwner, a1); + assert.equal(_cliff.toString(), cliff); + assert.equal(_duration.toString(), duration); + assert.equal(_feeSharingProxy, feeSharingProxy.address); + }); + }); + + describe("constructor", () => { + it("sets the expected values", async () => { + let vestingInstance = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + feeSharingProxy.address + ); + vestingInstance = await VestingLogic.at(vestingInstance.address); + + // Check data + let _sov = await vestingInstance.SOV(); + let _stackingAddress = await vestingInstance.staking(); + let _tokenOwner = await vestingInstance.tokenOwner(); + let _cliff = await vestingInstance.CLIFF(); + let _duration = await vestingInstance.DURATION(); + let _feeSharingProxy = await vestingInstance.feeSharingProxy(); + + assert.equal(_sov, token.address); + assert.equal(_stackingAddress, staking.address); + assert.equal(_tokenOwner, root); + assert.equal(_cliff.toString(), cliff); + assert.equal(_duration.toString(), duration); + assert.equal(_feeSharingProxy, feeSharingProxy.address); + }); + + it("fails if the 0 address is passed as SOV address", async () => { + await expectRevert( + Vesting.new( + vestingLogic.address, + constants.ZERO_ADDRESS, + staking.address, + root, + feeSharingProxy.address + ), + "SOV address invalid" + ); + }); + + it("fails if the 0 address is passed as token owner address", async () => { + await expectRevert( + Vesting.new( + vestingLogic.address, + token.address, + staking.address, + constants.ZERO_ADDRESS, + feeSharingProxy.address + ), + "token owner address invalid" + ); + }); + + it("fails if the 0 address is passed as staking address", async () => { + await expectRevert( + Vesting.new( + vestingLogic.address, + token.address, + constants.ZERO_ADDRESS, + root, + feeSharingProxy.address + ), + "staking address invalid" + ); + }); + + it("fails if the 0 address is passed as feeSharingProxy address", async () => { + await expectRevert( + Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + constants.ZERO_ADDRESS + ), + "feeSharingProxy address invalid" + ); + }); + + it("fails if logic is not a contract address", async () => { + await expectRevert( + Vesting.new(a1, token.address, staking.address, a1, feeSharingProxy.address), + "_logic not a contract" + ); + }); + + it("fails if SOV is not a contract address", async () => { + await expectRevert( + Vesting.new( + vestingLogic.address, + a1, + staking.address, + a1, + feeSharingProxy.address + ), + "_SOV not a contract" + ); + }); + + it("fails if staking address is not a contract address", async () => { + await expectRevert( + Vesting.new(vestingLogic.address, token.address, a1, a1, feeSharingProxy.address), + "_stakingAddress not a contract" + ); + }); + + it("fails if fee sharing is not a contract address", async () => { + await expectRevert( + Vesting.new(vestingLogic.address, token.address, staking.address, a1, a1), + "_feeSharingProxy not a contract" + ); + }); + }); + + describe("delegate", () => { + let vesting; + it("should stake tokens and delegate voting power", async () => { + let toStake = ONE_MILLON; + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + a2, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await token.approve(vesting.address, toStake); + let remainingStakeAmount = ONE_MILLON; + let lastStakingSchedule = 0; + while (remainingStakeAmount > 0) { + await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + } + + // check delegatee + let data = await staking.getStakes.call(vesting.address); + /// @dev Optimization: This loop through 40 steps is a bottleneck + for (let i = 0; i < data.dates.length; i++) { + let delegatee = await staking.delegates(vesting.address, data.dates[i]); + expect(delegatee).equal(a2); + } + + // delegate + let tx = await vesting.delegate(a1, { from: a2 }); + + expectEvent(tx, "VotesDelegated", { + caller: a2, + delegatee: a1, + }); + + // check new delegatee + data = await staking.getStakes.call(vesting.address); + /// @dev Optimization: This loop through 40 steps is a bottleneck + for (let i = 0; i < data.dates.length; i++) { + let delegatee = await staking.delegates(vesting.address, data.dates[i]); + expect(delegatee).equal(a1); + } + }); + + it("fails if delegatee is zero address", async () => { + await expectRevert( + vesting.delegate(constants.ZERO_ADDRESS, { from: a2 }), + "delegatee address invalid" + ); + }); + + it("fails if not a token owner", async () => { + await expectRevert(vesting.delegate(a1, { from: a1 }), "unauthorized"); + }); + }); + + describe("stakeTokens; using Ganache", () => { + // Check random scenarios + let vesting; + it("should stake 1,000,000 SOV with a duration of 156 weeks and a 4 week cliff", async () => { + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + await token.approve(vesting.address, ONE_MILLON); + let remainingStakeAmount = ONE_MILLON; + let lastStakingSchedule = 0; + while (remainingStakeAmount > 0) { + let tx = await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); + expectEvent(tx, "TokensStaked"); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + } + + // check delegatee + let data = await staking.getStakes.call(vesting.address); + for (let i = 0; i < data.dates.length; i++) { + let delegatee = await staking.delegates(vesting.address, data.dates[i]); + expect(delegatee).equal(root); + } + }); + + it("should stake 1,000,000 SOV with a duration of 156 weeks and a 4 week cliff", async () => { + let block = await lastBlock(); + let timestamp = parseInt(block.timestamp); + + let kickoffTS = await staking.kickoffTS(); + + let start = timestamp + 4 * WEEK; + let end = timestamp + 156 * WEEK; + + let numIntervals = Math.floor((end - start) / (4 * WEEK)) + 1; + let stakedPerInterval = ONE_MILLON / numIntervals; + + // positive case + for (let i = start; i <= end; i += 4 * WEEK) { + let periodFromKickoff = Math.floor((i - kickoffTS.toNumber()) / (2 * WEEK)); + let startBuf = periodFromKickoff * 2 * WEEK + kickoffTS.toNumber(); + let userStakingCheckpoints = await staking.userStakingCheckpoints( + vesting.address, + startBuf, + 0 + ); + + assert.equal(userStakingCheckpoints.stake.toString(), stakedPerInterval); + + let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints( + vesting.address, + startBuf + ); + assert.equal(numUserStakingCheckpoints.toString(), "1"); + } + + // negative cases + + // start-10 to avoid coming to active checkpoint + let periodFromKickoff = Math.floor((start - 10 - kickoffTS.toNumber()) / (2 * WEEK)); + let startBuf = periodFromKickoff * 2 * WEEK + kickoffTS.toNumber(); + let userStakingCheckpoints = await staking.userStakingCheckpoints( + vesting.address, + startBuf, + 0 + ); + + assert.equal(userStakingCheckpoints.fromBlock.toNumber(), 0); + assert.equal(userStakingCheckpoints.stake.toString(), 0); + + let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints( + vesting.address, + startBuf + ); + assert.equal(numUserStakingCheckpoints.toString(), "0"); + periodFromKickoff = Math.floor((end + 3 * WEEK - kickoffTS.toNumber()) / (2 * WEEK)); + startBuf = periodFromKickoff * 2 * WEEK + kickoffTS.toNumber(); + userStakingCheckpoints = await staking.userStakingCheckpoints( + vesting.address, + startBuf, + 0 + ); + + assert.equal(userStakingCheckpoints.fromBlock.toNumber(), 0); + assert.equal(userStakingCheckpoints.stake.toString(), 0); + + numUserStakingCheckpoints = await staking.numUserStakingCheckpoints( + vesting.address, + startBuf + ); + assert.equal(numUserStakingCheckpoints.toString(), "0"); + }); + + it("should not allow to stake 2 times 1,000,000 SOV with a duration of 156 weeks and a 4 week cliff", async () => { + let amount = ONE_MILLON; + let cliff = 4 * WEEK; + let duration = 156 * WEEK; + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await token.approve(vesting.address, amount); + let remainingStakeAmount = amount; + let lastStakingSchedule = 0; + while (remainingStakeAmount > 0) { + await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + } + + await increaseTime(52 * WEEK); + await token.approve(vesting.address, amount); + await expectRevert(vesting.stakeTokens(amount, 0), "create new vesting address"); + }); + }); + + describe("stakeTokensWithApproval", () => { + let vesting; + + it("fails if invoked directly", async () => { + let amount = 1000; + let cliff = 4 * WEEK; + let duration = 39 * 4 * WEEK; + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + await expectRevert(vesting.stakeTokensWithApproval(root, amount, 0), "unauthorized"); + }); + + it("fails if pass wrong method in data", async () => { + let amount = 1000; + let cliff = 4 * WEEK; + let duration = 39 * 4 * WEEK; + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + let contract = new web3.eth.Contract(vesting.abi, vesting.address); + let sender = root; + let data = contract.methods.stakeTokens(amount, 0).encodeABI(); + + await expectRevert( + token.approveAndCall(vesting.address, amount, data, { from: sender }), + "method is not allowed" + ); + }); + + it("should stake ONE MILLION tokens with a duration of 156 weeks and a 4 week cliff", async () => { + let amount = ONE_MILLON; + let cliff = 4 * WEEK; + let duration = 39 * 4 * WEEK; + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + let contract = new web3.eth.Contract(vesting.abi, vesting.address); + let sender = root; + let data = contract.methods.stakeTokensWithApproval(sender, amount, 0).encodeABI(); + let tx = await token.approveAndCall(vesting.address, amount, data, { from: sender }); + let lastStakingSchedule = await vesting.lastStakingSchedule(); + let remainingStakeAmount = await vesting.remainingStakeAmount(); + + data = contract.methods + .stakeTokensWithApproval(sender, remainingStakeAmount, lastStakingSchedule) + .encodeABI(); + await token.approveAndCall(vesting.address, remainingStakeAmount, data, { + from: sender, + }); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + + data = contract.methods + .stakeTokensWithApproval(sender, remainingStakeAmount, lastStakingSchedule) + .encodeABI(); + await token.approveAndCall(vesting.address, remainingStakeAmount, data, { + from: sender, + }); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + assert.equal(remainingStakeAmount, 0); + }); + + it("should stake 39000 tokens with a duration of 156 weeks and a 4 week cliff", async () => { + let amount = 39000; + let cliff = 4 * WEEK; + let duration = 156 * WEEK; + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + let contract = new web3.eth.Contract(vesting.abi, vesting.address); + let sender = root; + let data = contract.methods.stakeTokensWithApproval(sender, amount, 0).encodeABI(); + await token.approveAndCall(vesting.address, amount, data, { from: sender }); + let lastStakingSchedule = await vesting.lastStakingSchedule(); + let remainingStakeAmount = await vesting.remainingStakeAmount(); + + data = contract.methods + .stakeTokensWithApproval(sender, remainingStakeAmount, lastStakingSchedule) + .encodeABI(); + await token.approveAndCall(vesting.address, remainingStakeAmount, data, { + from: sender, + }); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + + data = contract.methods + .stakeTokensWithApproval(sender, remainingStakeAmount, lastStakingSchedule) + .encodeABI(); + await token.approveAndCall(vesting.address, remainingStakeAmount, data, { + from: sender, + }); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + assert.equal(remainingStakeAmount, 0); + + let block = await web3.eth.getBlock("latest"); + let timestamp = block.timestamp; + + let start = timestamp + cliff; + let end = timestamp + duration; + + let numIntervals = Math.floor((end - start) / (4 * WEEK)) + 1; + let stakedPerInterval = Math.floor(amount / numIntervals); + + // positive case + for (let i = start; i <= end; i += 4 * WEEK) { + let periodFromKickoff = Math.floor((i - kickoffTS.toNumber()) / (2 * WEEK)); + let startBuf = periodFromKickoff * 2 * WEEK + kickoffTS.toNumber(); + let userStakingCheckpoints = await staking.userStakingCheckpoints( + vesting.address, + startBuf, + 0 + ); + + assert.equal(userStakingCheckpoints.stake.toString(), stakedPerInterval); + + let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints( + vesting.address, + startBuf + ); + assert.equal(numUserStakingCheckpoints.toString(), "1"); + } + }); + }); + + describe("withdrawTokens", () => { + let vesting; + it("should withdraw unlocked tokens", async () => { + // Save current amount + let previousAmount = await token.balanceOf(root); + let toStake = ONE_MILLON; + + // Stake + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await token.approve(vesting.address, toStake); + let remainingStakeAmount = ONE_MILLON; + let lastStakingSchedule = 0; + while (remainingStakeAmount > 0) { + await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + } + + let amountAfterStake = await token.balanceOf(root); + + // time travel + await increaseTime(104 * WEEK); + + // withdraw + tx = await vesting.withdrawTokens(root); + + // check event + expectEvent(tx, "TokensWithdrawn", { + caller: root, + receiver: root, + }); + + // verify amount + let amount = await token.balanceOf(root); + + assert.equal( + previousAmount.sub(new BN(toStake)).toString(), + amountAfterStake.toString() + ); + expect(previousAmount).to.be.bignumber.greaterThan(amount); + expect(amount).to.be.bignumber.greaterThan(amountAfterStake); + }); + + it("should not withdraw unlocked tokens in the first year", async () => { + // Save current amount + let previousAmount = await token.balanceOf(root); + let toStake = ONE_MILLON; + + // Stake + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await token.approve(vesting.address, toStake); + let remainingStakeAmount = ONE_MILLON; + let lastStakingSchedule = 0; + while (remainingStakeAmount > 0) { + await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + } + + let amountAfterStake = await token.balanceOf(root); + + // time travel + await increaseTime(34 * WEEK); + + // withdraw + tx = await vesting.withdrawTokens(root); + + // verify amount + let amount = await token.balanceOf(root); + + assert.equal( + previousAmount.sub(new BN(toStake)).toString(), + amountAfterStake.toString() + ); + expect(previousAmount).to.be.bignumber.greaterThan(amount); + assert.equal(amountAfterStake.toString(), amount.toString()); + }); + + it("should not allow for 2 stakes and withdrawal for the first year", async () => { + // Save current amount + let previousAmount = await token.balanceOf(root); + let toStake = ONE_ETHER; + + // Stake + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await token.approve(vesting.address, toStake); + await vesting.stakeTokens(toStake, 0); + let amountAfterStake = await token.balanceOf(root); + + // time travel + await increaseTime(20 * WEEK); + await token.approve(vesting.address, toStake); + await expectRevert(vesting.stakeTokens(toStake, 0), "create new vesting address"); + + // withdraw + tx = await vesting.withdrawTokens(root); + + // verify amount + let amount = await token.balanceOf(root); + + expect(previousAmount).to.be.bignumber.greaterThan(amount); + assert.equal(amountAfterStake.toString(), amount.toString()); + }); + + it("should do nothing if withdrawing a second time", async () => { + let amountOld = await token.balanceOf(root); + // withdraw + tx = await vesting.withdrawTokens(root); + + // verify amount + let amount = await token.balanceOf(root); + assert.equal(amountOld.toString(), amount.toString()); + }); + + it("should do nothing if withdrawing before reaching the cliff", async () => { + let toStake = ONE_MILLON; + + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + a1, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await token.approve(vesting.address, toStake); + let remainingStakeAmount = ONE_MILLON; + let lastStakingSchedule = 0; + while (remainingStakeAmount > 0) { + await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + } + let amountOld = await token.balanceOf(root); + + // time travel + await increaseTime(2 * WEEK); + + // withdraw + tx = await vesting.withdrawTokens(a2, { from: a1 }); + + // verify amount + let amount = await token.balanceOf(root); + assert.equal(amountOld.toString(), amount.toString()); + }); + + it("should fail if the caller is not token owner", async () => { + await expectRevert(vesting.withdrawTokens(root, { from: a2 }), "unauthorized"); + await expectRevert(vesting.withdrawTokens(root, { from: a3 }), "unauthorized"); + + await expectRevert(vesting.withdrawTokens(root, { from: root }), "unauthorized"); + await increaseTime(30 * WEEK); + await expectRevert(vesting.withdrawTokens(root, { from: a2 }), "unauthorized"); + }); + + it("shouldn't be possible to use governanceWithdrawVesting by anyone but owner", async () => { + let toStake = ONE_MILLON; + + // Stake + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await token.approve(vesting.address, toStake); + let remainingStakeAmount = ONE_MILLON; + let lastStakingSchedule = 0; + while (remainingStakeAmount > 0) { + await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + } + + await expectRevert( + staking.governanceWithdrawVesting(vesting.address, root, { from: a1 }), + "unauthorized" + ); + }); + + it("shouldn't be possible to use governanceWithdraw by user", async () => { + await expectRevert( + staking.governanceWithdraw(100, kickoffTS.toNumber() + 52 * WEEK, root), + "unauthorized" + ); + }); + }); + + describe("collectDividends", async () => { + let vesting; + it("should fail if the caller is neither owner nor token owner", async () => { + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + a1, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + await expectRevert( + vesting.collectDividends(root, 10, a1, { from: a2 }), + "unauthorized" + ); + await expectRevert( + vesting.collectDividends(root, 10, a1, { from: a3 }), + "unauthorized" + ); + }); + + it("should fail if receiver address is invalid", async () => { + let maxCheckpoints = new BN(10); + await expectRevert( + vesting.collectDividends(a1, maxCheckpoints, constants.ZERO_ADDRESS, { from: a1 }), + "receiver address invalid" + ); + }); + + it("should collect dividends", async () => { + let maxCheckpoints = new BN(10); + await expectRevert(vesting.collectDividends(a1, maxCheckpoints, a2), "unauthorized"); + let tx = await vesting.collectDividends(a1, maxCheckpoints, a2, { from: a1 }); + + let testData = await feeSharingProxy.testData.call(); + expect(testData.loanPoolToken).to.be.equal(a1); + expect(testData.maxCheckpoints).to.be.bignumber.equal(maxCheckpoints); + expect(testData.receiver).to.be.equal(a2); + + expectEvent(tx, "DividendsCollected", { + caller: a1, + loanPoolToken: a1, + receiver: a2, + maxCheckpoints: maxCheckpoints, + }); + }); + }); + + describe("migrateToNewStakingContract", async () => { + let vesting; + it("should set the new staking contract", async () => { + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + a1, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + // 1. set new staking contract address on staking contract + + let newStaking = await StakingProxy.new(token.address); + await newStaking.setImplementation(stakingLogic.address); + newStaking = await StakingLogic.at(newStaking.address); + + await staking.setNewStakingContract(newStaking.address); + + // 2. call migrateToNewStakingContract + let tx = await vesting.migrateToNewStakingContract(); + expectEvent(tx, "MigratedToNewStakingContract", { + caller: root, + newStakingContract: newStaking.address, + }); + let _staking = await vesting.staking(); + assert.equal(_staking, newStaking.address); + }); + + it("should fail if there is no new staking contract set", async () => { + let newStaking = await StakingProxy.new(token.address); + await newStaking.setImplementation(stakingLogic.address); + newStaking = await StakingLogic.at(newStaking.address); + + vesting = await Vesting.new( + vestingLogic.address, + token.address, + newStaking.address, + a1, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + await expectRevert( + vesting.migrateToNewStakingContract(), + "there is no new staking contract set" + ); + }); + + it("should fail if the caller is neither owner nor token owner", async () => { + let newStaking = await StakingProxy.new(token.address); + await newStaking.setImplementation(stakingLogic.address); + newStaking = await StakingLogic.at(newStaking.address); + + vesting = await Vesting.new( + vestingLogic.address, + token.address, + newStaking.address, + a1, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await newStaking.setNewStakingContract(newStaking.address); + + await expectRevert(vesting.migrateToNewStakingContract({ from: a2 }), "unauthorized"); + await expectRevert(vesting.migrateToNewStakingContract({ from: a3 }), "unauthorized"); + + await vesting.migrateToNewStakingContract(); + await vesting.migrateToNewStakingContract({ from: a1 }); + }); + }); + + describe("fouryearvesting", async () => { + let vesting, dates0, dates3, dates5; + it("staking schedule must fail if sufficient tokens aren't approved", async () => { + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + await token.approve(vesting.address, 1000); + await expectRevert( + vesting.stakeTokens(ONE_MILLON, 0), + "transfer amount exceeds allowance" + ); + }); + + it("staking schedule must fail for incorrect parameters", async () => { + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + await token.approve(vesting.address, ONE_MILLON); + + let remainingStakeAmount = ONE_MILLON; + let lastStakingSchedule = 0; + await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + await expectRevert( + vesting.stakeTokens(remainingStakeAmount + 100, lastStakingSchedule), + "invalid params" + ); + await expectRevert( + vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule + 100), + "invalid params" + ); + }); + + it("staking schedule must run for max duration", async () => { + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + await token.approve(vesting.address, ONE_MILLON); + + let remainingStakeAmount = ONE_MILLON; + let lastStakingSchedule = 0; + while (remainingStakeAmount > 0) { + await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + } + + let data = await staking.getStakes.call(vesting.address); + assert.equal(data.dates.length, 39); + assert.equal(data.stakes.length, 39); + expect(data.stakes[0]).to.be.bignumber.equal(data.stakes[15]); + dates0 = data.dates[0]; + dates5 = data.dates[5]; + }); + + it("should extend duration of first 5 staking periods", async () => { + await increaseTime(20 * WEEK); + tx = await vesting.extendStaking(); + data = await staking.getStakes.call(vesting.address); + expect(data.stakes[0]).to.be.bignumber.equal(data.stakes[15]); + expect(dates0).to.be.bignumber.not.equal(data.dates[0]); + expect(dates5).to.be.bignumber.equal(data.dates[0]); + dates0 = data.dates[0]; + dates5 = data.dates[5]; + }); + + it("should extend duration of next 5 staking periods", async () => { + await increaseTime(20 * WEEK); + tx = await vesting.extendStaking(); + data = await staking.getStakes.call(vesting.address); + expect(data.stakes[0]).to.be.bignumber.equal(data.stakes[15]); + expect(dates0).to.be.bignumber.not.equal(data.dates[0]); + expect(dates5).to.be.bignumber.equal(data.dates[0]); + dates0 = data.dates[0]; + dates3 = data.dates[3]; + }); + + it("should extend duration of next 3 staking periods only", async () => { + await increaseTime(20 * WEEK); + tx = await vesting.extendStaking(); + data = await staking.getStakes.call(vesting.address); + expect(data.stakes[0]).to.be.bignumber.equal(data.stakes[15]); + expect(dates0).to.be.bignumber.not.equal(data.dates[0]); + expect(dates3).to.be.bignumber.equal(data.dates[0]); + }); + + it("should not withdraw unlocked tokens if receiver address is 0", async () => { + // withdraw + await expectRevert( + vesting.withdrawTokens(constants.ZERO_ADDRESS), + "receiver address invalid" + ); + }); + + it("should withdraw unlocked tokens for four year vesting after first year", async () => { + // time travel + await increaseTime(104 * WEEK); + + // withdraw + tx = await vesting.withdrawTokens(root); + + // check event + expectEvent(tx, "TokensWithdrawn", { + caller: root, + receiver: root, + }); + }); + }); + + describe("setMaxInterval", async () => { + it("should set/alter maxInterval", async () => { + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + a2, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + let maxIntervalOld = await vesting.maxInterval(); + await vesting.setMaxInterval(60 * WEEK); + let maxIntervalNew = await vesting.maxInterval(); + expect(maxIntervalOld).to.be.bignumber.not.equal(maxIntervalNew); + }); + + it("should not set/alter maxInterval", async () => { + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + a2, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + await expectRevert(vesting.setMaxInterval(7 * WEEK), "invalid interval"); + }); + }); + + describe("extend duration and delegate", async () => { + it("must delegate for all intervals", async () => { + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + await token.approve(vesting.address, ONE_MILLON); + + let remainingStakeAmount = ONE_MILLON; + let lastStakingSchedule = 0; + while (remainingStakeAmount > 0) { + await vesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); + lastStakingSchedule = await vesting.lastStakingSchedule(); + remainingStakeAmount = await vesting.remainingStakeAmount(); + } + + let data = await staking.getStakes.call(vesting.address); + for (let i = 0; i < data.dates.length; i++) { + let delegatee = await staking.delegates(vesting.address, data.dates[i]); + expect(delegatee).equal(root); + } + + await increaseTime(80 * WEEK); + let tx = await vesting.extendStaking(); + // delegate + tx = await vesting.delegate(a1); + console.log("gasUsed: " + tx.receipt.gasUsed); + expectEvent(tx, "VotesDelegated", { + caller: root, + delegatee: a1, + }); + data = await staking.getStakes.call(vesting.address); + for (let i = 0; i < data.dates.length; i++) { + let delegatee = await staking.delegates(vesting.address, data.dates[i]); + expect(delegatee).equal(a1); + } + }); + }); + + describe("changeTokenOwner", async () => { + let vesting; + it("should not change token owner if vesting owner didn't approve", async () => { + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + a1, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + await expectRevert(vesting.changeTokenOwner(a2, { from: a1 }), "unauthorized"); + }); + + it("changeTokenOwner should revert if address is zero", async () => { + await expectRevert( + vesting.changeTokenOwner(constants.ZERO_ADDRESS), + "invalid new token owner address" + ); + }); + + it("changeTokenOwner should revert if new owner is the same owner", async () => { + await expectRevert(vesting.changeTokenOwner(a1), "same owner not allowed"); + }); + + it("approveOwnershipTransfer should revert if new token address is zero", async () => { + await expectRevert(vesting.approveOwnershipTransfer({ from: a1 }), "invalid address"); + }); + + it("should not change token owner if token owner hasn't approved", async () => { + await vesting.changeTokenOwner(a2, { from: root }); + let newTokenOwner = await vesting.tokenOwner(); + expect(newTokenOwner).to.be.not.equal(a2); + }); + + it("approveOwnershipTransfer should revert if not signed by vesting owner", async () => { + await expectRevert(vesting.approveOwnershipTransfer({ from: a2 }), "unauthorized"); + }); + + it("should be able to change token owner", async () => { + let tx = await vesting.approveOwnershipTransfer({ from: a1 }); + // check event + expectEvent(tx, "TokenOwnerChanged", { + newOwner: a2, + oldOwner: a1, + }); + let newTokenOwner = await vesting.tokenOwner(); + assert.equal(newTokenOwner, a2); + }); + }); + + describe("setImplementation", async () => { + let vesting, newVestingLogic, vestingObject; + const NewVestingLogic = artifacts.require("MockFourYearVestingLogic"); + it("should not change implementation if token owner didn't sign", async () => { + vestingObject = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + a1, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vestingObject.address); + newVestingLogic = await NewVestingLogic.new(); + await expectRevert( + vesting.setImpl(newVestingLogic.address, { from: a3 }), + "unauthorized" + ); + await expectRevert(vesting.setImpl(newVestingLogic.address), "unauthorized"); + await expectRevert( + vesting.setImpl(constants.ZERO_ADDRESS, { from: a1 }), + "invalid new implementation address" + ); + }); + + it("should not change implementation if still unauthorized by vesting owner", async () => { + await vesting.setImpl(newVestingLogic.address, { from: a1 }); + let newImplementation = await vestingObject.getImplementation(); + expect(newImplementation).to.not.equal(newVestingLogic.address); + }); + + it("setImplementation should revert if not signed by vesting owner", async () => { + await expectRevert( + vestingObject.setImplementation(newVestingLogic.address, { from: a1 }), + "Proxy:: access denied" + ); + }); + + it("setImplementation should revert if logic address is not a contract", async () => { + await expectRevert( + vestingObject.setImplementation(a3, { from: root }), + "_implementation not a contract" + ); + }); + + it("setImplementation should revert if address mismatch", async () => { + await expectRevert( + vestingObject.setImplementation(vestingLogic.address, { from: root }), + "address mismatch" + ); + }); + + it("should be able to change implementation", async () => { + await vestingObject.setImplementation(newVestingLogic.address); + vesting = await NewVestingLogic.at(vesting.address); + + let durationLeft = await vesting.getDurationLeft(); + await token.approve(vesting.address, ONE_MILLON); + await vesting.stakeTokens(ONE_MILLON, 0); + let durationLeftNew = await vesting.getDurationLeft(); + expect(durationLeft).to.be.bignumber.not.equal(durationLeftNew); + }); + }); }); diff --git a/tests/vesting/GenericTokenSender.js b/tests/vesting/GenericTokenSender.js index f4046d8df..297304f42 100644 --- a/tests/vesting/GenericTokenSender.js +++ b/tests/vesting/GenericTokenSender.js @@ -1,5 +1,12 @@ const { expect } = require("chai"); -const { expectRevert, expectEvent, constants, BN, balance, time } = require("@openzeppelin/test-helpers"); +const { + expectRevert, + expectEvent, + constants, + BN, + balance, + time, +} = require("@openzeppelin/test-helpers"); const SOV_ABI = artifacts.require("SOV"); const GenericTokenSender = artifacts.require("GenericTokenSender"); @@ -8,152 +15,188 @@ const TOTAL_SUPPLY = "100000000000000000000000000"; const ZERO_ADDRESS = constants.ZERO_ADDRESS; contract("GenericTokenSender", (accounts) => { - let root, account1, account2, account3; - let SOV, tokenSender; - - before(async () => { - [root, account1, account2, account3, ...accounts] = accounts; - }); - - beforeEach(async () => { - SOV = await SOV_ABI.new(TOTAL_SUPPLY); - tokenSender = await GenericTokenSender.new(); - }); - - describe("addAdmin", () => { - it("adds admin", async () => { - let tx = await tokenSender.addAdmin(account1); - - expectEvent(tx, "AdminAdded", { - admin: account1, - }); - - let isAdmin = await tokenSender.admins(account1); - expect(isAdmin).equal(true); - }); - - it("fails sender isn't an owner", async () => { - await expectRevert(tokenSender.addAdmin(account1, { from: account1 }), "unauthorized"); - }); - }); - - describe("removeAdmin", () => { - it("adds admin", async () => { - await tokenSender.addAdmin(account1); - let tx = await tokenSender.removeAdmin(account1); - - expectEvent(tx, "AdminRemoved", { - admin: account1, - }); - - let isAdmin = await tokenSender.admins(account1); - expect(isAdmin).equal(false); - }); - - it("fails sender isn't an owner", async () => { - await expectRevert(tokenSender.removeAdmin(account1, { from: account1 }), "unauthorized"); - }); - }); - - describe("transferTokens", () => { - it("should be able to transfer SOV", async () => { - let amount = new BN(1000); - await SOV.transfer(tokenSender.address, amount); - - let balanceBefore = await SOV.balanceOf(account1); - - await tokenSender.addAdmin(account1); - await tokenSender.transferTokens(SOV.address, account1, amount, { from: account1 }); - - let balanceAfter = await SOV.balanceOf(account1); - - expect(amount).to.be.bignumber.equal(balanceAfter.sub(balanceBefore)); - }); - - it("only owner or admin should be able to transfer", async () => { - await expectRevert(tokenSender.transferTokens(SOV.address, account1, 1000, { from: account1 }), "unauthorized"); - }); - - it("fails if the 0 address is passed as token address", async () => { - await expectRevert(tokenSender.transferTokens(ZERO_ADDRESS, account1, 1000), "token address invalid"); - }); - - it("fails if the 0 address is passed as receiver address", async () => { - await expectRevert(tokenSender.transferTokens(SOV.address, ZERO_ADDRESS, 1000), "receiver address invalid"); - }); - - it("fails if the 0 is passed as an amount", async () => { - await expectRevert(tokenSender.transferTokens(SOV.address, account1, 0), "amount invalid"); - }); - }); - - describe("transferTokensUsingList", () => { - it("should be able to transfer SOV", async () => { - let amount = web3.utils.toWei(new BN(10)); - await SOV.transfer(tokenSender.address, amount); - - let balanceBefore = await SOV.balanceOf(account1); - - await tokenSender.addAdmin(account1); - let tx = await tokenSender.transferTokensUsingList(SOV.address, [account1], [amount], { from: account1 }); - console.log("gasUsed = " + tx.receipt.gasUsed); - - let balanceAfter = await SOV.balanceOf(account1); - - expect(amount).to.be.bignumber.equal(balanceAfter.sub(balanceBefore)); - }); - - it("should be able to transfer SOV to N users", async () => { - let amount = web3.utils.toWei(new BN(10)); - await SOV.transfer(tokenSender.address, amount.mul(new BN(2))); - - let balanceBefore1 = await SOV.balanceOf(account1); - let balanceBefore2 = await SOV.balanceOf(account2); - - await tokenSender.addAdmin(account1); - let tx = await tokenSender.transferTokensUsingList(SOV.address, [account1, account2], [amount, amount], { from: account1 }); - - let balanceAfter1 = await SOV.balanceOf(account1); - let balanceAfter2 = await SOV.balanceOf(account2); - - expect(amount).to.be.bignumber.equal(balanceAfter1.sub(balanceBefore1)); - expect(amount).to.be.bignumber.equal(balanceAfter2.sub(balanceBefore2)); - }); - - it("should be able to transfer SOV to N users and check gas usage", async () => { - let userCount = 500; - let amount = web3.utils.toWei(new BN(10)); - let totalAmount = amount.mul(new BN(userCount)); - await SOV.transfer(tokenSender.address, totalAmount); - - let accounts = []; - let amounts = []; - for (let i = 0; i < userCount; i++) { - accounts.push(account1); - amounts.push(amount); - } - - let tx = await tokenSender.transferTokensUsingList(SOV.address, accounts, amounts); - console.log("gasUsed = " + tx.receipt.gasUsed); - - let balance = await SOV.balanceOf(account1); - expect(totalAmount).to.be.bignumber.equal(balance); - }); - - it("only owner or admin should be able to transfer", async () => { - await expectRevert(tokenSender.transferTokensUsingList(SOV.address, [account1], [1000], { from: account1 }), "unauthorized"); - }); - - it("fails if the 0 address is passed as receiver address", async () => { - await expectRevert(tokenSender.transferTokensUsingList(ZERO_ADDRESS, [account1], [1000]), "token address invalid"); - }); - - it("fails if the 0 address is passed as receiver address", async () => { - await expectRevert(tokenSender.transferTokensUsingList(SOV.address, [ZERO_ADDRESS], [1000]), "receiver address invalid"); - }); - - it("fails if the 0 is passed as an amount", async () => { - await expectRevert(tokenSender.transferTokensUsingList(SOV.address, [account1], [0]), "amount invalid"); - }); - }); + let root, account1, account2, account3; + let SOV, tokenSender; + + before(async () => { + [root, account1, account2, account3, ...accounts] = accounts; + }); + + beforeEach(async () => { + SOV = await SOV_ABI.new(TOTAL_SUPPLY); + tokenSender = await GenericTokenSender.new(); + }); + + describe("addAdmin", () => { + it("adds admin", async () => { + let tx = await tokenSender.addAdmin(account1); + + expectEvent(tx, "AdminAdded", { + admin: account1, + }); + + let isAdmin = await tokenSender.admins(account1); + expect(isAdmin).equal(true); + }); + + it("fails sender isn't an owner", async () => { + await expectRevert(tokenSender.addAdmin(account1, { from: account1 }), "unauthorized"); + }); + }); + + describe("removeAdmin", () => { + it("adds admin", async () => { + await tokenSender.addAdmin(account1); + let tx = await tokenSender.removeAdmin(account1); + + expectEvent(tx, "AdminRemoved", { + admin: account1, + }); + + let isAdmin = await tokenSender.admins(account1); + expect(isAdmin).equal(false); + }); + + it("fails sender isn't an owner", async () => { + await expectRevert( + tokenSender.removeAdmin(account1, { from: account1 }), + "unauthorized" + ); + }); + }); + + describe("transferTokens", () => { + it("should be able to transfer SOV", async () => { + let amount = new BN(1000); + await SOV.transfer(tokenSender.address, amount); + + let balanceBefore = await SOV.balanceOf(account1); + + await tokenSender.addAdmin(account1); + await tokenSender.transferTokens(SOV.address, account1, amount, { from: account1 }); + + let balanceAfter = await SOV.balanceOf(account1); + + expect(amount).to.be.bignumber.equal(balanceAfter.sub(balanceBefore)); + }); + + it("only owner or admin should be able to transfer", async () => { + await expectRevert( + tokenSender.transferTokens(SOV.address, account1, 1000, { from: account1 }), + "unauthorized" + ); + }); + + it("fails if the 0 address is passed as token address", async () => { + await expectRevert( + tokenSender.transferTokens(ZERO_ADDRESS, account1, 1000), + "token address invalid" + ); + }); + + it("fails if the 0 address is passed as receiver address", async () => { + await expectRevert( + tokenSender.transferTokens(SOV.address, ZERO_ADDRESS, 1000), + "receiver address invalid" + ); + }); + + it("fails if the 0 is passed as an amount", async () => { + await expectRevert( + tokenSender.transferTokens(SOV.address, account1, 0), + "amount invalid" + ); + }); + }); + + describe("transferTokensUsingList", () => { + it("should be able to transfer SOV", async () => { + let amount = web3.utils.toWei(new BN(10)); + await SOV.transfer(tokenSender.address, amount); + + let balanceBefore = await SOV.balanceOf(account1); + + await tokenSender.addAdmin(account1); + let tx = await tokenSender.transferTokensUsingList(SOV.address, [account1], [amount], { + from: account1, + }); + console.log("gasUsed = " + tx.receipt.gasUsed); + + let balanceAfter = await SOV.balanceOf(account1); + + expect(amount).to.be.bignumber.equal(balanceAfter.sub(balanceBefore)); + }); + + it("should be able to transfer SOV to N users", async () => { + let amount = web3.utils.toWei(new BN(10)); + await SOV.transfer(tokenSender.address, amount.mul(new BN(2))); + + let balanceBefore1 = await SOV.balanceOf(account1); + let balanceBefore2 = await SOV.balanceOf(account2); + + await tokenSender.addAdmin(account1); + let tx = await tokenSender.transferTokensUsingList( + SOV.address, + [account1, account2], + [amount, amount], + { from: account1 } + ); + + let balanceAfter1 = await SOV.balanceOf(account1); + let balanceAfter2 = await SOV.balanceOf(account2); + + expect(amount).to.be.bignumber.equal(balanceAfter1.sub(balanceBefore1)); + expect(amount).to.be.bignumber.equal(balanceAfter2.sub(balanceBefore2)); + }); + + it("should be able to transfer SOV to N users and check gas usage", async () => { + let userCount = 500; + let amount = web3.utils.toWei(new BN(10)); + let totalAmount = amount.mul(new BN(userCount)); + await SOV.transfer(tokenSender.address, totalAmount); + + let accounts = []; + let amounts = []; + for (let i = 0; i < userCount; i++) { + accounts.push(account1); + amounts.push(amount); + } + + let tx = await tokenSender.transferTokensUsingList(SOV.address, accounts, amounts); + console.log("gasUsed = " + tx.receipt.gasUsed); + + let balance = await SOV.balanceOf(account1); + expect(totalAmount).to.be.bignumber.equal(balance); + }); + + it("only owner or admin should be able to transfer", async () => { + await expectRevert( + tokenSender.transferTokensUsingList(SOV.address, [account1], [1000], { + from: account1, + }), + "unauthorized" + ); + }); + + it("fails if the 0 address is passed as receiver address", async () => { + await expectRevert( + tokenSender.transferTokensUsingList(ZERO_ADDRESS, [account1], [1000]), + "token address invalid" + ); + }); + + it("fails if the 0 address is passed as receiver address", async () => { + await expectRevert( + tokenSender.transferTokensUsingList(SOV.address, [ZERO_ADDRESS], [1000]), + "receiver address invalid" + ); + }); + + it("fails if the 0 is passed as an amount", async () => { + await expectRevert( + tokenSender.transferTokensUsingList(SOV.address, [account1], [0]), + "amount invalid" + ); + }); + }); }); diff --git a/tests/vesting/PostCSOV_dev.js b/tests/vesting/PostCSOV_dev.js index d350fb22d..8dbe4d2ad 100644 --- a/tests/vesting/PostCSOV_dev.js +++ b/tests/vesting/PostCSOV_dev.js @@ -16,206 +16,212 @@ const TestToken = artifacts.require("TestToken.sol"); const TOTAL_SUPPLY = "100000000000000000000000000"; contract("PostCSOV", (accounts) => { - let postcsov; - let token1; - let token2; - let postcsovAddr; - - const dummyAddress = accounts[9]; - const owner = accounts[5]; - const csovAdmin = accounts[0]; - const amountUser = web3.utils.toWei("3"); - // console.log("Owner: " + owner); - - const pricsSats = "2500"; - - async function deploymentAndInitFixture(_wallets, _provider) { - // deploy CSOVToken1 - token1 = await TestToken.new("cSOV1", "cSOV1", 18, TOTAL_SUPPLY); - tokenAddr1 = await token1.address; - - await token1.transfer(accounts[2], amountUser, { from: csovAdmin }); - - let CSOVAmountWei = await token1.balanceOf(accounts[2]); - console.log("CSOVAmountWei: " + CSOVAmountWei); - - // deploy CSOVToken2 - token2 = await TestToken.new("cSOV2", "cSOV2", 18, TOTAL_SUPPLY); - tokenAddr2 = await token2.address; - - await token2.transfer(accounts[2], amountUser, { from: csovAdmin }); - - CSOVAmountWei = await token2.balanceOf(accounts[2]); - console.log("CSOVAmountWei: " + CSOVAmountWei); - - // deploy PostCSOV - postcsov = await PostCSOV.new( - dummyAddress, - dummyAddress, - [tokenAddr1, tokenAddr2], - pricsSats, - dummyAddress, - dummyAddress, - dummyAddress, - { from: owner } - ); - console.log(tokenAddr1 + " " + tokenAddr2 + " " + pricsSats); - postcsovAddr = await postcsov.address; - } + let postcsov; + let token1; + let token2; + let postcsovAddr; + + const dummyAddress = accounts[9]; + const owner = accounts[5]; + const csovAdmin = accounts[0]; + const amountUser = web3.utils.toWei("3"); + // console.log("Owner: " + owner); + + const pricsSats = "2500"; + + async function deploymentAndInitFixture(_wallets, _provider) { + // deploy CSOVToken1 + token1 = await TestToken.new("cSOV1", "cSOV1", 18, TOTAL_SUPPLY); + tokenAddr1 = await token1.address; + + await token1.transfer(accounts[2], amountUser, { from: csovAdmin }); + + let CSOVAmountWei = await token1.balanceOf(accounts[2]); + console.log("CSOVAmountWei: " + CSOVAmountWei); + + // deploy CSOVToken2 + token2 = await TestToken.new("cSOV2", "cSOV2", 18, TOTAL_SUPPLY); + tokenAddr2 = await token2.address; + + await token2.transfer(accounts[2], amountUser, { from: csovAdmin }); + + CSOVAmountWei = await token2.balanceOf(accounts[2]); + console.log("CSOVAmountWei: " + CSOVAmountWei); + + // deploy PostCSOV + postcsov = await PostCSOV.new( + dummyAddress, + dummyAddress, + [tokenAddr1, tokenAddr2], + pricsSats, + dummyAddress, + dummyAddress, + dummyAddress, + { from: owner } + ); + console.log(tokenAddr1 + " " + tokenAddr2 + " " + pricsSats); + postcsovAddr = await postcsov.address; + } - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - describe("deposit funds", () => { - it("should deposit", async () => { - const amount = web3.utils.toWei("3"); - let postBudget = await postcsov.budget(); - console.log("postBudget: " + postBudget); - - await postcsov.deposit({ from: accounts[1], value: amount }); - - postBudget = await postcsov.budget(); - console.log("postBudget: " + postBudget); - }); - }); - - describe("reImburse", () => { - it("should reImburse", async () => { - const amount = web3.utils.toWei("3"); - let postBudget = await postcsov.budget(); - console.log("postBudget: " + postBudget); - - await postcsov.deposit({ from: accounts[1], value: amount }); - - postBudget = await postcsov.budget(); - console.log("postBudget: " + postBudget); + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + describe("deposit funds", () => { + it("should deposit", async () => { + const amount = web3.utils.toWei("3"); + let postBudget = await postcsov.budget(); + console.log("postBudget: " + postBudget); + + await postcsov.deposit({ from: accounts[1], value: amount }); + + postBudget = await postcsov.budget(); + console.log("postBudget: " + postBudget); + }); + }); + + describe("reImburse", () => { + it("should reImburse", async () => { + const amount = web3.utils.toWei("3"); + let postBudget = await postcsov.budget(); + console.log("postBudget: " + postBudget); + + await postcsov.deposit({ from: accounts[1], value: amount }); + + postBudget = await postcsov.budget(); + console.log("postBudget: " + postBudget); + + let CSOVAmountWei1 = await token1.balanceOf(accounts[2]); + console.log("CSOVAmountWei1: " + CSOVAmountWei1); + + let CSOVAmountWei2 = await token2.balanceOf(accounts[2]); + console.log("CSOVAmountWei2: " + CSOVAmountWei2); + + let tx = await postcsov.reImburse({ from: accounts[2] }); + + // Found and fixed the SIP-0007 bug on VestingRegistry::reImburse formula. + // More details at Documenting Code issues at point 11 in + // https://docs.google.com/document/d/10idTD1K6JvoBmtPKGuJ2Ub_mMh6qTLLlTP693GQKMyU/ + // Bug: let rbtcAmount = ((CSOVAmountWei1 + CSOVAmountWei2) * pricsSats) / 10 ** 10; + let rbtcAmount = ((CSOVAmountWei1 + CSOVAmountWei2) * pricsSats) / 10 ** 8; + console.log("rbtcAmount: " + rbtcAmount); + + expectEvent(tx, "CSOVReImburse", { + from: accounts[2], + CSOVamount: "6000000000000000000", + reImburseAmount: "150000000000000", + }); + }); + + it("should reImburse partially", async () => { + const amount = web3.utils.toWei("3"); + let postBudget = await postcsov.budget(); + console.log("postBudget: " + postBudget); + + await postcsov.deposit({ from: accounts[1], value: amount }); - let CSOVAmountWei1 = await token1.balanceOf(accounts[2]); - console.log("CSOVAmountWei1: " + CSOVAmountWei1); - - let CSOVAmountWei2 = await token2.balanceOf(accounts[2]); - console.log("CSOVAmountWei2: " + CSOVAmountWei2); - - let tx = await postcsov.reImburse({ from: accounts[2] }); + postBudget = await postcsov.budget(); + console.log("postBudget: " + postBudget); + + let CSOVAmountWei1 = await token1.balanceOf(accounts[2]); + console.log("CSOVAmountWei1: " + CSOVAmountWei1); - // Found and fixed the SIP-0007 bug on VestingRegistry::reImburse formula. - // More details at Documenting Code issues at point 11 in - // https://docs.google.com/document/d/10idTD1K6JvoBmtPKGuJ2Ub_mMh6qTLLlTP693GQKMyU/ - // Bug: let rbtcAmount = ((CSOVAmountWei1 + CSOVAmountWei2) * pricsSats) / 10 ** 10; - let rbtcAmount = ((CSOVAmountWei1 + CSOVAmountWei2) * pricsSats) / 10 ** 8; - console.log("rbtcAmount: " + rbtcAmount); + let CSOVAmountWei2 = await token2.balanceOf(accounts[2]); + console.log("CSOVAmountWei2: " + CSOVAmountWei2); + + await postcsov.setLockedAmount(accounts[2], "2000000000000000000", { from: owner }); + let tx = await postcsov.reImburse({ from: accounts[2] }); + + // Found and fixed the SIP-0007 bug on VestingRegistry::reImburse formula. + // More details at Documenting Code issues at point 11 in + // https://docs.google.com/document/d/10idTD1K6JvoBmtPKGuJ2Ub_mMh6qTLLlTP693GQKMyU/ + let rbtcAmount = ((CSOVAmountWei1 + CSOVAmountWei2) * pricsSats) / 10 ** 8; + console.log("rbtcAmount: " + rbtcAmount); + + expectEvent(tx, "CSOVReImburse", { + from: accounts[2], + CSOVamount: "4000000000000000000", + reImburseAmount: "100000000000000", + }); + }); + + it("should NOT reImburse twice", async () => { + const amount = web3.utils.toWei("3"); + let postBudget = await postcsov.budget(); + console.log("postBudget: " + postBudget); + + await postcsov.deposit({ from: accounts[1], value: amount }); + + postBudget = await postcsov.budget(); + console.log("postBudget: " + postBudget); + + let CSOVAmountWei1 = await token1.balanceOf(accounts[2]); + console.log("CSOVAmountWei1: " + CSOVAmountWei1); - expectEvent(tx, "CSOVReImburse", { - from: accounts[2], - CSOVamount: "6000000000000000000", - reImburseAmount: "150000000000000", - }); - }); + let CSOVAmountWei2 = await token2.balanceOf(accounts[2]); + console.log("CSOVAmountWei2: " + CSOVAmountWei2); - it("should reImburse partially", async () => { - const amount = web3.utils.toWei("3"); - let postBudget = await postcsov.budget(); - console.log("postBudget: " + postBudget); + let tx = await postcsov.reImburse({ from: accounts[2] }); - await postcsov.deposit({ from: accounts[1], value: amount }); + // Found and fixed the SIP-0007 bug on VestingRegistry::reImburse formula. + // More details at Documenting Code issues at point 11 in + // https://docs.google.com/document/d/10idTD1K6JvoBmtPKGuJ2Ub_mMh6qTLLlTP693GQKMyU/ + let rbtcAmount = ((CSOVAmountWei1 + CSOVAmountWei2) * pricsSats) / 10 ** 8; + console.log("rbtcAmount: " + rbtcAmount); - postBudget = await postcsov.budget(); - console.log("postBudget: " + postBudget); + await expectRevert(postcsov.reImburse({ from: accounts[3] }), "holder has no CSOV"); - let CSOVAmountWei1 = await token1.balanceOf(accounts[2]); - console.log("CSOVAmountWei1: " + CSOVAmountWei1); + expectEvent(tx, "CSOVReImburse", { + from: accounts[2], + CSOVamount: "6000000000000000000", + reImburseAmount: "150000000000000", + }); - let CSOVAmountWei2 = await token2.balanceOf(accounts[2]); - console.log("CSOVAmountWei2: " + CSOVAmountWei2); + await expectRevert( + postcsov.reImburse({ from: accounts[2] }), + "Address cannot be processed twice" + ); + }); - await postcsov.setLockedAmount(accounts[2], "2000000000000000000", { from: owner }); - let tx = await postcsov.reImburse({ from: accounts[2] }); + it("should not reImburse if user has no CSOV", async () => { + let postBudget = await postcsov.budget(); + console.log("postBudget: " + postBudget); - // Found and fixed the SIP-0007 bug on VestingRegistry::reImburse formula. - // More details at Documenting Code issues at point 11 in - // https://docs.google.com/document/d/10idTD1K6JvoBmtPKGuJ2Ub_mMh6qTLLlTP693GQKMyU/ - let rbtcAmount = ((CSOVAmountWei1 + CSOVAmountWei2) * pricsSats) / 10 ** 8; - console.log("rbtcAmount: " + rbtcAmount); + let CSOVAmountWei1 = await token1.balanceOf(accounts[3]); + console.log("CSOVAmountWei1: " + CSOVAmountWei1); - expectEvent(tx, "CSOVReImburse", { - from: accounts[2], - CSOVamount: "4000000000000000000", - reImburseAmount: "100000000000000", - }); - }); + let CSOVAmountWei2 = await token2.balanceOf(accounts[3]); + console.log("CSOVAmountWei2: " + CSOVAmountWei2); - it("should NOT reImburse twice", async () => { - const amount = web3.utils.toWei("3"); - let postBudget = await postcsov.budget(); - console.log("postBudget: " + postBudget); - - await postcsov.deposit({ from: accounts[1], value: amount }); + postBudget = await postcsov.budget(); + console.log("postBudget: " + postBudget); + console.log("CSOVAmountWei1: " + CSOVAmountWei1); + console.log("CSOVAmountWei2: " + CSOVAmountWei2); - postBudget = await postcsov.budget(); - console.log("postBudget: " + postBudget); + await expectRevert(postcsov.reImburse({ from: accounts[3] }), "holder has no CSOV"); + }); + + it("should not reImburse if user blacklisted", async () => { + await postcsov.setBlacklistFlag(accounts[3], true, { from: owner }); - let CSOVAmountWei1 = await token1.balanceOf(accounts[2]); - console.log("CSOVAmountWei1: " + CSOVAmountWei1); + await expectRevert(postcsov.reImburse({ from: accounts[3] }), "Address blacklisted"); + }); + }); - let CSOVAmountWei2 = await token2.balanceOf(accounts[2]); - console.log("CSOVAmountWei2: " + CSOVAmountWei2); + describe("withdraw funds", () => { + it("should withdraw", async () => { + await expectRevert( + postcsov.withdrawAll(accounts[4], { from: accounts[4] }), + "unauthorized" + ); - let tx = await postcsov.reImburse({ from: accounts[2] }); + let postBudget = await postcsov.budget(); + console.log("postBudget: " + postBudget); - // Found and fixed the SIP-0007 bug on VestingRegistry::reImburse formula. - // More details at Documenting Code issues at point 11 in - // https://docs.google.com/document/d/10idTD1K6JvoBmtPKGuJ2Ub_mMh6qTLLlTP693GQKMyU/ - let rbtcAmount = ((CSOVAmountWei1 + CSOVAmountWei2) * pricsSats) / 10 ** 8; - console.log("rbtcAmount: " + rbtcAmount); + await postcsov.withdrawAll(accounts[4], { from: owner }); - await expectRevert(postcsov.reImburse({ from: accounts[3] }), "holder has no CSOV"); - - expectEvent(tx, "CSOVReImburse", { - from: accounts[2], - CSOVamount: "6000000000000000000", - reImburseAmount: "150000000000000", - }); - - await expectRevert(postcsov.reImburse({ from: accounts[2] }), "Address cannot be processed twice"); - }); - - it("should not reImburse if user has no CSOV", async () => { - let postBudget = await postcsov.budget(); - console.log("postBudget: " + postBudget); - - let CSOVAmountWei1 = await token1.balanceOf(accounts[3]); - console.log("CSOVAmountWei1: " + CSOVAmountWei1); - - let CSOVAmountWei2 = await token2.balanceOf(accounts[3]); - console.log("CSOVAmountWei2: " + CSOVAmountWei2); - - postBudget = await postcsov.budget(); - console.log("postBudget: " + postBudget); - console.log("CSOVAmountWei1: " + CSOVAmountWei1); - console.log("CSOVAmountWei2: " + CSOVAmountWei2); - - await expectRevert(postcsov.reImburse({ from: accounts[3] }), "holder has no CSOV"); - }); - - it("should not reImburse if user blacklisted", async () => { - await postcsov.setBlacklistFlag(accounts[3], true, { from: owner }); - - await expectRevert(postcsov.reImburse({ from: accounts[3] }), "Address blacklisted"); - }); - }); - - describe("withdraw funds", () => { - it("should withdraw", async () => { - await expectRevert(postcsov.withdrawAll(accounts[4], { from: accounts[4] }), "unauthorized"); - - let postBudget = await postcsov.budget(); - console.log("postBudget: " + postBudget); - - await postcsov.withdrawAll(accounts[4], { from: owner }); - - postBudget = await postcsov.budget(); - console.log("postBudget: " + postBudget); - }); - }); + postBudget = await postcsov.budget(); + console.log("postBudget: " + postBudget); + }); + }); }); diff --git a/tests/vesting/SVRTest.js b/tests/vesting/SVRTest.js index f8b1679f0..99e8a2601 100644 --- a/tests/vesting/SVRTest.js +++ b/tests/vesting/SVRTest.js @@ -28,172 +28,201 @@ const DECIMALS = 18; const WEEK = new BN(7 * 24 * 60 * 60); contract("SVR:", (accounts) => { - let root, account1, account2, account3; - let tokenSOV, tokenSVR, staking; - - async function deploymentAndInitFixture(_wallets, _provider) { - tokenSOV = await SOV.new(TOTAL_SUPPLY); - - let stakingLogic = await StakingLogic.new(tokenSOV.address); - staking = await StakingProxy.new(tokenSOV.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingLogic.at(staking.address); - - tokenSVR = await SVR.new(tokenSOV.address, staking.address); - } - - before(async () => { - [root, account1, account2, account3, ...accounts] = accounts; - }); - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - describe("constructor:", () => { - it("sets the expected values", async () => { - let tokenTemp = await SVR.new(tokenSOV.address, staking.address); - - expect(await tokenTemp.name.call()).to.be.equal(NAME); - expect(await tokenTemp.symbol.call()).to.be.equal(SYMBOL); - expect(await tokenTemp.decimals.call()).to.be.bignumber.equal(new BN(DECIMALS)); - - expect(await tokenTemp.SOV.call()).to.be.equal(tokenSOV.address); - expect(await tokenTemp.staking.call()).to.be.equal(staking.address); - }); - - it("fails if SOV address is zero", async () => { - await expectRevert(SVR.new(ZERO_ADDRESS, staking.address), "SVR::SOV address invalid"); - }); - - it("fails if staking address is zero", async () => { - await expectRevert(SVR.new(tokenSOV.address, ZERO_ADDRESS), "SVR::staking address invalid"); - }); - }); - - describe("mint:", () => { - it("should be able to mint SVR tokens", async () => { - let amount = new BN(1000); - - await tokenSOV.transfer(account1, amount); - await tokenSOV.approve(tokenSVR.address, amount, { from: account1 }); - let tx = await tokenSVR.mint(amount, { from: account1 }); - - expect(await tokenSOV.balanceOf.call(account1)).to.be.bignumber.equal(ZERO); - expect(await tokenSVR.balanceOf.call(account1)).to.be.bignumber.equal(amount); - expect(await tokenSOV.balanceOf.call(tokenSVR.address)).to.be.bignumber.equal(amount); - - expectEvent(tx, "Mint", { - sender: account1, - amount: "1000", - }); - }); - - it("fails if amount is zero", async () => { - await expectRevert(tokenSVR.mint(0), "SVR::mint: amount invalid"); - }); - - it("fails if transfer is not approved", async () => { - await expectRevert(tokenSVR.mint(100), "ERC20: transfer amount exceeds allowance"); - }); - }); - - describe("mintWithApproval:", () => { - let amount = new BN(5000); - - it("should be able to mint SVR tokens", async () => { - await tokenSOV.transfer(account1, amount); - - let contract = new web3.eth.Contract(tokenSVR.abi, tokenSVR.address); - let sender = account1; - let data = contract.methods.mintWithApproval(sender, amount).encodeABI(); - await tokenSOV.approveAndCall(tokenSVR.address, amount, data, { from: sender }); - - expect(await tokenSOV.balanceOf.call(account1)).to.be.bignumber.equal(ZERO); - expect(await tokenSVR.balanceOf.call(account1)).to.be.bignumber.equal(amount); - expect(await tokenSOV.balanceOf.call(tokenSVR.address)).to.be.bignumber.equal(amount); - }); - - it("fails if invoked directly", async () => { - await expectRevert(tokenSVR.mintWithApproval(account1, new BN(5000)), "unauthorized"); - }); - - it("fails if pass wrong method in data", async () => { - let contract = new web3.eth.Contract(tokenSVR.abi, tokenSVR.address); - let data = contract.methods.mint(amount).encodeABI(); - - await expectRevert(tokenSOV.approveAndCall(tokenSVR.address, amount, data, { from: account1 }), "method is not allowed"); - }); - - it("fails if pass wrong method params in data", async () => { - let contract = new web3.eth.Contract(tokenSVR.abi, tokenSVR.address); - let data = contract.methods.mintWithApproval(account1, new BN(0)).encodeABI(); - - await expectRevert(tokenSOV.approveAndCall(tokenSVR.address, amount, data, { from: account1 }), "amount mismatch"); - }); - }); - - describe("receiveApproval:", () => { - it("fails if invoked directly", async () => { - let amount = new BN(5000); - let contract = new web3.eth.Contract(tokenSVR.abi, tokenSVR.address); - let data = contract.methods.mintWithApproval(account1, amount).encodeABI(); - await expectRevert(tokenSVR.receiveApproval(account1, amount, tokenSOV.address, data), "unauthorized"); - }); - }); - - describe("burn:", () => { - it("should be able to burn SVR tokens and stake for 13 positions", async () => { - let initialAmount = 1000; - let amount = initialAmount; - - await tokenSOV.transfer(account1, amount); - await tokenSOV.approve(tokenSVR.address, amount, { from: account1 }); - await tokenSVR.mint(amount, { from: account1 }); - - let tx = await tokenSVR.burn(amount, { from: account1 }); - - let block = await web3.eth.getBlock("latest"); - let timestamp = block.timestamp; - - let start = timestamp + 4 * WEEK; - let end = timestamp + 52 * WEEK; - - let transferAmount = Math.floor(amount / 14); - amount -= transferAmount; - - let numIntervals = Math.floor((end - start) / (4 * WEEK)) + 1; - let stakedPerInterval = Math.floor(amount / numIntervals); - let stakeForFirstInterval = amount - stakedPerInterval * (numIntervals - 1); - - for (let i = start; i <= end; i += 4 * WEEK) { - let lockedTS = await staking.timestampToLockDate(i); - let userStakingCheckpoints = await staking.userStakingCheckpoints(account1, lockedTS, 0); - - expect(userStakingCheckpoints.fromBlock).to.be.bignumber.equal(new BN(block.number)); - if (i === start) { - expect(userStakingCheckpoints.stake).to.be.bignumber.equal(new BN(stakeForFirstInterval)); - } else { - expect(userStakingCheckpoints.stake).to.be.bignumber.equal(new BN(stakedPerInterval)); - } - - let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints(account1, lockedTS); - expect(numUserStakingCheckpoints).to.be.bignumber.equal(new BN(1)); - } - - expect(await tokenSVR.balanceOf.call(account1)).to.be.bignumber.equal(ZERO); - expect(await tokenSOV.balanceOf.call(account1)).to.be.bignumber.equal(new BN(transferAmount)); - expect(await tokenSOV.balanceOf.call(staking.address)).to.be.bignumber.equal(new BN(amount)); - expect(transferAmount + amount).to.be.equal(initialAmount); - - expectEvent(tx, "Burn", { - sender: account1, - amount: new BN(amount), - }); - }); - - it("fails if amount is zero", async () => { - await expectRevert(tokenSVR.burn(0), "SVR:: burn: amount invalid"); - }); - }); + let root, account1, account2, account3; + let tokenSOV, tokenSVR, staking; + + async function deploymentAndInitFixture(_wallets, _provider) { + tokenSOV = await SOV.new(TOTAL_SUPPLY); + + let stakingLogic = await StakingLogic.new(tokenSOV.address); + staking = await StakingProxy.new(tokenSOV.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); + + tokenSVR = await SVR.new(tokenSOV.address, staking.address); + } + + before(async () => { + [root, account1, account2, account3, ...accounts] = accounts; + }); + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + describe("constructor:", () => { + it("sets the expected values", async () => { + let tokenTemp = await SVR.new(tokenSOV.address, staking.address); + + expect(await tokenTemp.name.call()).to.be.equal(NAME); + expect(await tokenTemp.symbol.call()).to.be.equal(SYMBOL); + expect(await tokenTemp.decimals.call()).to.be.bignumber.equal(new BN(DECIMALS)); + + expect(await tokenTemp.SOV.call()).to.be.equal(tokenSOV.address); + expect(await tokenTemp.staking.call()).to.be.equal(staking.address); + }); + + it("fails if SOV address is zero", async () => { + await expectRevert(SVR.new(ZERO_ADDRESS, staking.address), "SVR::SOV address invalid"); + }); + + it("fails if staking address is zero", async () => { + await expectRevert( + SVR.new(tokenSOV.address, ZERO_ADDRESS), + "SVR::staking address invalid" + ); + }); + }); + + describe("mint:", () => { + it("should be able to mint SVR tokens", async () => { + let amount = new BN(1000); + + await tokenSOV.transfer(account1, amount); + await tokenSOV.approve(tokenSVR.address, amount, { from: account1 }); + let tx = await tokenSVR.mint(amount, { from: account1 }); + + expect(await tokenSOV.balanceOf.call(account1)).to.be.bignumber.equal(ZERO); + expect(await tokenSVR.balanceOf.call(account1)).to.be.bignumber.equal(amount); + expect(await tokenSOV.balanceOf.call(tokenSVR.address)).to.be.bignumber.equal(amount); + + expectEvent(tx, "Mint", { + sender: account1, + amount: "1000", + }); + }); + + it("fails if amount is zero", async () => { + await expectRevert(tokenSVR.mint(0), "SVR::mint: amount invalid"); + }); + + it("fails if transfer is not approved", async () => { + await expectRevert(tokenSVR.mint(100), "ERC20: transfer amount exceeds allowance"); + }); + }); + + describe("mintWithApproval:", () => { + let amount = new BN(5000); + + it("should be able to mint SVR tokens", async () => { + await tokenSOV.transfer(account1, amount); + + let contract = new web3.eth.Contract(tokenSVR.abi, tokenSVR.address); + let sender = account1; + let data = contract.methods.mintWithApproval(sender, amount).encodeABI(); + await tokenSOV.approveAndCall(tokenSVR.address, amount, data, { from: sender }); + + expect(await tokenSOV.balanceOf.call(account1)).to.be.bignumber.equal(ZERO); + expect(await tokenSVR.balanceOf.call(account1)).to.be.bignumber.equal(amount); + expect(await tokenSOV.balanceOf.call(tokenSVR.address)).to.be.bignumber.equal(amount); + }); + + it("fails if invoked directly", async () => { + await expectRevert(tokenSVR.mintWithApproval(account1, new BN(5000)), "unauthorized"); + }); + + it("fails if pass wrong method in data", async () => { + let contract = new web3.eth.Contract(tokenSVR.abi, tokenSVR.address); + let data = contract.methods.mint(amount).encodeABI(); + + await expectRevert( + tokenSOV.approveAndCall(tokenSVR.address, amount, data, { from: account1 }), + "method is not allowed" + ); + }); + + it("fails if pass wrong method params in data", async () => { + let contract = new web3.eth.Contract(tokenSVR.abi, tokenSVR.address); + let data = contract.methods.mintWithApproval(account1, new BN(0)).encodeABI(); + + await expectRevert( + tokenSOV.approveAndCall(tokenSVR.address, amount, data, { from: account1 }), + "amount mismatch" + ); + }); + }); + + describe("receiveApproval:", () => { + it("fails if invoked directly", async () => { + let amount = new BN(5000); + let contract = new web3.eth.Contract(tokenSVR.abi, tokenSVR.address); + let data = contract.methods.mintWithApproval(account1, amount).encodeABI(); + await expectRevert( + tokenSVR.receiveApproval(account1, amount, tokenSOV.address, data), + "unauthorized" + ); + }); + }); + + describe("burn:", () => { + it("should be able to burn SVR tokens and stake for 13 positions", async () => { + let initialAmount = 1000; + let amount = initialAmount; + + await tokenSOV.transfer(account1, amount); + await tokenSOV.approve(tokenSVR.address, amount, { from: account1 }); + await tokenSVR.mint(amount, { from: account1 }); + + let tx = await tokenSVR.burn(amount, { from: account1 }); + + let block = await web3.eth.getBlock("latest"); + let timestamp = block.timestamp; + + let start = timestamp + 4 * WEEK; + let end = timestamp + 52 * WEEK; + + let transferAmount = Math.floor(amount / 14); + amount -= transferAmount; + + let numIntervals = Math.floor((end - start) / (4 * WEEK)) + 1; + let stakedPerInterval = Math.floor(amount / numIntervals); + let stakeForFirstInterval = amount - stakedPerInterval * (numIntervals - 1); + + for (let i = start; i <= end; i += 4 * WEEK) { + let lockedTS = await staking.timestampToLockDate(i); + let userStakingCheckpoints = await staking.userStakingCheckpoints( + account1, + lockedTS, + 0 + ); + + expect(userStakingCheckpoints.fromBlock).to.be.bignumber.equal( + new BN(block.number) + ); + if (i === start) { + expect(userStakingCheckpoints.stake).to.be.bignumber.equal( + new BN(stakeForFirstInterval) + ); + } else { + expect(userStakingCheckpoints.stake).to.be.bignumber.equal( + new BN(stakedPerInterval) + ); + } + + let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints( + account1, + lockedTS + ); + expect(numUserStakingCheckpoints).to.be.bignumber.equal(new BN(1)); + } + + expect(await tokenSVR.balanceOf.call(account1)).to.be.bignumber.equal(ZERO); + expect(await tokenSOV.balanceOf.call(account1)).to.be.bignumber.equal( + new BN(transferAmount) + ); + expect(await tokenSOV.balanceOf.call(staking.address)).to.be.bignumber.equal( + new BN(amount) + ); + expect(transferAmount + amount).to.be.equal(initialAmount); + + expectEvent(tx, "Burn", { + sender: account1, + amount: new BN(amount), + }); + }); + + it("fails if amount is zero", async () => { + await expectRevert(tokenSVR.burn(0), "SVR:: burn: amount invalid"); + }); + }); }); diff --git a/tests/vesting/TeamVesting.js b/tests/vesting/TeamVesting.js index 6465f5619..f13cfb2d2 100644 --- a/tests/vesting/TeamVesting.js +++ b/tests/vesting/TeamVesting.js @@ -16,25 +16,25 @@ const TestToken = artifacts.require("TestToken"); const TOTAL_SUPPLY = "10000000000000000000000000"; contract("TeamVesting", (accounts) => { - const name = "Test token"; - const symbol = "TST"; + const name = "Test token"; + const symbol = "TST"; - let root, a1, a2, a3; - let token, staking; - let kickoffTS; + let root, a1, a2, a3; + let token, staking; + let kickoffTS; - before(async () => { - [root, a1, a2, a3, ...accounts] = accounts; - token = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); + before(async () => { + [root, a1, a2, a3, ...accounts] = accounts; + token = await TestToken.new(name, symbol, 18, TOTAL_SUPPLY); - let stakingLogic = await StakingLogic.new(token.address); - staking = await StakingProxy.new(token.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingLogic.at(staking.address); + let stakingLogic = await StakingLogic.new(token.address); + staking = await StakingProxy.new(token.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); - await token.transfer(a2, "1000"); - await token.approve(staking.address, "1000", { from: a2 }); + await token.transfer(a2, "1000"); + await token.approve(staking.address, "1000", { from: a2 }); - kickoffTS = await staking.kickoffTS.call(); - }); + kickoffTS = await staking.kickoffTS.call(); + }); }); diff --git a/tests/vesting/TokenSender.js b/tests/vesting/TokenSender.js index 2ab7dcf35..1d6635019 100644 --- a/tests/vesting/TokenSender.js +++ b/tests/vesting/TokenSender.js @@ -22,162 +22,186 @@ const TOTAL_SUPPLY = "100000000000000000000000000"; const ZERO_ADDRESS = constants.ZERO_ADDRESS; contract("TokenSender", (accounts) => { - let root, account1, account2, account3; - let SOV, tokenSender; - - async function deploymentAndInitFixture(_wallets, _provider) { - SOV = await SOV_ABI.new(TOTAL_SUPPLY); - tokenSender = await TokenSender.new(SOV.address); - } - - before(async () => { - [root, account1, account2, account3, ...accounts] = accounts; - }); - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - describe("constructor", () => { - it("sets the expected values", async () => { - let _sov = await tokenSender.SOV(); - - expect(_sov).equal(SOV.address); - }); - - it("fails if the 0 address is passed as SOV address", async () => { - await expectRevert(TokenSender.new(ZERO_ADDRESS), "SOV address invalid"); - }); - }); - - describe("addAdmin", () => { - it("adds admin", async () => { - let tx = await tokenSender.addAdmin(account1); - - expectEvent(tx, "AdminAdded", { - admin: account1, - }); - - let isAdmin = await tokenSender.admins(account1); - expect(isAdmin).equal(true); - }); - - it("fails sender isn't an owner", async () => { - await expectRevert(tokenSender.addAdmin(account1, { from: account1 }), "unauthorized"); - }); - }); - - describe("removeAdmin", () => { - it("adds admin", async () => { - await tokenSender.addAdmin(account1); - let tx = await tokenSender.removeAdmin(account1); - - expectEvent(tx, "AdminRemoved", { - admin: account1, - }); - - let isAdmin = await tokenSender.admins(account1); - expect(isAdmin).equal(false); - }); - - it("fails sender isn't an owner", async () => { - await expectRevert(tokenSender.removeAdmin(account1, { from: account1 }), "unauthorized"); - }); - }); - - describe("transferSOV", () => { - it("should be able to transfer SOV", async () => { - let amount = new BN(1000); - await SOV.transfer(tokenSender.address, amount); - - let balanceBefore = await SOV.balanceOf(account1); - - await tokenSender.addAdmin(account1); - await tokenSender.transferSOV(account1, amount, { from: account1 }); - - let balanceAfter = await SOV.balanceOf(account1); - - expect(amount).to.be.bignumber.equal(balanceAfter.sub(balanceBefore)); - }); - - it("only owner or admin should be able to transfer", async () => { - await expectRevert(tokenSender.transferSOV(account1, 1000, { from: account1 }), "unauthorized"); - }); - - it("fails if the 0 address is passed as receiver address", async () => { - await expectRevert(tokenSender.transferSOV(ZERO_ADDRESS, 1000), "receiver address invalid"); - }); - - it("fails if the 0 is passed as an amount", async () => { - await expectRevert(tokenSender.transferSOV(account1, 0), "amount invalid"); - }); - }); - - describe("transferSOVusingList", () => { - it("should be able to transfer SOV", async () => { - let amount = web3.utils.toWei(new BN(10)); - await SOV.transfer(tokenSender.address, amount); - - let balanceBefore = await SOV.balanceOf(account1); - - await tokenSender.addAdmin(account1); - let tx = await tokenSender.transferSOVusingList([account1], [amount], { from: account1 }); - console.log("gasUsed = " + tx.receipt.gasUsed); - - let balanceAfter = await SOV.balanceOf(account1); - - expect(amount).to.be.bignumber.equal(balanceAfter.sub(balanceBefore)); - }); - - it("should be able to transfer SOV to N users", async () => { - let amount = web3.utils.toWei(new BN(10)); - await SOV.transfer(tokenSender.address, amount.mul(new BN(2))); - - let balanceBefore1 = await SOV.balanceOf(account1); - let balanceBefore2 = await SOV.balanceOf(account2); - - await tokenSender.addAdmin(account1); - let tx = await tokenSender.transferSOVusingList([account1, account2], [amount, amount], { from: account1 }); - - let balanceAfter1 = await SOV.balanceOf(account1); - let balanceAfter2 = await SOV.balanceOf(account2); - - expect(amount).to.be.bignumber.equal(balanceAfter1.sub(balanceBefore1)); - expect(amount).to.be.bignumber.equal(balanceAfter2.sub(balanceBefore2)); - }); - - it("should be able to transfer SOV to N users and check gas usage", async () => { - /// @dev Reduced loop size from 500 to 50, for optimization purposes - let userCount = 50; - - let amount = web3.utils.toWei(new BN(10)); - let totalAmount = amount.mul(new BN(userCount)); - await SOV.transfer(tokenSender.address, totalAmount); - - let accounts = []; - let amounts = []; - for (let i = 0; i < userCount; i++) { - accounts.push(account1); - amounts.push(amount); - } - - let tx = await tokenSender.transferSOVusingList(accounts, amounts); - console.log("gasUsed = " + tx.receipt.gasUsed); - - let balance = await SOV.balanceOf(account1); - expect(totalAmount).to.be.bignumber.equal(balance); - }); - - it("only owner or admin should be able to transfer", async () => { - await expectRevert(tokenSender.transferSOVusingList([account1], [1000], { from: account1 }), "unauthorized"); - }); - - it("fails if the 0 address is passed as receiver address", async () => { - await expectRevert(tokenSender.transferSOVusingList([ZERO_ADDRESS], [1000]), "receiver address invalid"); - }); - - it("fails if the 0 is passed as an amount", async () => { - await expectRevert(tokenSender.transferSOVusingList([account1], [0]), "amount invalid"); - }); - }); + let root, account1, account2, account3; + let SOV, tokenSender; + + async function deploymentAndInitFixture(_wallets, _provider) { + SOV = await SOV_ABI.new(TOTAL_SUPPLY); + tokenSender = await TokenSender.new(SOV.address); + } + + before(async () => { + [root, account1, account2, account3, ...accounts] = accounts; + }); + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + describe("constructor", () => { + it("sets the expected values", async () => { + let _sov = await tokenSender.SOV(); + + expect(_sov).equal(SOV.address); + }); + + it("fails if the 0 address is passed as SOV address", async () => { + await expectRevert(TokenSender.new(ZERO_ADDRESS), "SOV address invalid"); + }); + }); + + describe("addAdmin", () => { + it("adds admin", async () => { + let tx = await tokenSender.addAdmin(account1); + + expectEvent(tx, "AdminAdded", { + admin: account1, + }); + + let isAdmin = await tokenSender.admins(account1); + expect(isAdmin).equal(true); + }); + + it("fails sender isn't an owner", async () => { + await expectRevert(tokenSender.addAdmin(account1, { from: account1 }), "unauthorized"); + }); + }); + + describe("removeAdmin", () => { + it("adds admin", async () => { + await tokenSender.addAdmin(account1); + let tx = await tokenSender.removeAdmin(account1); + + expectEvent(tx, "AdminRemoved", { + admin: account1, + }); + + let isAdmin = await tokenSender.admins(account1); + expect(isAdmin).equal(false); + }); + + it("fails sender isn't an owner", async () => { + await expectRevert( + tokenSender.removeAdmin(account1, { from: account1 }), + "unauthorized" + ); + }); + }); + + describe("transferSOV", () => { + it("should be able to transfer SOV", async () => { + let amount = new BN(1000); + await SOV.transfer(tokenSender.address, amount); + + let balanceBefore = await SOV.balanceOf(account1); + + await tokenSender.addAdmin(account1); + await tokenSender.transferSOV(account1, amount, { from: account1 }); + + let balanceAfter = await SOV.balanceOf(account1); + + expect(amount).to.be.bignumber.equal(balanceAfter.sub(balanceBefore)); + }); + + it("only owner or admin should be able to transfer", async () => { + await expectRevert( + tokenSender.transferSOV(account1, 1000, { from: account1 }), + "unauthorized" + ); + }); + + it("fails if the 0 address is passed as receiver address", async () => { + await expectRevert( + tokenSender.transferSOV(ZERO_ADDRESS, 1000), + "receiver address invalid" + ); + }); + + it("fails if the 0 is passed as an amount", async () => { + await expectRevert(tokenSender.transferSOV(account1, 0), "amount invalid"); + }); + }); + + describe("transferSOVusingList", () => { + it("should be able to transfer SOV", async () => { + let amount = web3.utils.toWei(new BN(10)); + await SOV.transfer(tokenSender.address, amount); + + let balanceBefore = await SOV.balanceOf(account1); + + await tokenSender.addAdmin(account1); + let tx = await tokenSender.transferSOVusingList([account1], [amount], { + from: account1, + }); + console.log("gasUsed = " + tx.receipt.gasUsed); + + let balanceAfter = await SOV.balanceOf(account1); + + expect(amount).to.be.bignumber.equal(balanceAfter.sub(balanceBefore)); + }); + + it("should be able to transfer SOV to N users", async () => { + let amount = web3.utils.toWei(new BN(10)); + await SOV.transfer(tokenSender.address, amount.mul(new BN(2))); + + let balanceBefore1 = await SOV.balanceOf(account1); + let balanceBefore2 = await SOV.balanceOf(account2); + + await tokenSender.addAdmin(account1); + let tx = await tokenSender.transferSOVusingList( + [account1, account2], + [amount, amount], + { from: account1 } + ); + + let balanceAfter1 = await SOV.balanceOf(account1); + let balanceAfter2 = await SOV.balanceOf(account2); + + expect(amount).to.be.bignumber.equal(balanceAfter1.sub(balanceBefore1)); + expect(amount).to.be.bignumber.equal(balanceAfter2.sub(balanceBefore2)); + }); + + it("should be able to transfer SOV to N users and check gas usage", async () => { + /// @dev Reduced loop size from 500 to 50, for optimization purposes + let userCount = 50; + + let amount = web3.utils.toWei(new BN(10)); + let totalAmount = amount.mul(new BN(userCount)); + await SOV.transfer(tokenSender.address, totalAmount); + + let accounts = []; + let amounts = []; + for (let i = 0; i < userCount; i++) { + accounts.push(account1); + amounts.push(amount); + } + + let tx = await tokenSender.transferSOVusingList(accounts, amounts); + console.log("gasUsed = " + tx.receipt.gasUsed); + + let balance = await SOV.balanceOf(account1); + expect(totalAmount).to.be.bignumber.equal(balance); + }); + + it("only owner or admin should be able to transfer", async () => { + await expectRevert( + tokenSender.transferSOVusingList([account1], [1000], { from: account1 }), + "unauthorized" + ); + }); + + it("fails if the 0 address is passed as receiver address", async () => { + await expectRevert( + tokenSender.transferSOVusingList([ZERO_ADDRESS], [1000]), + "receiver address invalid" + ); + }); + + it("fails if the 0 is passed as an amount", async () => { + await expectRevert( + tokenSender.transferSOVusingList([account1], [0]), + "amount invalid" + ); + }); + }); }); diff --git a/tests/vesting/Vesting.js b/tests/vesting/Vesting.js index 935ad84b2..abff543e8 100644 --- a/tests/vesting/Vesting.js +++ b/tests/vesting/Vesting.js @@ -15,16 +15,16 @@ const { expect } = require("chai"); const { expectRevert, expectEvent, constants, BN } = require("@openzeppelin/test-helpers"); const { - address, - minerStart, - minerStop, - unlockedAccount, - mineBlock, - etherMantissa, - etherUnsigned, - setTime, - increaseTime, - lastBlock, + address, + minerStart, + minerStop, + unlockedAccount, + mineBlock, + etherMantissa, + etherUnsigned, + setTime, + increaseTime, + lastBlock, } = require("../Utils/Ethereum"); const StakingLogic = artifacts.require("Staking"); @@ -45,984 +45,1100 @@ const TOTAL_SUPPLY = "10000000000000000000000000"; const ONE_MILLON = "1000000000000000000000000"; contract("Vesting", (accounts) => { - const name = "Test token"; - const symbol = "TST"; - - let root, a1, a2, a3; - let token, staking, stakingLogic, feeSharingProxy; - let vestingLogic; - let kickoffTS; - - let cliff = "10"; - let duration = "100"; - - before(async () => { - [root, a1, a2, a3, ...accounts] = accounts; - token = await SOV.new(TOTAL_SUPPLY); - wrbtc = await TestWrbtc.new(); - - vestingLogic = await VestingLogic.new(); - - feeSharingProxy = await FeeSharingProxy.new(constants.ZERO_ADDRESS, constants.ZERO_ADDRESS); - - stakingLogic = await StakingLogic.new(token.address); - staking = await StakingProxy.new(token.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingLogic.at(staking.address); - //Upgradable Vesting Registry - vestingRegistryLogic = await VestingRegistryLogic.new(); - vestingReg = await VestingRegistryProxy.new(); - await vestingReg.setImplementation(vestingRegistryLogic.address); - vestingReg = await VestingRegistryLogic.at(vestingReg.address); - await staking.setVestingRegistry(vestingReg.address); - - await token.transfer(a2, "1000"); - await token.approve(staking.address, "1000", { from: a2 }); - - kickoffTS = await staking.kickoffTS.call(); - }); - - describe("constructor", () => { - it("sets the expected values", async () => { - let vestingInstance = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - cliff, - duration, - feeSharingProxy.address - ); - vestingInstance = await VestingLogic.at(vestingInstance.address); - - // Check data - let _sov = await vestingInstance.SOV(); - let _stackingAddress = await vestingInstance.staking(); - let _tokenOwner = await vestingInstance.tokenOwner(); - let _cliff = await vestingInstance.cliff(); - let _duration = await vestingInstance.duration(); - let _feeSharingProxy = await vestingInstance.feeSharingProxy(); - - assert.equal(_sov, token.address); - assert.equal(_stackingAddress, staking.address); - assert.equal(_tokenOwner, root); - assert.equal(_cliff.toString(), cliff); - assert.equal(_duration.toString(), duration); - assert.equal(_feeSharingProxy, feeSharingProxy.address); - }); - - it("fails if the 0 address is passed as SOV address", async () => { - await expectRevert( - Vesting.new(vestingLogic.address, constants.ZERO_ADDRESS, staking.address, root, cliff, duration, feeSharingProxy.address), - - "SOV address invalid" - ); - }); - - it("fails if the 0 address is passed as token owner address", async () => { - await expectRevert( - Vesting.new( - vestingLogic.address, - token.address, - staking.address, - constants.ZERO_ADDRESS, - cliff, - duration, - feeSharingProxy.address - ), - "token owner address invalid" - ); - }); - - it("fails if the 0 address is passed as staking address", async () => { - await expectRevert( - Vesting.new(vestingLogic.address, token.address, constants.ZERO_ADDRESS, root, cliff, duration, feeSharingProxy.address), - "staking address invalid" - ); - }); - - it("fails if the vesting duration is bigger than the max staking duration", async () => { - await expectRevert( - Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - cliff, - MAX_DURATION.add(new BN(1)), - feeSharingProxy.address - ), - "duration may not exceed the max duration" - ); - }); - - it("fails if the vesting duration is shorter than the cliff", async () => { - await expectRevert( - Vesting.new(vestingLogic.address, token.address, staking.address, root, 100, 99, feeSharingProxy.address), - "duration must be bigger than or equal to the cliff" - ); - }); - - it("fails if the 0 address is passed as feeSharingProxy address", async () => { - await expectRevert( - Vesting.new(vestingLogic.address, token.address, staking.address, root, cliff, duration, constants.ZERO_ADDRESS), - "feeSharingProxy address invalid" - ); - }); - }); - - describe("delegate", () => { - let vesting; - it("should stake tokens 2 times and delegate voting power", async () => { - let toStake = ONE_MILLON; - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - a2, - 16 * WEEK, - 26 * WEEK, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - - await token.approve(vesting.address, toStake); - await vesting.stakeTokens(toStake); - - await increaseTime(20 * WEEK); - await token.approve(vesting.address, toStake); - await vesting.stakeTokens(toStake); - - // check delegatee - let data = await staking.getStakes.call(vesting.address); - /// @dev Optimization: This loop through 40 steps is a bottleneck - for (let i = 0; i < data.dates.length; i++) { - let delegatee = await staking.delegates(vesting.address, data.dates[i]); - expect(delegatee).equal(a2); - } - - // delegate - let tx = await vesting.delegate(a1, { from: a2 }); - - expectEvent(tx, "VotesDelegated", { - caller: a2, - delegatee: a1, - }); - - // check new delegatee - data = await staking.getStakes.call(vesting.address); - /// @dev Optimization: This loop through 40 steps is a bottleneck - for (let i = 0; i < data.dates.length; i++) { - let delegatee = await staking.delegates(vesting.address, data.dates[i]); - expect(delegatee).equal(a1); - } - }); - - it("should stake tokens 1 time and delegate voting power (using vesting logic with bug in delegation)", async () => { - let toStake = ONE_MILLON; - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - a2, - 16 * WEEK, - 26 * WEEK, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - - await token.approve(vesting.address, toStake); - await vesting.stakeTokens(toStake); - - // check delegatee - let data = await staking.getStakes.call(vesting.address); - for (let i = 0; i < data.dates.length; i++) { - let delegatee = await staking.delegates(vesting.address, data.dates[i]); - expect(delegatee).equal(a2); - } - - // delegate - let tx = await vesting.delegate(a1, { from: a2 }); - - expectEvent(tx, "VotesDelegated", { - caller: a2, - delegatee: a1, - }); - - // check new delegatee - data = await staking.getStakes.call(vesting.address); - for (let i = 0; i < data.dates.length; i++) { - let delegatee = await staking.delegates(vesting.address, data.dates[i]); - expect(delegatee).equal(a1); - } - }); - - it("fails if delegatee is zero address", async () => { - await expectRevert(vesting.delegate(constants.ZERO_ADDRESS, { from: a2 }), "delegatee address invalid"); - }); - - it("fails if not a token owner", async () => { - await expectRevert(vesting.delegate(a1, { from: a1 }), "unauthorized"); - }); - }); - - describe("stakeTokens; using Ganache", () => { - let vesting; - it("should stake 1,000,000 SOV with a duration of 104 weeks and a 26 week cliff", async () => { - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - 26 * WEEK, - 104 * WEEK, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - await token.approve(vesting.address, ONE_MILLON); - let tx = await vesting.stakeTokens(ONE_MILLON); - - expectEvent(tx, "TokensStaked", { - caller: root, - amount: ONE_MILLON, - }); - - // check delegatee - let data = await staking.getStakes.call(vesting.address); - for (let i = 0; i < data.dates.length; i++) { - let delegatee = await staking.delegates(vesting.address, data.dates[i]); - expect(delegatee).equal(root); - } - }); - - it("should stake 1,000,000 SOV with a duration of 104 weeks and a 26 week cliff", async () => { - // let block = await web3.eth.getBlock("latest"); - let block = await lastBlock(); // ethers.provider.getBlock("latest"); - let timestamp = parseInt(block.timestamp); - - let kickoffTS = await staking.kickoffTS(); - - let start = timestamp + 26 * WEEK; - let end = timestamp + 104 * WEEK; - - let numIntervals = Math.floor((end - start) / (4 * WEEK)) + 1; - let stakedPerInterval = ONE_MILLON / numIntervals; - - // positive case - for (let i = start; i <= end; i += 4 * WEEK) { - let periodFromKickoff = Math.floor((i - kickoffTS.toNumber()) / (2 * WEEK)); - let startBuf = periodFromKickoff * 2 * WEEK + kickoffTS.toNumber(); - let userStakingCheckpoints = await staking.userStakingCheckpoints(vesting.address, startBuf, 0); - - assert.equal(userStakingCheckpoints.fromBlock.toNumber(), block.number); - assert.equal(userStakingCheckpoints.stake.toString(), stakedPerInterval); - - let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints(vesting.address, startBuf); - assert.equal(numUserStakingCheckpoints.toString(), "1"); - } - - // negative cases - - // start-10 to avoid coming to active checkpoint - let periodFromKickoff = Math.floor((start - 10 - kickoffTS.toNumber()) / (2 * WEEK)); - let startBuf = periodFromKickoff * 2 * WEEK + kickoffTS.toNumber(); - let userStakingCheckpoints = await staking.userStakingCheckpoints(vesting.address, startBuf, 0); - - assert.equal(userStakingCheckpoints.fromBlock.toNumber(), 0); - assert.equal(userStakingCheckpoints.stake.toString(), 0); - - let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints(vesting.address, startBuf); - assert.equal(numUserStakingCheckpoints.toString(), "0"); - - periodFromKickoff = Math.floor((end + 1 - kickoffTS.toNumber()) / (2 * WEEK)); - startBuf = periodFromKickoff * 2 * WEEK + kickoffTS.toNumber(); - userStakingCheckpoints = await staking.userStakingCheckpoints(vesting.address, startBuf, 0); - - assert.equal(userStakingCheckpoints.fromBlock.toNumber(), 0); - assert.equal(userStakingCheckpoints.stake.toString(), 0); - - numUserStakingCheckpoints = await staking.numUserStakingCheckpoints(vesting.address, startBuf); - assert.equal(numUserStakingCheckpoints.toString(), "0"); - }); - - it("should stake 2 times 1,000,000 SOV with a duration of 104 weeks and a 26 week cliff", async () => { - let amount = 1000; - let cliff = 28 * WEEK; - let duration = 104 * WEEK; - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - cliff, - duration, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - - await token.approve(vesting.address, amount); - await vesting.stakeTokens(amount); - - let block1 = await web3.eth.getBlock("latest"); - let timestamp1 = block1.timestamp; - - let start = timestamp1 + cliff; - let end = timestamp1 + duration; - - let numIntervals = Math.floor((end - start) / (4 * WEEK)) + 1; - let stakedPerInterval = amount / numIntervals; - - await increaseTime(52 * WEEK); - await token.approve(vesting.address, amount); - await vesting.stakeTokens(amount); - - let block2 = await web3.eth.getBlock("latest"); - let timestamp2 = block2.timestamp; - - let start2 = await staking.timestampToLockDate(timestamp2 + cliff); - let end2 = timestamp2 + duration; - - // positive case - for (let i = start; i <= end2; i += 4 * WEEK) { - let lockedTS = await staking.timestampToLockDate(i); - let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints(vesting.address, lockedTS); - let userStakingCheckpoints = await staking.userStakingCheckpoints(vesting.address, lockedTS, numUserStakingCheckpoints - 1); - if (i < start2 || i > end) { - assert.equal(numUserStakingCheckpoints.toString(), "1"); - assert.equal(userStakingCheckpoints.stake.toString(), stakedPerInterval); - } else { - assert.equal(numUserStakingCheckpoints.toString(), "2"); - assert.equal(userStakingCheckpoints.stake.toString(), stakedPerInterval * 2); - } - } - }); - - it("should stake 1000 tokens with a duration of 34 weeks and a 26 week cliff (dust on rounding)", async () => { - let amount = 1000; - let cliff = 26 * WEEK; - let duration = 34 * WEEK; - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - cliff, - duration, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - - await token.approve(vesting.address, amount); - await vesting.stakeTokens(amount); - - let block = await web3.eth.getBlock("latest"); - let timestamp = block.timestamp; - - let start = timestamp + cliff; - let end = timestamp + duration; - - let numIntervals = Math.floor((end - start) / (4 * WEEK)) + 1; - let stakedPerInterval = Math.floor(amount / numIntervals); - - let stakeForFirstInterval = amount - stakedPerInterval * (numIntervals - 1); - - // positive case - for (let i = start; i <= end; i += 4 * WEEK) { - let periodFromKickoff = Math.floor((i - kickoffTS.toNumber()) / (2 * WEEK)); - let startBuf = periodFromKickoff * 2 * WEEK + kickoffTS.toNumber(); - let userStakingCheckpoints = await staking.userStakingCheckpoints(vesting.address, startBuf, 0); - - assert.equal(userStakingCheckpoints.fromBlock.toNumber(), block.number); - if (i === start) { - assert.equal(userStakingCheckpoints.stake.toString(), stakeForFirstInterval); - } else { - assert.equal(userStakingCheckpoints.stake.toString(), stakedPerInterval); - } - - let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints(vesting.address, startBuf); - assert.equal(numUserStakingCheckpoints.toString(), "1"); - } - }); - }); - - describe("stakeTokensWithApproval", () => { - let vesting; - - it("fails if invoked directly", async () => { - let amount = 1000; - let cliff = 26 * WEEK; - let duration = 34 * WEEK; - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - cliff, - duration, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - await expectRevert(vesting.stakeTokensWithApproval(root, amount), "unauthorized"); - }); - - it("fails if pass wrong method in data", async () => { - let amount = 1000; - let cliff = 26 * WEEK; - let duration = 34 * WEEK; - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - cliff, - duration, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - - let contract = new web3.eth.Contract(vesting.abi, vesting.address); - let sender = root; - let data = contract.methods.stakeTokens(amount).encodeABI(); - - await expectRevert(token.approveAndCall(vesting.address, amount, data, { from: sender }), "method is not allowed"); - }); - - it("should stake 1000 tokens with a duration of 34 weeks and a 26 week cliff (dust on rounding)", async () => { - let amount = 1000; - let cliff = 26 * WEEK; - let duration = 34 * WEEK; - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - cliff, - duration, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - - let contract = new web3.eth.Contract(vesting.abi, vesting.address); - let sender = root; - let data = contract.methods.stakeTokensWithApproval(sender, amount).encodeABI(); - await token.approveAndCall(vesting.address, amount, data, { from: sender }); - - let block = await web3.eth.getBlock("latest"); - let timestamp = block.timestamp; - - let start = timestamp + cliff; - let end = timestamp + duration; - - let numIntervals = Math.floor((end - start) / (4 * WEEK)) + 1; - let stakedPerInterval = Math.floor(amount / numIntervals); - - let stakeForFirstInterval = amount - stakedPerInterval * (numIntervals - 1); - - // positive case - for (let i = start; i <= end; i += 4 * WEEK) { - let periodFromKickoff = Math.floor((i - kickoffTS.toNumber()) / (2 * WEEK)); - let startBuf = periodFromKickoff * 2 * WEEK + kickoffTS.toNumber(); - let userStakingCheckpoints = await staking.userStakingCheckpoints(vesting.address, startBuf, 0); - - assert.equal(userStakingCheckpoints.fromBlock.toNumber(), block.number); - if (i === start) { - assert.equal(userStakingCheckpoints.stake.toString(), stakeForFirstInterval); - } else { - assert.equal(userStakingCheckpoints.stake.toString(), stakedPerInterval); - } - - let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints(vesting.address, startBuf); - assert.equal(numUserStakingCheckpoints.toString(), "1"); - } - }); - }); - - describe("withdrawTokens", () => { - let vesting; - - it("should withdraw unlocked tokens (cliff = 3 weeks)", async () => { - // Save current amount - let previousAmount = await token.balanceOf(root); - let toStake = ONE_MILLON; - - await increaseTime(3 * WEEK); - - // Stake - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - 3 * WEEK, - 3 * WEEK, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - - await token.approve(vesting.address, toStake); - await vesting.stakeTokens(toStake); - - let amountAfterStake = await token.balanceOf(root); - - // time travel - await increaseTime(3 * WEEK); - - // withdraw - let tx = await vesting.withdrawTokens(root); - - // check event - expectEvent(tx, "TokensWithdrawn", { - caller: root, - receiver: root, - }); - - // verify amount - let amount = await token.balanceOf(root); - - assert.equal(previousAmount.sub(new BN(toStake)).toString(), amountAfterStake.toString()); - assert.equal(previousAmount.toString(), amount.toString()); - }); - - it("should withdraw unlocked tokens", async () => { - // Save current amount - let previousAmount = await token.balanceOf(root); - let toStake = ONE_MILLON; - - // Stake - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - 26 * WEEK, - 104 * WEEK, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - - await token.approve(vesting.address, toStake); - await vesting.stakeTokens(toStake); - - let amountAfterStake = await token.balanceOf(root); - - // time travel - await increaseTime(104 * WEEK); - - // withdraw - let tx = await vesting.withdrawTokens(root); - - // check event - expectEvent(tx, "TokensWithdrawn", { - caller: root, - receiver: root, - }); - - // verify amount - let amount = await token.balanceOf(root); - - assert.equal(previousAmount.sub(new BN(toStake)).toString(), amountAfterStake.toString()); - assert.equal(previousAmount.toString(), amount.toString()); - }); - - it("should withdraw unlocked tokens for 2 stakes", async () => { - // Save current amount - let previousAmount = await token.balanceOf(root); - let toStake = ONE_MILLON; - - // Stake - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - 16 * WEEK, - 34 * WEEK, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - - await token.approve(vesting.address, toStake); - await vesting.stakeTokens(toStake); - - await increaseTime(20 * WEEK); - await token.approve(vesting.address, toStake); - await vesting.stakeTokens(toStake); - - let amountAfterStake = await token.balanceOf(root); - - // time travel - await increaseTime(34 * WEEK); - - // withdraw - let tx = await vesting.withdrawTokens(root); - - // check event - expectEvent(tx, "TokensWithdrawn", { - caller: root, - receiver: root, - }); - - // verify amount - let amount = await token.balanceOf(root); - - assert.equal(previousAmount.sub(new BN(toStake).mul(new BN(2))).toString(), amountAfterStake.toString()); - assert.equal(previousAmount.toString(), amount.toString()); - }); - - it("should withdraw unlocked tokens for 2 stakes (current time >= last locking date of the second stake)", async () => { - // Save current amount - let previousAmount = await token.balanceOf(root); - let toStake = ONE_MILLON; - - // Stake - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - 4 * WEEK, - 20 * WEEK, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - - await token.approve(vesting.address, toStake); - await vesting.stakeTokens(toStake); - - await increaseTime(2 * WEEK); - await token.approve(vesting.address, toStake); - await vesting.stakeTokens(toStake); - - let amountAfterStake = await token.balanceOf(root); - - // time travel - await increaseTime(20 * WEEK); - - // withdraw - let tx = await vesting.withdrawTokens(root); - - // check event - expectEvent(tx, "TokensWithdrawn", { - caller: root, - receiver: root, - }); - - // verify amount - let amount = await token.balanceOf(root); - - assert.equal(previousAmount.sub(new BN(toStake).mul(new BN(2))).toString(), amountAfterStake.toString()); - assert.equal(previousAmount.toString(), amount.toString()); - }); - - it("should withdraw unlocked tokens for 2 stakes (shouldn't withdraw the latest stake)", async () => { - // Save current amount - let previousAmount = await token.balanceOf(root); - let toStake = ONE_MILLON; - - // Stake - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - 4 * WEEK, - 20 * WEEK, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - - await token.approve(vesting.address, toStake); - await vesting.stakeTokens(toStake); - - await increaseTime(2 * WEEK); - await token.approve(vesting.address, toStake); - await vesting.stakeTokens(toStake); - - let amountAfterStake = await token.balanceOf(root); - - // time travel - await increaseTime(18 * WEEK); - - // withdraw - await vesting.withdrawTokens(root); - - let stakes = await staking.getStakes(vesting.address); - expect(stakes.dates.length).equal(1); - }); - - it("should do nothing if withdrawing a second time", async () => { - // This part should be tested on staking contract, function getPriorUserStakeByDate - let previousAmount = await token.balanceOf(root); - await vesting.withdrawTokens(root); - let amount = await token.balanceOf(root); - - assert.equal(previousAmount.toString(), amount.toString()); - }); - - it("should do nothing if withdrawing before reaching the cliff", async () => { - let toStake = ONE_MILLON; - - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - a1, - 26 * WEEK, - 104 * WEEK, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - - let previousAmount = await token.balanceOf(root); - - await token.approve(vesting.address, toStake); - await vesting.stakeTokens(toStake); - - let amountAfterStake = await token.balanceOf(root); - - // time travel - await increaseTime(25 * WEEK); - - await vesting.withdrawTokens(root, { from: a1 }); - let amount = await token.balanceOf(root); - - assert.equal(previousAmount.sub(new BN(toStake)).toString(), amountAfterStake.toString()); - assert.equal(amountAfterStake.toString(), amount.toString()); - }); - - it("should fail if the caller is neither owner nor token owner", async () => { - await expectRevert(vesting.withdrawTokens(root, { from: a2 }), "unauthorized"); - await expectRevert(vesting.withdrawTokens(root, { from: a3 }), "unauthorized"); - - await vesting.withdrawTokens(root, { from: root }); - await vesting.withdrawTokens(root, { from: a1 }); - }); - - it("Shouldn't be possible to use governanceWithdrawVesting by not owner", async () => { - let toStake = ONE_MILLON; - - // Stake - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - 26 * WEEK, - 104 * WEEK, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - - await token.approve(vesting.address, toStake); - await vesting.stakeTokens(toStake); - - await expectRevert(staking.governanceWithdrawVesting(vesting.address, root, { from: a1 }), "unauthorized"); - }); - - it("Shouldn't be possible to use governanceWithdraw by user", async () => { - let toStake = ONE_MILLON; - - // Stake - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - 26 * WEEK, - 104 * WEEK, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - - await token.approve(vesting.address, toStake); - await vesting.stakeTokens(toStake); - - await expectRevert(staking.governanceWithdraw(100, kickoffTS.toNumber() + 52 * WEEK, root), "unauthorized"); - }); - - it("Shouldn't be possible to use governanceWithdrawTokens by user", async () => { - let toStake = ONE_MILLON; - - // Stake - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - 26 * WEEK, - 104 * WEEK, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - - await token.approve(vesting.address, toStake); - await vesting.stakeTokens(toStake); - - await expectRevert(vesting.governanceWithdrawTokens(root), "unauthorized"); - }); - - it("governanceWithdrawTokens", async () => { - let previousAmount = await token.balanceOf(root); - let toStake = ONE_MILLON; - - // Stake - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - root, - 16 * WEEK, - 36 * WEEK, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - - await token.approve(vesting.address, toStake); - await vesting.stakeTokens(toStake); - - await increaseTime(20 * WEEK); - await token.approve(vesting.address, toStake); - await vesting.stakeTokens(toStake); - - let amountAfterStake = await token.balanceOf(root); - - await staking.addAdmin(a1); - // governance withdraw until duration must withdraw all staked tokens without fees - let tx = await staking.governanceWithdrawVesting(vesting.address, root, { from: a1 }); - - expectEvent(tx, "VestingTokensWithdrawn", { - vesting: vesting.address, - receiver: root, - }); - - // verify amount - let amount = await token.balanceOf(root); - - assert.equal(previousAmount.sub(new BN(toStake).mul(new BN(2))).toString(), amountAfterStake.toString()); - assert.equal(previousAmount.toString(), amount.toString()); - - let vestingBalance = await staking.balanceOf(vesting.address); - expect(vestingBalance).to.be.bignumber.equal(new BN(0)); - }); - }); - - describe("collectDividends", async () => { - it("should fail if the caller is neither owner nor token owner", async () => { - let vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - a1, - 26 * WEEK, - 104 * WEEK, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - await expectRevert(vesting.collectDividends(root, 10, a1, { from: a2 }), "unauthorized"); - await expectRevert(vesting.collectDividends(root, 10, a1, { from: a3 }), "unauthorized"); - }); - - it("should collect dividends", async () => { - let vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - a1, - 26 * WEEK, - 104 * WEEK, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - - let maxCheckpoints = new BN(10); - let tx = await vesting.collectDividends(a1, maxCheckpoints, a2); - - let testData = await feeSharingProxy.testData.call(); - expect(testData.loanPoolToken).to.be.equal(a1); - expect(testData.maxCheckpoints).to.be.bignumber.equal(maxCheckpoints); - expect(testData.receiver).to.be.equal(a2); - - expectEvent(tx, "DividendsCollected", { - caller: root, - loanPoolToken: a1, - receiver: a2, - maxCheckpoints: maxCheckpoints, - }); - }); - }); - - describe("migrateToNewStakingContract", async () => { - let vesting; - it("should set the new staking contract", async () => { - vesting = await Vesting.new( - vestingLogic.address, - token.address, - staking.address, - a1, - 26 * WEEK, - 104 * WEEK, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - // 1. set new staking contract address on staking contract - - let newStaking = await StakingProxy.new(token.address); - await newStaking.setImplementation(stakingLogic.address); - newStaking = await StakingLogic.at(newStaking.address); - - await staking.setNewStakingContract(newStaking.address); - - // 2. call migrateToNewStakingContract - let tx = await vesting.migrateToNewStakingContract(); - expectEvent(tx, "MigratedToNewStakingContract", { - caller: root, - newStakingContract: newStaking.address, - }); - let _staking = await vesting.staking(); - assert.equal(_staking, newStaking.address); - }); - - it("should fail if there is no new staking contract set", async () => { - let newStaking = await StakingProxy.new(token.address); - await newStaking.setImplementation(stakingLogic.address); - newStaking = await StakingLogic.at(newStaking.address); - - vesting = await Vesting.new( - vestingLogic.address, - token.address, - newStaking.address, - a1, - 26 * WEEK, - 104 * WEEK, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - await expectRevert(vesting.migrateToNewStakingContract(), "there is no new staking contract set"); - }); - - it("should fail if the caller is neither owner nor token owner", async () => { - let newStaking = await StakingProxy.new(token.address); - await newStaking.setImplementation(stakingLogic.address); - newStaking = await StakingLogic.at(newStaking.address); - - vesting = await Vesting.new( - vestingLogic.address, - token.address, - newStaking.address, - a1, - 26 * WEEK, - 104 * WEEK, - feeSharingProxy.address - ); - vesting = await VestingLogic.at(vesting.address); - - await newStaking.setNewStakingContract(newStaking.address); - - await expectRevert(vesting.migrateToNewStakingContract({ from: a2 }), "unauthorized"); - await expectRevert(vesting.migrateToNewStakingContract({ from: a3 }), "unauthorized"); - - await vesting.migrateToNewStakingContract(); - await vesting.migrateToNewStakingContract({ from: a1 }); - }); - }); + const name = "Test token"; + const symbol = "TST"; + + let root, a1, a2, a3; + let token, staking, stakingLogic, feeSharingProxy; + let vestingLogic; + let kickoffTS; + + let cliff = "10"; + let duration = "100"; + + before(async () => { + [root, a1, a2, a3, ...accounts] = accounts; + token = await SOV.new(TOTAL_SUPPLY); + wrbtc = await TestWrbtc.new(); + + vestingLogic = await VestingLogic.new(); + + feeSharingProxy = await FeeSharingProxy.new( + constants.ZERO_ADDRESS, + constants.ZERO_ADDRESS + ); + + stakingLogic = await StakingLogic.new(token.address); + staking = await StakingProxy.new(token.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); + //Upgradable Vesting Registry + vestingRegistryLogic = await VestingRegistryLogic.new(); + vestingReg = await VestingRegistryProxy.new(); + await vestingReg.setImplementation(vestingRegistryLogic.address); + vestingReg = await VestingRegistryLogic.at(vestingReg.address); + await staking.setVestingRegistry(vestingReg.address); + + await token.transfer(a2, "1000"); + await token.approve(staking.address, "1000", { from: a2 }); + + kickoffTS = await staking.kickoffTS.call(); + }); + + describe("constructor", () => { + it("sets the expected values", async () => { + let vestingInstance = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + cliff, + duration, + feeSharingProxy.address + ); + vestingInstance = await VestingLogic.at(vestingInstance.address); + + // Check data + let _sov = await vestingInstance.SOV(); + let _stackingAddress = await vestingInstance.staking(); + let _tokenOwner = await vestingInstance.tokenOwner(); + let _cliff = await vestingInstance.cliff(); + let _duration = await vestingInstance.duration(); + let _feeSharingProxy = await vestingInstance.feeSharingProxy(); + + assert.equal(_sov, token.address); + assert.equal(_stackingAddress, staking.address); + assert.equal(_tokenOwner, root); + assert.equal(_cliff.toString(), cliff); + assert.equal(_duration.toString(), duration); + assert.equal(_feeSharingProxy, feeSharingProxy.address); + }); + + it("fails if the 0 address is passed as SOV address", async () => { + await expectRevert( + Vesting.new( + vestingLogic.address, + constants.ZERO_ADDRESS, + staking.address, + root, + cliff, + duration, + feeSharingProxy.address + ), + + "SOV address invalid" + ); + }); + + it("fails if the 0 address is passed as token owner address", async () => { + await expectRevert( + Vesting.new( + vestingLogic.address, + token.address, + staking.address, + constants.ZERO_ADDRESS, + cliff, + duration, + feeSharingProxy.address + ), + "token owner address invalid" + ); + }); + + it("fails if the 0 address is passed as staking address", async () => { + await expectRevert( + Vesting.new( + vestingLogic.address, + token.address, + constants.ZERO_ADDRESS, + root, + cliff, + duration, + feeSharingProxy.address + ), + "staking address invalid" + ); + }); + + it("fails if the vesting duration is bigger than the max staking duration", async () => { + await expectRevert( + Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + cliff, + MAX_DURATION.add(new BN(1)), + feeSharingProxy.address + ), + "duration may not exceed the max duration" + ); + }); + + it("fails if the vesting duration is shorter than the cliff", async () => { + await expectRevert( + Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + 100, + 99, + feeSharingProxy.address + ), + "duration must be bigger than or equal to the cliff" + ); + }); + + it("fails if the 0 address is passed as feeSharingProxy address", async () => { + await expectRevert( + Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + cliff, + duration, + constants.ZERO_ADDRESS + ), + "feeSharingProxy address invalid" + ); + }); + }); + + describe("delegate", () => { + let vesting; + it("should stake tokens 2 times and delegate voting power", async () => { + let toStake = ONE_MILLON; + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + a2, + 16 * WEEK, + 26 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await token.approve(vesting.address, toStake); + await vesting.stakeTokens(toStake); + + await increaseTime(20 * WEEK); + await token.approve(vesting.address, toStake); + await vesting.stakeTokens(toStake); + + // check delegatee + let data = await staking.getStakes.call(vesting.address); + /// @dev Optimization: This loop through 40 steps is a bottleneck + for (let i = 0; i < data.dates.length; i++) { + let delegatee = await staking.delegates(vesting.address, data.dates[i]); + expect(delegatee).equal(a2); + } + + // delegate + let tx = await vesting.delegate(a1, { from: a2 }); + + expectEvent(tx, "VotesDelegated", { + caller: a2, + delegatee: a1, + }); + + // check new delegatee + data = await staking.getStakes.call(vesting.address); + /// @dev Optimization: This loop through 40 steps is a bottleneck + for (let i = 0; i < data.dates.length; i++) { + let delegatee = await staking.delegates(vesting.address, data.dates[i]); + expect(delegatee).equal(a1); + } + }); + + it("should stake tokens 1 time and delegate voting power (using vesting logic with bug in delegation)", async () => { + let toStake = ONE_MILLON; + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + a2, + 16 * WEEK, + 26 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await token.approve(vesting.address, toStake); + await vesting.stakeTokens(toStake); + + // check delegatee + let data = await staking.getStakes.call(vesting.address); + for (let i = 0; i < data.dates.length; i++) { + let delegatee = await staking.delegates(vesting.address, data.dates[i]); + expect(delegatee).equal(a2); + } + + // delegate + let tx = await vesting.delegate(a1, { from: a2 }); + + expectEvent(tx, "VotesDelegated", { + caller: a2, + delegatee: a1, + }); + + // check new delegatee + data = await staking.getStakes.call(vesting.address); + for (let i = 0; i < data.dates.length; i++) { + let delegatee = await staking.delegates(vesting.address, data.dates[i]); + expect(delegatee).equal(a1); + } + }); + + it("fails if delegatee is zero address", async () => { + await expectRevert( + vesting.delegate(constants.ZERO_ADDRESS, { from: a2 }), + "delegatee address invalid" + ); + }); + + it("fails if not a token owner", async () => { + await expectRevert(vesting.delegate(a1, { from: a1 }), "unauthorized"); + }); + }); + + describe("stakeTokens; using Ganache", () => { + let vesting; + it("should stake 1,000,000 SOV with a duration of 104 weeks and a 26 week cliff", async () => { + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + 26 * WEEK, + 104 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + await token.approve(vesting.address, ONE_MILLON); + let tx = await vesting.stakeTokens(ONE_MILLON); + + expectEvent(tx, "TokensStaked", { + caller: root, + amount: ONE_MILLON, + }); + + // check delegatee + let data = await staking.getStakes.call(vesting.address); + for (let i = 0; i < data.dates.length; i++) { + let delegatee = await staking.delegates(vesting.address, data.dates[i]); + expect(delegatee).equal(root); + } + }); + + it("should stake 1,000,000 SOV with a duration of 104 weeks and a 26 week cliff", async () => { + // let block = await web3.eth.getBlock("latest"); + let block = await lastBlock(); // ethers.provider.getBlock("latest"); + let timestamp = parseInt(block.timestamp); + + let kickoffTS = await staking.kickoffTS(); + + let start = timestamp + 26 * WEEK; + let end = timestamp + 104 * WEEK; + + let numIntervals = Math.floor((end - start) / (4 * WEEK)) + 1; + let stakedPerInterval = ONE_MILLON / numIntervals; + + // positive case + for (let i = start; i <= end; i += 4 * WEEK) { + let periodFromKickoff = Math.floor((i - kickoffTS.toNumber()) / (2 * WEEK)); + let startBuf = periodFromKickoff * 2 * WEEK + kickoffTS.toNumber(); + let userStakingCheckpoints = await staking.userStakingCheckpoints( + vesting.address, + startBuf, + 0 + ); + + assert.equal(userStakingCheckpoints.fromBlock.toNumber(), block.number); + assert.equal(userStakingCheckpoints.stake.toString(), stakedPerInterval); + + let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints( + vesting.address, + startBuf + ); + assert.equal(numUserStakingCheckpoints.toString(), "1"); + } + + // negative cases + + // start-10 to avoid coming to active checkpoint + let periodFromKickoff = Math.floor((start - 10 - kickoffTS.toNumber()) / (2 * WEEK)); + let startBuf = periodFromKickoff * 2 * WEEK + kickoffTS.toNumber(); + let userStakingCheckpoints = await staking.userStakingCheckpoints( + vesting.address, + startBuf, + 0 + ); + + assert.equal(userStakingCheckpoints.fromBlock.toNumber(), 0); + assert.equal(userStakingCheckpoints.stake.toString(), 0); + + let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints( + vesting.address, + startBuf + ); + assert.equal(numUserStakingCheckpoints.toString(), "0"); + + periodFromKickoff = Math.floor((end + 1 - kickoffTS.toNumber()) / (2 * WEEK)); + startBuf = periodFromKickoff * 2 * WEEK + kickoffTS.toNumber(); + userStakingCheckpoints = await staking.userStakingCheckpoints( + vesting.address, + startBuf, + 0 + ); + + assert.equal(userStakingCheckpoints.fromBlock.toNumber(), 0); + assert.equal(userStakingCheckpoints.stake.toString(), 0); + + numUserStakingCheckpoints = await staking.numUserStakingCheckpoints( + vesting.address, + startBuf + ); + assert.equal(numUserStakingCheckpoints.toString(), "0"); + }); + + it("should stake 2 times 1,000,000 SOV with a duration of 104 weeks and a 26 week cliff", async () => { + let amount = 1000; + let cliff = 28 * WEEK; + let duration = 104 * WEEK; + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + cliff, + duration, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await token.approve(vesting.address, amount); + await vesting.stakeTokens(amount); + + let block1 = await web3.eth.getBlock("latest"); + let timestamp1 = block1.timestamp; + + let start = timestamp1 + cliff; + let end = timestamp1 + duration; + + let numIntervals = Math.floor((end - start) / (4 * WEEK)) + 1; + let stakedPerInterval = amount / numIntervals; + + await increaseTime(52 * WEEK); + await token.approve(vesting.address, amount); + await vesting.stakeTokens(amount); + + let block2 = await web3.eth.getBlock("latest"); + let timestamp2 = block2.timestamp; + + let start2 = await staking.timestampToLockDate(timestamp2 + cliff); + let end2 = timestamp2 + duration; + + // positive case + for (let i = start; i <= end2; i += 4 * WEEK) { + let lockedTS = await staking.timestampToLockDate(i); + let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints( + vesting.address, + lockedTS + ); + let userStakingCheckpoints = await staking.userStakingCheckpoints( + vesting.address, + lockedTS, + numUserStakingCheckpoints - 1 + ); + if (i < start2 || i > end) { + assert.equal(numUserStakingCheckpoints.toString(), "1"); + assert.equal(userStakingCheckpoints.stake.toString(), stakedPerInterval); + } else { + assert.equal(numUserStakingCheckpoints.toString(), "2"); + assert.equal(userStakingCheckpoints.stake.toString(), stakedPerInterval * 2); + } + } + }); + + it("should stake 1000 tokens with a duration of 34 weeks and a 26 week cliff (dust on rounding)", async () => { + let amount = 1000; + let cliff = 26 * WEEK; + let duration = 34 * WEEK; + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + cliff, + duration, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await token.approve(vesting.address, amount); + await vesting.stakeTokens(amount); + + let block = await web3.eth.getBlock("latest"); + let timestamp = block.timestamp; + + let start = timestamp + cliff; + let end = timestamp + duration; + + let numIntervals = Math.floor((end - start) / (4 * WEEK)) + 1; + let stakedPerInterval = Math.floor(amount / numIntervals); + + let stakeForFirstInterval = amount - stakedPerInterval * (numIntervals - 1); + + // positive case + for (let i = start; i <= end; i += 4 * WEEK) { + let periodFromKickoff = Math.floor((i - kickoffTS.toNumber()) / (2 * WEEK)); + let startBuf = periodFromKickoff * 2 * WEEK + kickoffTS.toNumber(); + let userStakingCheckpoints = await staking.userStakingCheckpoints( + vesting.address, + startBuf, + 0 + ); + + assert.equal(userStakingCheckpoints.fromBlock.toNumber(), block.number); + if (i === start) { + assert.equal(userStakingCheckpoints.stake.toString(), stakeForFirstInterval); + } else { + assert.equal(userStakingCheckpoints.stake.toString(), stakedPerInterval); + } + + let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints( + vesting.address, + startBuf + ); + assert.equal(numUserStakingCheckpoints.toString(), "1"); + } + }); + }); + + describe("stakeTokensWithApproval", () => { + let vesting; + + it("fails if invoked directly", async () => { + let amount = 1000; + let cliff = 26 * WEEK; + let duration = 34 * WEEK; + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + cliff, + duration, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + await expectRevert(vesting.stakeTokensWithApproval(root, amount), "unauthorized"); + }); + + it("fails if pass wrong method in data", async () => { + let amount = 1000; + let cliff = 26 * WEEK; + let duration = 34 * WEEK; + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + cliff, + duration, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + let contract = new web3.eth.Contract(vesting.abi, vesting.address); + let sender = root; + let data = contract.methods.stakeTokens(amount).encodeABI(); + + await expectRevert( + token.approveAndCall(vesting.address, amount, data, { from: sender }), + "method is not allowed" + ); + }); + + it("should stake 1000 tokens with a duration of 34 weeks and a 26 week cliff (dust on rounding)", async () => { + let amount = 1000; + let cliff = 26 * WEEK; + let duration = 34 * WEEK; + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + cliff, + duration, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + let contract = new web3.eth.Contract(vesting.abi, vesting.address); + let sender = root; + let data = contract.methods.stakeTokensWithApproval(sender, amount).encodeABI(); + await token.approveAndCall(vesting.address, amount, data, { from: sender }); + + let block = await web3.eth.getBlock("latest"); + let timestamp = block.timestamp; + + let start = timestamp + cliff; + let end = timestamp + duration; + + let numIntervals = Math.floor((end - start) / (4 * WEEK)) + 1; + let stakedPerInterval = Math.floor(amount / numIntervals); + + let stakeForFirstInterval = amount - stakedPerInterval * (numIntervals - 1); + + // positive case + for (let i = start; i <= end; i += 4 * WEEK) { + let periodFromKickoff = Math.floor((i - kickoffTS.toNumber()) / (2 * WEEK)); + let startBuf = periodFromKickoff * 2 * WEEK + kickoffTS.toNumber(); + let userStakingCheckpoints = await staking.userStakingCheckpoints( + vesting.address, + startBuf, + 0 + ); + + assert.equal(userStakingCheckpoints.fromBlock.toNumber(), block.number); + if (i === start) { + assert.equal(userStakingCheckpoints.stake.toString(), stakeForFirstInterval); + } else { + assert.equal(userStakingCheckpoints.stake.toString(), stakedPerInterval); + } + + let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints( + vesting.address, + startBuf + ); + assert.equal(numUserStakingCheckpoints.toString(), "1"); + } + }); + }); + + describe("withdrawTokens", () => { + let vesting; + + it("should withdraw unlocked tokens (cliff = 3 weeks)", async () => { + // Save current amount + let previousAmount = await token.balanceOf(root); + let toStake = ONE_MILLON; + + await increaseTime(3 * WEEK); + + // Stake + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + 3 * WEEK, + 3 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await token.approve(vesting.address, toStake); + await vesting.stakeTokens(toStake); + + let amountAfterStake = await token.balanceOf(root); + + // time travel + await increaseTime(3 * WEEK); + + // withdraw + let tx = await vesting.withdrawTokens(root); + + // check event + expectEvent(tx, "TokensWithdrawn", { + caller: root, + receiver: root, + }); + + // verify amount + let amount = await token.balanceOf(root); + + assert.equal( + previousAmount.sub(new BN(toStake)).toString(), + amountAfterStake.toString() + ); + assert.equal(previousAmount.toString(), amount.toString()); + }); + + it("should withdraw unlocked tokens", async () => { + // Save current amount + let previousAmount = await token.balanceOf(root); + let toStake = ONE_MILLON; + + // Stake + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + 26 * WEEK, + 104 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await token.approve(vesting.address, toStake); + await vesting.stakeTokens(toStake); + + let amountAfterStake = await token.balanceOf(root); + + // time travel + await increaseTime(104 * WEEK); + + // withdraw + let tx = await vesting.withdrawTokens(root); + + // check event + expectEvent(tx, "TokensWithdrawn", { + caller: root, + receiver: root, + }); + + // verify amount + let amount = await token.balanceOf(root); + + assert.equal( + previousAmount.sub(new BN(toStake)).toString(), + amountAfterStake.toString() + ); + assert.equal(previousAmount.toString(), amount.toString()); + }); + + it("should withdraw unlocked tokens for 2 stakes", async () => { + // Save current amount + let previousAmount = await token.balanceOf(root); + let toStake = ONE_MILLON; + + // Stake + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + 16 * WEEK, + 34 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await token.approve(vesting.address, toStake); + await vesting.stakeTokens(toStake); + + await increaseTime(20 * WEEK); + await token.approve(vesting.address, toStake); + await vesting.stakeTokens(toStake); + + let amountAfterStake = await token.balanceOf(root); + + // time travel + await increaseTime(34 * WEEK); + + // withdraw + let tx = await vesting.withdrawTokens(root); + + // check event + expectEvent(tx, "TokensWithdrawn", { + caller: root, + receiver: root, + }); + + // verify amount + let amount = await token.balanceOf(root); + + assert.equal( + previousAmount.sub(new BN(toStake).mul(new BN(2))).toString(), + amountAfterStake.toString() + ); + assert.equal(previousAmount.toString(), amount.toString()); + }); + + it("should withdraw unlocked tokens for 2 stakes (current time >= last locking date of the second stake)", async () => { + // Save current amount + let previousAmount = await token.balanceOf(root); + let toStake = ONE_MILLON; + + // Stake + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + 4 * WEEK, + 20 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await token.approve(vesting.address, toStake); + await vesting.stakeTokens(toStake); + + await increaseTime(2 * WEEK); + await token.approve(vesting.address, toStake); + await vesting.stakeTokens(toStake); + + let amountAfterStake = await token.balanceOf(root); + + // time travel + await increaseTime(20 * WEEK); + + // withdraw + let tx = await vesting.withdrawTokens(root); + + // check event + expectEvent(tx, "TokensWithdrawn", { + caller: root, + receiver: root, + }); + + // verify amount + let amount = await token.balanceOf(root); + + assert.equal( + previousAmount.sub(new BN(toStake).mul(new BN(2))).toString(), + amountAfterStake.toString() + ); + assert.equal(previousAmount.toString(), amount.toString()); + }); + + it("should withdraw unlocked tokens for 2 stakes (shouldn't withdraw the latest stake)", async () => { + // Save current amount + let previousAmount = await token.balanceOf(root); + let toStake = ONE_MILLON; + + // Stake + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + 4 * WEEK, + 20 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await token.approve(vesting.address, toStake); + await vesting.stakeTokens(toStake); + + await increaseTime(2 * WEEK); + await token.approve(vesting.address, toStake); + await vesting.stakeTokens(toStake); + + let amountAfterStake = await token.balanceOf(root); + + // time travel + await increaseTime(18 * WEEK); + + // withdraw + await vesting.withdrawTokens(root); + + let stakes = await staking.getStakes(vesting.address); + expect(stakes.dates.length).equal(1); + }); + + it("should do nothing if withdrawing a second time", async () => { + // This part should be tested on staking contract, function getPriorUserStakeByDate + let previousAmount = await token.balanceOf(root); + await vesting.withdrawTokens(root); + let amount = await token.balanceOf(root); + + assert.equal(previousAmount.toString(), amount.toString()); + }); + + it("should do nothing if withdrawing before reaching the cliff", async () => { + let toStake = ONE_MILLON; + + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + a1, + 26 * WEEK, + 104 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + let previousAmount = await token.balanceOf(root); + + await token.approve(vesting.address, toStake); + await vesting.stakeTokens(toStake); + + let amountAfterStake = await token.balanceOf(root); + + // time travel + await increaseTime(25 * WEEK); + + await vesting.withdrawTokens(root, { from: a1 }); + let amount = await token.balanceOf(root); + + assert.equal( + previousAmount.sub(new BN(toStake)).toString(), + amountAfterStake.toString() + ); + assert.equal(amountAfterStake.toString(), amount.toString()); + }); + + it("should fail if the caller is neither owner nor token owner", async () => { + await expectRevert(vesting.withdrawTokens(root, { from: a2 }), "unauthorized"); + await expectRevert(vesting.withdrawTokens(root, { from: a3 }), "unauthorized"); + + await vesting.withdrawTokens(root, { from: root }); + await vesting.withdrawTokens(root, { from: a1 }); + }); + + it("Shouldn't be possible to use governanceWithdrawVesting by not owner", async () => { + let toStake = ONE_MILLON; + + // Stake + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + 26 * WEEK, + 104 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await token.approve(vesting.address, toStake); + await vesting.stakeTokens(toStake); + + await expectRevert( + staking.governanceWithdrawVesting(vesting.address, root, { from: a1 }), + "unauthorized" + ); + }); + + it("Shouldn't be possible to use governanceWithdraw by user", async () => { + let toStake = ONE_MILLON; + + // Stake + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + 26 * WEEK, + 104 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await token.approve(vesting.address, toStake); + await vesting.stakeTokens(toStake); + + await expectRevert( + staking.governanceWithdraw(100, kickoffTS.toNumber() + 52 * WEEK, root), + "unauthorized" + ); + }); + + it("Shouldn't be possible to use governanceWithdrawTokens by user", async () => { + let toStake = ONE_MILLON; + + // Stake + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + 26 * WEEK, + 104 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await token.approve(vesting.address, toStake); + await vesting.stakeTokens(toStake); + + await expectRevert(vesting.governanceWithdrawTokens(root), "unauthorized"); + }); + + it("governanceWithdrawTokens", async () => { + let previousAmount = await token.balanceOf(root); + let toStake = ONE_MILLON; + + // Stake + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + 16 * WEEK, + 36 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await token.approve(vesting.address, toStake); + await vesting.stakeTokens(toStake); + + await increaseTime(20 * WEEK); + await token.approve(vesting.address, toStake); + await vesting.stakeTokens(toStake); + + let amountAfterStake = await token.balanceOf(root); + + await staking.addAdmin(a1); + // governance withdraw until duration must withdraw all staked tokens without fees + let tx = await staking.governanceWithdrawVesting(vesting.address, root, { from: a1 }); + + expectEvent(tx, "VestingTokensWithdrawn", { + vesting: vesting.address, + receiver: root, + }); + + // verify amount + let amount = await token.balanceOf(root); + + assert.equal( + previousAmount.sub(new BN(toStake).mul(new BN(2))).toString(), + amountAfterStake.toString() + ); + assert.equal(previousAmount.toString(), amount.toString()); + + let vestingBalance = await staking.balanceOf(vesting.address); + expect(vestingBalance).to.be.bignumber.equal(new BN(0)); + }); + }); + + describe("collectDividends", async () => { + it("should fail if the caller is neither owner nor token owner", async () => { + let vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + a1, + 26 * WEEK, + 104 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + await expectRevert( + vesting.collectDividends(root, 10, a1, { from: a2 }), + "unauthorized" + ); + await expectRevert( + vesting.collectDividends(root, 10, a1, { from: a3 }), + "unauthorized" + ); + }); + + it("should collect dividends", async () => { + let vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + a1, + 26 * WEEK, + 104 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + let maxCheckpoints = new BN(10); + let tx = await vesting.collectDividends(a1, maxCheckpoints, a2); + + let testData = await feeSharingProxy.testData.call(); + expect(testData.loanPoolToken).to.be.equal(a1); + expect(testData.maxCheckpoints).to.be.bignumber.equal(maxCheckpoints); + expect(testData.receiver).to.be.equal(a2); + + expectEvent(tx, "DividendsCollected", { + caller: root, + loanPoolToken: a1, + receiver: a2, + maxCheckpoints: maxCheckpoints, + }); + }); + }); + + describe("migrateToNewStakingContract", async () => { + let vesting; + it("should set the new staking contract", async () => { + vesting = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + a1, + 26 * WEEK, + 104 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + // 1. set new staking contract address on staking contract + + let newStaking = await StakingProxy.new(token.address); + await newStaking.setImplementation(stakingLogic.address); + newStaking = await StakingLogic.at(newStaking.address); + + await staking.setNewStakingContract(newStaking.address); + + // 2. call migrateToNewStakingContract + let tx = await vesting.migrateToNewStakingContract(); + expectEvent(tx, "MigratedToNewStakingContract", { + caller: root, + newStakingContract: newStaking.address, + }); + let _staking = await vesting.staking(); + assert.equal(_staking, newStaking.address); + }); + + it("should fail if there is no new staking contract set", async () => { + let newStaking = await StakingProxy.new(token.address); + await newStaking.setImplementation(stakingLogic.address); + newStaking = await StakingLogic.at(newStaking.address); + + vesting = await Vesting.new( + vestingLogic.address, + token.address, + newStaking.address, + a1, + 26 * WEEK, + 104 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + await expectRevert( + vesting.migrateToNewStakingContract(), + "there is no new staking contract set" + ); + }); + + it("should fail if the caller is neither owner nor token owner", async () => { + let newStaking = await StakingProxy.new(token.address); + await newStaking.setImplementation(stakingLogic.address); + newStaking = await StakingLogic.at(newStaking.address); + + vesting = await Vesting.new( + vestingLogic.address, + token.address, + newStaking.address, + a1, + 26 * WEEK, + 104 * WEEK, + feeSharingProxy.address + ); + vesting = await VestingLogic.at(vesting.address); + + await newStaking.setNewStakingContract(newStaking.address); + + await expectRevert(vesting.migrateToNewStakingContract({ from: a2 }), "unauthorized"); + await expectRevert(vesting.migrateToNewStakingContract({ from: a3 }), "unauthorized"); + + await vesting.migrateToNewStakingContract(); + await vesting.migrateToNewStakingContract({ from: a1 }); + }); + }); }); diff --git a/tests/vesting/VestingCreator.js b/tests/vesting/VestingCreator.js index 62aee7614..f2f5d3619 100644 --- a/tests/vesting/VestingCreator.js +++ b/tests/vesting/VestingCreator.js @@ -39,857 +39,1008 @@ const MAX_PERIOD = 8; const pricsSats = "2500"; contract("VestingCreator", (accounts) => { - let root, account1, account2, account3, account4; - let SOV, lockedSOV; - let vesting, vestingLogic, vestingRegistryLogic; - let vestingCreator; - let vestingRegistry, vestingRegistry2, vestingRegistry3; - - let cliff = 1; // This is in 4 weeks. i.e. 1 * 4 weeks. - let duration = 11; // This is in 4 weeks. i.e. 11 * 4 weeks. - - async function deploymentAndInitFixture(_wallets, _provider) { - SOV = await SOV_ABI.new(TOTAL_SUPPLY); - cSOV1 = await TestToken.new("cSOV1", "cSOV1", 18, TOTAL_SUPPLY); - cSOV2 = await TestToken.new("cSOV2", "cSOV2", 18, TOTAL_SUPPLY); - - stakingLogic = await StakingLogic.new(); - staking = await StakingProxy.new(SOV.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingLogic.at(staking.address); - - feeSharingProxy = await FeeSharingProxy.new(ZERO_ADDRESS, staking.address); - - vestingLogic = await VestingLogic.new(); - vestingFactory = await VestingFactory.new(vestingLogic.address); - - vestingRegistryLogic = await VestingRegistryLogic.new(); - vesting = await VestingRegistryProxy.new(); - await vesting.setImplementation(vestingRegistryLogic.address); - vesting = await VestingRegistryLogic.at(vesting.address); - vestingFactory.transferOwnership(vesting.address); - - vestingCreator = await VestingCreator.new(SOV.address, vesting.address); - - lockedSOV = await LockedSOV.new(SOV.address, vesting.address, cliff, duration, [root]); - - vestingRegistry = await VestingRegistry.new( - vestingFactory.address, - SOV.address, - [cSOV1.address, cSOV2.address], - pricsSats, - staking.address, - feeSharingProxy.address, - account1 - ); - - vestingRegistry2 = await VestingRegistry2.new( - vestingFactory.address, - SOV.address, - [cSOV1.address, cSOV2.address], - pricsSats, - staking.address, - feeSharingProxy.address, - account1 - ); - - vestingRegistry3 = await VestingRegistry3.new( - vestingFactory.address, - SOV.address, - staking.address, - feeSharingProxy.address, - account1 - ); - - await vesting.initialize( - vestingFactory.address, - SOV.address, - staking.address, - feeSharingProxy.address, - account4, - lockedSOV.address, - [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] - ); - - await vesting.addAdmin(vestingCreator.address); - - teamVesting = await TeamVesting.new( - vestingLogic.address, - cSOV1.address, - staking.address, - account2, - 16 * WEEK, - 46 * WEEK, - feeSharingProxy.address - ); - teamVesting = await VestingLogic.at(teamVesting.address); - } - - before(async () => { - [root, account1, account2, account3, account4, ...accounts] = accounts; - }); - - beforeEach(async () => { - await loadFixture(deploymentAndInitFixture); - }); - - describe("constructor", () => { - it("sets the expected values", async () => { - let _sov = await vestingCreator.SOV(); - let _vestingRegistryLogic = await vestingCreator.vestingRegistryLogic(); - - expect(_sov).equal(SOV.address); - expect(_vestingRegistryLogic).equal(vesting.address); - }); - - it("fails if the 0 address is passed as SOV Address", async () => { - await expectRevert(VestingCreator.new(ZERO_ADDRESS, vesting.address), "SOV address invalid"); - }); - - it("fails if the 0 address is passed as Vesting Registry Address", async () => { - await expectRevert(VestingCreator.new(SOV.address, ZERO_ADDRESS), "Vesting registry address invalid"); - }); - }); - - describe("transferSOV", () => { - it("should be able to transfer SOV", async () => { - let amount = new BN(1000); - await SOV.transfer(vestingCreator.address, amount); - - let balanceBefore = await SOV.balanceOf(account1); - let tx = await vestingCreator.transferSOV(account1, amount); - expectEvent(tx, "SOVTransferred", { - receiver: account1, - amount: amount, - }); - let balanceAfter = await SOV.balanceOf(account1); - - expect(amount).to.be.bignumber.equal(balanceAfter.sub(balanceBefore)); - }); - - it("only owner should be able to transfer", async () => { - await expectRevert(vestingCreator.transferSOV(account1, 1000, { from: account1 }), "unauthorized"); - }); - - it("fails if the 0 address is passed as receiver address", async () => { - await expectRevert(vestingCreator.transferSOV(ZERO_ADDRESS, 1000), "transfer to the zero address"); - }); - - it("fails if the 0 is passed as an amount", async () => { - await expectRevert(vestingCreator.transferSOV(account1, 0), "amount invalid"); - }); - }); - - describe("addAdmin", () => { - it("adds admin", async () => { - let tx = await vesting.addAdmin(account1); - - expectEvent(tx, "AdminAdded", { - admin: account1, - }); - - let isAdmin = await vesting.admins(account1); - expect(isAdmin).equal(true); - }); - - it("fails sender isn't an owner", async () => { - await expectRevert(vesting.addAdmin(account1, { from: account1 }), "unauthorized"); - }); - }); - - describe("removeAdmin", () => { - it("removes admin", async () => { - await vesting.addAdmin(account1); - let tx = await vesting.removeAdmin(account1); - - expectEvent(tx, "AdminRemoved", { - admin: account1, - }); - - let isAdmin = await vesting.admins(account1); - expect(isAdmin).equal(false); - }); - - it("fails sender isn't an owner", async () => { - await expectRevert(vesting.removeAdmin(account1, { from: account1 }), "unauthorized"); - }); - }); - - describe("add vestings", () => { - it("fails if the input arrays has mismatch", async () => { - await expectRevert( - vestingCreator.addVestings( - [account1, account2, account3, account1, account1], - [new BN(3787.24), new BN(627.22), new BN(156.8), new BN(627.22), new BN(1000)], - [1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 26 * FOUR_WEEKS], - [26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS], - [false, false, true, true, false], - [1, 1, 1, 1, 1] - ), - "arrays mismatch" - ); - }); - - it("fails if durations is greater than cliff for any vesting contract", async () => { - await expectRevert( - vestingCreator.addVestings( - [account1, account2, account3, account1, account1], - [new BN(3787.24), new BN(627.22), new BN(156.8), new BN(627.22), new BN(1000)], - [1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 106 * FOUR_WEEKS], - [26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 104 * FOUR_WEEKS], - [false, false, true, true, false], - [1, 1, 1, 1, 1] - ), - "duration must be bigger than or equal to the cliff" - ); - }); - - it("fails if amount is 0", async () => { - await expectRevert( - vestingCreator.addVestings( - [account1, account2, account3, account1, account1], - [new BN(0), new BN(627.22), new BN(156.8), new BN(627.22), new BN(1000)], - [1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 106 * FOUR_WEEKS], - [26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 104 * FOUR_WEEKS], - [false, false, true, true, false], - [1, 1, 1, 1, 1] - ), - "vesting amount cannot be 0" - ); - }); - - it("fails if token owner address is 0", async () => { - await expectRevert( - vestingCreator.addVestings( - [ZERO_ADDRESS, account2, account3, account1, account1], - [new BN(3787.24), new BN(627.22), new BN(156.8), new BN(627.22), new BN(1000)], - [1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 106 * FOUR_WEEKS], - [26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 104 * FOUR_WEEKS], - [false, false, true, true, false], - [1, 1, 1, 1, 1] - ), - "token owner cannot be 0 address" - ); - }); - - it("fails if cliff does not have interval of two weeks", async () => { - await expectRevert( - vestingCreator.addVestings( - [account1, account2, account3, account1, account1], - [new BN(3787.24), new BN(627.22), new BN(156.8), new BN(627.22), new BN(1000)], - [1 * WEEK, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS], - [26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS], - [false, false, true, true, false], - [1, 1, 1, 1, 1] - ), - "cliffs should have intervals of two weeks" - ); - }); - - it("fails if duration does not have interval of two weeks", async () => { - await expectRevert( - vestingCreator.addVestings( - [account1, account2, account3, account1, account1], - [new BN(3787.24), new BN(627.22), new BN(156.8), new BN(627.22), new BN(1000)], - [2 * WEEK, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS], - [3 * WEEK, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS], - [false, false, true, true, false], - [1, 1, 1, 1, 1] - ), - "durations should have intervals of two weeks" - ); - }); - - it("adds the vesting", async () => { - let amount = new BN(1000000); - await SOV.transfer(vestingCreator.address, amount); - - await vestingCreator.addVestings( - [account1, account2, account3, account1, account1], - [new BN(3787), new BN(627), new BN(156), new BN(627), new BN(1000)], - [1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 26 * FOUR_WEEKS], - [26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 104 * FOUR_WEEKS], - [false, false, true, true, false], - [1, 1, 1, 1, 1] - ); - - let count = await vestingCreator.getUnprocessedCount(); - expect(count).to.be.bignumber.equal("5"); - - let totalUnprocessedAmount = await vestingCreator.getUnprocessedAmount(); - expect(totalUnprocessedAmount).to.be.bignumber.equal("6197"); - - let isEnoughBalance = await vestingCreator.isEnoughBalance(); - expect(isEnoughBalance).equal(true); - }); - - it("stress test - adds the vesting", async () => { - let amount = new BN(1000000); - await SOV.transfer(vestingCreator.address, amount); - - let tx = await vestingCreator.addVestings( - [ - account1, - account2, - account3, - account1, - account1, - account1, - account2, - account3, - account1, - account1, - account1, - account2, - account3, - account1, - account1, - account1, - account2, - account3, - account1, - account1, - account1, - account2, - account3, - account1, - account1, - account1, - account2, - account3, - account1, - account1, - account1, - account2, - account3, - account1, - account1, - account1, - account2, - account3, - account1, - account1, - account1, - account2, - account3, - account1, - account1, - account1, - account2, - account3, - account1, - account1, - ], - [ - new BN(3787), - new BN(627), - new BN(156), - new BN(627), - new BN(1000), - new BN(3787), - new BN(627), - new BN(156), - new BN(627), - new BN(1000), - new BN(3787), - new BN(627), - new BN(156), - new BN(627), - new BN(1000), - new BN(3787), - new BN(627), - new BN(156), - new BN(627), - new BN(1000), - new BN(3787), - new BN(627), - new BN(156), - new BN(627), - new BN(1000), - new BN(3787), - new BN(627), - new BN(156), - new BN(627), - new BN(1000), - new BN(3787), - new BN(627), - new BN(156), - new BN(627), - new BN(1000), - new BN(3787), - new BN(627), - new BN(156), - new BN(627), - new BN(1000), - new BN(3787), - new BN(627), - new BN(156), - new BN(627), - new BN(1000), - new BN(3787), - new BN(627), - new BN(156), - new BN(627), - new BN(1000), - ], - [ - 1 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 1 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - ], - [ - 26 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 104 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 104 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 104 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 104 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 104 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 104 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 104 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 104 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 104 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 26 * FOUR_WEEKS, - 104 * FOUR_WEEKS, - ], - [ - false, - false, - true, - true, - false, - false, - false, - true, - true, - false, - false, - false, - true, - true, - false, - false, - false, - true, - true, - false, - false, - false, - true, - true, - false, - false, - false, - true, - true, - false, - false, - false, - true, - true, - false, - false, - false, - true, - true, - false, - false, - false, - true, - true, - false, - false, - false, - true, - true, - false, - ], - [ - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - ] - ); - - console.log("gasUsed = " + tx.receipt.gasUsed); - web3.eth.getBlock("latest", false, (error, result) => { - console.log(result.gasLimit); - }); - - let count = await vestingCreator.getUnprocessedCount(); - expect(count).to.be.bignumber.equal("50"); - - let totalUnprocessedAmount = await vestingCreator.getUnprocessedAmount(); - expect(totalUnprocessedAmount).to.be.bignumber.equal("61970"); - - let isEnoughBalance = await vestingCreator.isEnoughBalance(); - expect(isEnoughBalance).equal(true); - }); - }); - - describe("process vesting creation and staking", () => { - it("process vesting creation and staking in a single txn", async () => { - let amount = new BN(1000000); - await SOV.transfer(vestingCreator.address, amount); - - await vestingCreator.addVestings( - [account1, account2, account3, account1, account1], - [new BN(3787.24), new BN(627.22), new BN(156.8), new BN(627.22), new BN(1000)], - [1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS], - [26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 9 * FOUR_WEEKS], - [false, false, true, true, true], - [1, 1, 1, 1, 1] - ); - - let period = await vestingCreator.getVestingPeriod(); - if (period <= MAX_PERIOD) { - let tx = await vestingCreator.processNextVesting(); - console.log("gasUsed = " + tx.receipt.gasUsed); - let count = await vestingCreator.getUnprocessedCount(); - expect(count).to.be.bignumber.equal("4"); - } - }); - - it("process vesting creation and staking separately", async () => { - let amount = new BN(1000000); - await SOV.transfer(vestingCreator.address, amount); - - await vestingCreator.addVestings( - [account1, account2, account3, account1, account1], - [new BN(3787.24), new BN(627.22), new BN(156.8), new BN(627.22), new BN(1000)], - [1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS], - [26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 10 * FOUR_WEEKS], - [false, false, true, true, false], - [1, 1, 1, 1, 1] - ); - - let period = await vestingCreator.getVestingPeriod(); - if (period > MAX_PERIOD) { - let tx = await vestingCreator.processVestingCreation(); - console.log("gasUsed = " + tx.receipt.gasUsed); - let vestingAddr = await vestingCreator.getVestingAddress(); - expect(await vesting.isVestingAdress(vestingAddr)).equal(true); - tx = await vestingCreator.processStaking(); - console.log("gasUsed = " + tx.receipt.gasUsed); - - expectEvent(tx, "TokensStaked", { - vesting: vestingAddr, - tokenOwner: account1, - amount: new BN(1000), - }); - - expectEvent(tx, "VestingDataRemoved", { - caller: root, - tokenOwner: account1, - }); - - let count = await vestingCreator.getUnprocessedCount(); - expect(count).to.be.bignumber.equal("4"); - } - }); - - it("vesting creation fails if staking is not done for previous creation", async () => { - await vestingCreator.addVestings( - [account1, account2, account3, account1, account1], - [new BN(3787.24), new BN(627.22), new BN(156.8), new BN(627.22), new BN(1000)], - [1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS], - [26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 10 * FOUR_WEEKS], - [false, false, true, true, false], - [1, 1, 1, 1, 1] - ); - await vestingCreator.processVestingCreation(); - await expectRevert(vestingCreator.processVestingCreation(), "staking not done for the previous vesting"); - }); - - it("staking fails if vesting is not created", async () => { - await expectRevert(vestingCreator.processStaking(), "cannot stake without vesting creation"); - }); - }); - - describe("get missing balance", () => { - it("returns missing balance", async () => { - let amount = new BN(1000); - await SOV.transfer(vestingCreator.address, amount); - - await vestingCreator.addVestings( - [account1, account2, account3, account1, account1], - [new BN(3787), new BN(627), new BN(156), new BN(627), new BN(1000)], - [1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 26 * FOUR_WEEKS], - [26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 104 * FOUR_WEEKS], - [false, false, true, true, false], - [1, 1, 1, 1, 1] - ); - - let balance = await vestingCreator.getMissingBalance(); - expect(balance).to.be.bignumber.equal("5197"); - }); - - it("returns 0 if no missing balance", async () => { - let amount = new BN(10000); - await SOV.transfer(vestingCreator.address, amount); - - await vestingCreator.addVestings( - [account1, account2, account3, account1, account1], - [new BN(3787), new BN(627), new BN(156), new BN(627), new BN(1000)], - [1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 26 * FOUR_WEEKS], - [26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 104 * FOUR_WEEKS], - [false, false, true, true, false], - [1, 1, 1, 1, 1] - ); - - let balance = await vestingCreator.getMissingBalance(); - expect(balance).to.be.bignumber.equal("0"); - }); - }); - - describe("remove vesting", () => { - it("removes the vesting", async () => { - let amount = new BN(1000); - await SOV.transfer(vestingCreator.address, amount); - - await vestingCreator.addVestings( - [account1, account2, account3, account1, account1], - [new BN(3787), new BN(627), new BN(156), new BN(627), new BN(1000)], - [1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS], - [26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS], - [false, false, true, true, false], - [1, 1, 1, 1, 1] - ); - - let tx = await vestingCreator.removeNextVesting(); - expectEvent(tx, "VestingDataRemoved", { - caller: root, - tokenOwner: account1, - }); - let count = await vestingCreator.getUnprocessedCount(); - expect(count).to.be.bignumber.equal("4"); - }); - }); - - describe("clear vesting data list", () => { - it("clears the vesting list", async () => { - let amount = new BN(1000); - await SOV.transfer(vestingCreator.address, amount); - - await vestingCreator.addVestings( - [account1, account2, account3, account1, account1], - [new BN(3787), new BN(627), new BN(156), new BN(627), new BN(1000)], - [1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS], - [26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS], - [false, false, true, true, false], - [1, 1, 1, 1, 1] - ); - - let tx = await vestingCreator.clearVestingDataList(); - let count = await vestingCreator.getUnprocessedCount(); - expect(count).to.be.bignumber.equal("0"); - - expectEvent(tx, "DataCleared", { - caller: root, - }); - }); - }); - - describe("vestingRegistry3", () => { - it("check transferSOV", async () => { - let amount = new BN(1000); - - // Send funds to vestingRegistry3 - await SOV.transfer(vestingRegistry3.address, amount); - - // Get recipient's balance before transfer - let balance2before = await SOV.balanceOf(account1); - // console.log("balance2before: ", balance2before.toString()); - - // Call transferSOV - await vestingRegistry3.transferSOV(account1, amount); - - // Get recipient's balance after transfer - let balance2after = await SOV.balanceOf(account1); - // console.log("balance2after: ", balance2after.toString()); - - // Check account1 received the transfer - expect(balance2after.sub(balance2before)).to.be.bignumber.equal(amount); - - // Fail to transfer to address(0) - await expectRevert(vestingRegistry3.transferSOV(ZERO_ADDRESS, amount), "receiver address invalid"); - - // Fail to transfer 0 amount - await expectRevert(vestingRegistry3.transferSOV(account1, 0), "amount invalid"); - }); - - /// @dev vestingRegistry3 has its own methods for adding and removing admins - it("add and remove admin", async () => { - // Add account1 as admin - let tx = await vestingRegistry3.addAdmin(account1); - expectEvent(tx, "AdminAdded", { - admin: account1, - }); - - // Check account1 is admin - let isAdmin = await vestingRegistry3.admins(account1); - expect(isAdmin).equal(true); - - // Remove account1 as admin - tx = await vestingRegistry3.removeAdmin(account1); - expectEvent(tx, "AdminRemoved", { - admin: account1, - }); - - // Check account1 is not admin anymore - isAdmin = await vestingRegistry3.admins(account1); - expect(isAdmin).equal(false); - }); - - /// @dev vestingRegistry3 has its own method for setting vesting factory - it("set vesting factory", async () => { - await vestingRegistry3.setVestingFactory(account2); - - let vestingFactory = await vestingRegistry3.vestingFactory(); - expect(vestingFactory).equal(account2); - }); - - /// @dev Checks all require statements for test coverage - it("constructor", async () => { - await expectRevert( - VestingRegistry3.new(vestingFactory.address, ZERO_ADDRESS, staking.address, feeSharingProxy.address, account1), - "SOV address invalid" - ); - - await expectRevert( - VestingRegistry3.new(vestingFactory.address, SOV.address, ZERO_ADDRESS, feeSharingProxy.address, account1), - "staking address invalid" - ); - - await expectRevert( - VestingRegistry3.new(vestingFactory.address, SOV.address, staking.address, ZERO_ADDRESS, account1), - "feeSharingProxy address invalid" - ); - - await expectRevert( - VestingRegistry3.new(vestingFactory.address, SOV.address, staking.address, feeSharingProxy.address, ZERO_ADDRESS), - "vestingOwner address invalid" - ); - }); - - /// @dev Check require statements for stakeTokens method - it("should fail stakeTokens when parameters are address(0), 0", async () => { - let amount = new BN(1000); - await SOV.transfer(vestingRegistry3.address, amount); - - await expectRevert(vestingRegistry3.stakeTokens(ZERO_ADDRESS, amount), "vesting address invalid"); - }); - }); - - /// @dev Test coverage - /// Vesting.js also checks delegate, but on a mockup contract - describe("VestingLogic", () => { - it("should revert when setting delegate by no token owner", async () => { - // Try to set delegate from other account than token owner - await expectRevert(teamVesting.delegate(account1, { from: account3 }), "unauthorized"); - }); - - it("should revert when setting address 0 as delegate", async () => { - // Try to set delegate as address 0 - await expectRevert(teamVesting.delegate(ZERO_ADDRESS, { from: account2 }), "delegatee address invalid"); - }); - - it("Delegate should work ok", async () => { - // Delegate - let tx = await teamVesting.delegate(account1, { from: account2 }); - - expectEvent(tx, "VotesDelegated", { - caller: account2, - delegatee: account1, - }); - }); - - it("should revert when withdrawing tokens to address 0", async () => { - await expectRevert(teamVesting.withdrawTokens(ZERO_ADDRESS, { from: root }), "receiver address invalid"); - }); - }); + let root, account1, account2, account3, account4; + let SOV, lockedSOV; + let vesting, vestingLogic, vestingRegistryLogic; + let vestingCreator; + let vestingRegistry, vestingRegistry2, vestingRegistry3; + + let cliff = 1; // This is in 4 weeks. i.e. 1 * 4 weeks. + let duration = 11; // This is in 4 weeks. i.e. 11 * 4 weeks. + + async function deploymentAndInitFixture(_wallets, _provider) { + SOV = await SOV_ABI.new(TOTAL_SUPPLY); + cSOV1 = await TestToken.new("cSOV1", "cSOV1", 18, TOTAL_SUPPLY); + cSOV2 = await TestToken.new("cSOV2", "cSOV2", 18, TOTAL_SUPPLY); + + stakingLogic = await StakingLogic.new(); + staking = await StakingProxy.new(SOV.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); + + feeSharingProxy = await FeeSharingProxy.new(ZERO_ADDRESS, staking.address); + + vestingLogic = await VestingLogic.new(); + vestingFactory = await VestingFactory.new(vestingLogic.address); + + vestingRegistryLogic = await VestingRegistryLogic.new(); + vesting = await VestingRegistryProxy.new(); + await vesting.setImplementation(vestingRegistryLogic.address); + vesting = await VestingRegistryLogic.at(vesting.address); + vestingFactory.transferOwnership(vesting.address); + + vestingCreator = await VestingCreator.new(SOV.address, vesting.address); + + lockedSOV = await LockedSOV.new(SOV.address, vesting.address, cliff, duration, [root]); + + vestingRegistry = await VestingRegistry.new( + vestingFactory.address, + SOV.address, + [cSOV1.address, cSOV2.address], + pricsSats, + staking.address, + feeSharingProxy.address, + account1 + ); + + vestingRegistry2 = await VestingRegistry2.new( + vestingFactory.address, + SOV.address, + [cSOV1.address, cSOV2.address], + pricsSats, + staking.address, + feeSharingProxy.address, + account1 + ); + + vestingRegistry3 = await VestingRegistry3.new( + vestingFactory.address, + SOV.address, + staking.address, + feeSharingProxy.address, + account1 + ); + + await vesting.initialize( + vestingFactory.address, + SOV.address, + staking.address, + feeSharingProxy.address, + account4, + lockedSOV.address, + [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] + ); + + await vesting.addAdmin(vestingCreator.address); + + teamVesting = await TeamVesting.new( + vestingLogic.address, + cSOV1.address, + staking.address, + account2, + 16 * WEEK, + 46 * WEEK, + feeSharingProxy.address + ); + teamVesting = await VestingLogic.at(teamVesting.address); + } + + before(async () => { + [root, account1, account2, account3, account4, ...accounts] = accounts; + }); + + beforeEach(async () => { + await loadFixture(deploymentAndInitFixture); + }); + + describe("constructor", () => { + it("sets the expected values", async () => { + let _sov = await vestingCreator.SOV(); + let _vestingRegistryLogic = await vestingCreator.vestingRegistryLogic(); + + expect(_sov).equal(SOV.address); + expect(_vestingRegistryLogic).equal(vesting.address); + }); + + it("fails if the 0 address is passed as SOV Address", async () => { + await expectRevert( + VestingCreator.new(ZERO_ADDRESS, vesting.address), + "SOV address invalid" + ); + }); + + it("fails if the 0 address is passed as Vesting Registry Address", async () => { + await expectRevert( + VestingCreator.new(SOV.address, ZERO_ADDRESS), + "Vesting registry address invalid" + ); + }); + }); + + describe("transferSOV", () => { + it("should be able to transfer SOV", async () => { + let amount = new BN(1000); + await SOV.transfer(vestingCreator.address, amount); + + let balanceBefore = await SOV.balanceOf(account1); + let tx = await vestingCreator.transferSOV(account1, amount); + expectEvent(tx, "SOVTransferred", { + receiver: account1, + amount: amount, + }); + let balanceAfter = await SOV.balanceOf(account1); + + expect(amount).to.be.bignumber.equal(balanceAfter.sub(balanceBefore)); + }); + + it("only owner should be able to transfer", async () => { + await expectRevert( + vestingCreator.transferSOV(account1, 1000, { from: account1 }), + "unauthorized" + ); + }); + + it("fails if the 0 address is passed as receiver address", async () => { + await expectRevert( + vestingCreator.transferSOV(ZERO_ADDRESS, 1000), + "transfer to the zero address" + ); + }); + + it("fails if the 0 is passed as an amount", async () => { + await expectRevert(vestingCreator.transferSOV(account1, 0), "amount invalid"); + }); + }); + + describe("addAdmin", () => { + it("adds admin", async () => { + let tx = await vesting.addAdmin(account1); + + expectEvent(tx, "AdminAdded", { + admin: account1, + }); + + let isAdmin = await vesting.admins(account1); + expect(isAdmin).equal(true); + }); + + it("fails sender isn't an owner", async () => { + await expectRevert(vesting.addAdmin(account1, { from: account1 }), "unauthorized"); + }); + }); + + describe("removeAdmin", () => { + it("removes admin", async () => { + await vesting.addAdmin(account1); + let tx = await vesting.removeAdmin(account1); + + expectEvent(tx, "AdminRemoved", { + admin: account1, + }); + + let isAdmin = await vesting.admins(account1); + expect(isAdmin).equal(false); + }); + + it("fails sender isn't an owner", async () => { + await expectRevert(vesting.removeAdmin(account1, { from: account1 }), "unauthorized"); + }); + }); + + describe("add vestings", () => { + it("fails if the input arrays has mismatch", async () => { + await expectRevert( + vestingCreator.addVestings( + [account1, account2, account3, account1, account1], + [new BN(3787.24), new BN(627.22), new BN(156.8), new BN(627.22), new BN(1000)], + [ + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + ], + [26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS], + [false, false, true, true, false], + [1, 1, 1, 1, 1] + ), + "arrays mismatch" + ); + }); + + it("fails if durations is greater than cliff for any vesting contract", async () => { + await expectRevert( + vestingCreator.addVestings( + [account1, account2, account3, account1, account1], + [new BN(3787.24), new BN(627.22), new BN(156.8), new BN(627.22), new BN(1000)], + [ + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 106 * FOUR_WEEKS, + ], + [ + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 104 * FOUR_WEEKS, + ], + [false, false, true, true, false], + [1, 1, 1, 1, 1] + ), + "duration must be bigger than or equal to the cliff" + ); + }); + + it("fails if amount is 0", async () => { + await expectRevert( + vestingCreator.addVestings( + [account1, account2, account3, account1, account1], + [new BN(0), new BN(627.22), new BN(156.8), new BN(627.22), new BN(1000)], + [ + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 106 * FOUR_WEEKS, + ], + [ + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 104 * FOUR_WEEKS, + ], + [false, false, true, true, false], + [1, 1, 1, 1, 1] + ), + "vesting amount cannot be 0" + ); + }); + + it("fails if token owner address is 0", async () => { + await expectRevert( + vestingCreator.addVestings( + [ZERO_ADDRESS, account2, account3, account1, account1], + [new BN(3787.24), new BN(627.22), new BN(156.8), new BN(627.22), new BN(1000)], + [ + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 106 * FOUR_WEEKS, + ], + [ + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 104 * FOUR_WEEKS, + ], + [false, false, true, true, false], + [1, 1, 1, 1, 1] + ), + "token owner cannot be 0 address" + ); + }); + + it("fails if cliff does not have interval of two weeks", async () => { + await expectRevert( + vestingCreator.addVestings( + [account1, account2, account3, account1, account1], + [new BN(3787.24), new BN(627.22), new BN(156.8), new BN(627.22), new BN(1000)], + [1 * WEEK, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS], + [ + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + ], + [false, false, true, true, false], + [1, 1, 1, 1, 1] + ), + "cliffs should have intervals of two weeks" + ); + }); + + it("fails if duration does not have interval of two weeks", async () => { + await expectRevert( + vestingCreator.addVestings( + [account1, account2, account3, account1, account1], + [new BN(3787.24), new BN(627.22), new BN(156.8), new BN(627.22), new BN(1000)], + [2 * WEEK, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS], + [3 * WEEK, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS, 26 * FOUR_WEEKS], + [false, false, true, true, false], + [1, 1, 1, 1, 1] + ), + "durations should have intervals of two weeks" + ); + }); + + it("adds the vesting", async () => { + let amount = new BN(1000000); + await SOV.transfer(vestingCreator.address, amount); + + await vestingCreator.addVestings( + [account1, account2, account3, account1, account1], + [new BN(3787), new BN(627), new BN(156), new BN(627), new BN(1000)], + [1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 26 * FOUR_WEEKS], + [ + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 104 * FOUR_WEEKS, + ], + [false, false, true, true, false], + [1, 1, 1, 1, 1] + ); + + let count = await vestingCreator.getUnprocessedCount(); + expect(count).to.be.bignumber.equal("5"); + + let totalUnprocessedAmount = await vestingCreator.getUnprocessedAmount(); + expect(totalUnprocessedAmount).to.be.bignumber.equal("6197"); + + let isEnoughBalance = await vestingCreator.isEnoughBalance(); + expect(isEnoughBalance).equal(true); + }); + + it("stress test - adds the vesting", async () => { + let amount = new BN(1000000); + await SOV.transfer(vestingCreator.address, amount); + + let tx = await vestingCreator.addVestings( + [ + account1, + account2, + account3, + account1, + account1, + account1, + account2, + account3, + account1, + account1, + account1, + account2, + account3, + account1, + account1, + account1, + account2, + account3, + account1, + account1, + account1, + account2, + account3, + account1, + account1, + account1, + account2, + account3, + account1, + account1, + account1, + account2, + account3, + account1, + account1, + account1, + account2, + account3, + account1, + account1, + account1, + account2, + account3, + account1, + account1, + account1, + account2, + account3, + account1, + account1, + ], + [ + new BN(3787), + new BN(627), + new BN(156), + new BN(627), + new BN(1000), + new BN(3787), + new BN(627), + new BN(156), + new BN(627), + new BN(1000), + new BN(3787), + new BN(627), + new BN(156), + new BN(627), + new BN(1000), + new BN(3787), + new BN(627), + new BN(156), + new BN(627), + new BN(1000), + new BN(3787), + new BN(627), + new BN(156), + new BN(627), + new BN(1000), + new BN(3787), + new BN(627), + new BN(156), + new BN(627), + new BN(1000), + new BN(3787), + new BN(627), + new BN(156), + new BN(627), + new BN(1000), + new BN(3787), + new BN(627), + new BN(156), + new BN(627), + new BN(1000), + new BN(3787), + new BN(627), + new BN(156), + new BN(627), + new BN(1000), + new BN(3787), + new BN(627), + new BN(156), + new BN(627), + new BN(1000), + ], + [ + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 1 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + ], + [ + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 104 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 104 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 104 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 104 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 104 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 104 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 104 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 104 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 104 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 104 * FOUR_WEEKS, + ], + [ + false, + false, + true, + true, + false, + false, + false, + true, + true, + false, + false, + false, + true, + true, + false, + false, + false, + true, + true, + false, + false, + false, + true, + true, + false, + false, + false, + true, + true, + false, + false, + false, + true, + true, + false, + false, + false, + true, + true, + false, + false, + false, + true, + true, + false, + false, + false, + true, + true, + false, + ], + [ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + ] + ); + + console.log("gasUsed = " + tx.receipt.gasUsed); + web3.eth.getBlock("latest", false, (error, result) => { + console.log(result.gasLimit); + }); + + let count = await vestingCreator.getUnprocessedCount(); + expect(count).to.be.bignumber.equal("50"); + + let totalUnprocessedAmount = await vestingCreator.getUnprocessedAmount(); + expect(totalUnprocessedAmount).to.be.bignumber.equal("61970"); + + let isEnoughBalance = await vestingCreator.isEnoughBalance(); + expect(isEnoughBalance).equal(true); + }); + }); + + describe("process vesting creation and staking", () => { + it("process vesting creation and staking in a single txn", async () => { + let amount = new BN(1000000); + await SOV.transfer(vestingCreator.address, amount); + + await vestingCreator.addVestings( + [account1, account2, account3, account1, account1], + [new BN(3787.24), new BN(627.22), new BN(156.8), new BN(627.22), new BN(1000)], + [1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS], + [ + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 9 * FOUR_WEEKS, + ], + [false, false, true, true, true], + [1, 1, 1, 1, 1] + ); + + let period = await vestingCreator.getVestingPeriod(); + if (period <= MAX_PERIOD) { + let tx = await vestingCreator.processNextVesting(); + console.log("gasUsed = " + tx.receipt.gasUsed); + let count = await vestingCreator.getUnprocessedCount(); + expect(count).to.be.bignumber.equal("4"); + } + }); + + it("process vesting creation and staking separately", async () => { + let amount = new BN(1000000); + await SOV.transfer(vestingCreator.address, amount); + + await vestingCreator.addVestings( + [account1, account2, account3, account1, account1], + [new BN(3787.24), new BN(627.22), new BN(156.8), new BN(627.22), new BN(1000)], + [1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS], + [ + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 10 * FOUR_WEEKS, + ], + [false, false, true, true, false], + [1, 1, 1, 1, 1] + ); + + let period = await vestingCreator.getVestingPeriod(); + if (period > MAX_PERIOD) { + let tx = await vestingCreator.processVestingCreation(); + console.log("gasUsed = " + tx.receipt.gasUsed); + let vestingAddr = await vestingCreator.getVestingAddress(); + expect(await vesting.isVestingAdress(vestingAddr)).equal(true); + tx = await vestingCreator.processStaking(); + console.log("gasUsed = " + tx.receipt.gasUsed); + + expectEvent(tx, "TokensStaked", { + vesting: vestingAddr, + tokenOwner: account1, + amount: new BN(1000), + }); + + expectEvent(tx, "VestingDataRemoved", { + caller: root, + tokenOwner: account1, + }); + + let count = await vestingCreator.getUnprocessedCount(); + expect(count).to.be.bignumber.equal("4"); + } + }); + + it("vesting creation fails if staking is not done for previous creation", async () => { + await vestingCreator.addVestings( + [account1, account2, account3, account1, account1], + [new BN(3787.24), new BN(627.22), new BN(156.8), new BN(627.22), new BN(1000)], + [1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS], + [ + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 10 * FOUR_WEEKS, + ], + [false, false, true, true, false], + [1, 1, 1, 1, 1] + ); + await vestingCreator.processVestingCreation(); + await expectRevert( + vestingCreator.processVestingCreation(), + "staking not done for the previous vesting" + ); + }); + + it("staking fails if vesting is not created", async () => { + await expectRevert( + vestingCreator.processStaking(), + "cannot stake without vesting creation" + ); + }); + }); + + describe("get missing balance", () => { + it("returns missing balance", async () => { + let amount = new BN(1000); + await SOV.transfer(vestingCreator.address, amount); + + await vestingCreator.addVestings( + [account1, account2, account3, account1, account1], + [new BN(3787), new BN(627), new BN(156), new BN(627), new BN(1000)], + [1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 26 * FOUR_WEEKS], + [ + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 104 * FOUR_WEEKS, + ], + [false, false, true, true, false], + [1, 1, 1, 1, 1] + ); + + let balance = await vestingCreator.getMissingBalance(); + expect(balance).to.be.bignumber.equal("5197"); + }); + + it("returns 0 if no missing balance", async () => { + let amount = new BN(10000); + await SOV.transfer(vestingCreator.address, amount); + + await vestingCreator.addVestings( + [account1, account2, account3, account1, account1], + [new BN(3787), new BN(627), new BN(156), new BN(627), new BN(1000)], + [1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 26 * FOUR_WEEKS], + [ + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 104 * FOUR_WEEKS, + ], + [false, false, true, true, false], + [1, 1, 1, 1, 1] + ); + + let balance = await vestingCreator.getMissingBalance(); + expect(balance).to.be.bignumber.equal("0"); + }); + }); + + describe("remove vesting", () => { + it("removes the vesting", async () => { + let amount = new BN(1000); + await SOV.transfer(vestingCreator.address, amount); + + await vestingCreator.addVestings( + [account1, account2, account3, account1, account1], + [new BN(3787), new BN(627), new BN(156), new BN(627), new BN(1000)], + [1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS], + [ + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + ], + [false, false, true, true, false], + [1, 1, 1, 1, 1] + ); + + let tx = await vestingCreator.removeNextVesting(); + expectEvent(tx, "VestingDataRemoved", { + caller: root, + tokenOwner: account1, + }); + let count = await vestingCreator.getUnprocessedCount(); + expect(count).to.be.bignumber.equal("4"); + }); + }); + + describe("clear vesting data list", () => { + it("clears the vesting list", async () => { + let amount = new BN(1000); + await SOV.transfer(vestingCreator.address, amount); + + await vestingCreator.addVestings( + [account1, account2, account3, account1, account1], + [new BN(3787), new BN(627), new BN(156), new BN(627), new BN(1000)], + [1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS, 1 * FOUR_WEEKS], + [ + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + 26 * FOUR_WEEKS, + ], + [false, false, true, true, false], + [1, 1, 1, 1, 1] + ); + + let tx = await vestingCreator.clearVestingDataList(); + let count = await vestingCreator.getUnprocessedCount(); + expect(count).to.be.bignumber.equal("0"); + + expectEvent(tx, "DataCleared", { + caller: root, + }); + }); + }); + + describe("vestingRegistry3", () => { + it("check transferSOV", async () => { + let amount = new BN(1000); + + // Send funds to vestingRegistry3 + await SOV.transfer(vestingRegistry3.address, amount); + + // Get recipient's balance before transfer + let balance2before = await SOV.balanceOf(account1); + // console.log("balance2before: ", balance2before.toString()); + + // Call transferSOV + await vestingRegistry3.transferSOV(account1, amount); + + // Get recipient's balance after transfer + let balance2after = await SOV.balanceOf(account1); + // console.log("balance2after: ", balance2after.toString()); + + // Check account1 received the transfer + expect(balance2after.sub(balance2before)).to.be.bignumber.equal(amount); + + // Fail to transfer to address(0) + await expectRevert( + vestingRegistry3.transferSOV(ZERO_ADDRESS, amount), + "receiver address invalid" + ); + + // Fail to transfer 0 amount + await expectRevert(vestingRegistry3.transferSOV(account1, 0), "amount invalid"); + }); + + /// @dev vestingRegistry3 has its own methods for adding and removing admins + it("add and remove admin", async () => { + // Add account1 as admin + let tx = await vestingRegistry3.addAdmin(account1); + expectEvent(tx, "AdminAdded", { + admin: account1, + }); + + // Check account1 is admin + let isAdmin = await vestingRegistry3.admins(account1); + expect(isAdmin).equal(true); + + // Remove account1 as admin + tx = await vestingRegistry3.removeAdmin(account1); + expectEvent(tx, "AdminRemoved", { + admin: account1, + }); + + // Check account1 is not admin anymore + isAdmin = await vestingRegistry3.admins(account1); + expect(isAdmin).equal(false); + }); + + /// @dev vestingRegistry3 has its own method for setting vesting factory + it("set vesting factory", async () => { + await vestingRegistry3.setVestingFactory(account2); + + let vestingFactory = await vestingRegistry3.vestingFactory(); + expect(vestingFactory).equal(account2); + }); + + /// @dev Checks all require statements for test coverage + it("constructor", async () => { + await expectRevert( + VestingRegistry3.new( + vestingFactory.address, + ZERO_ADDRESS, + staking.address, + feeSharingProxy.address, + account1 + ), + "SOV address invalid" + ); + + await expectRevert( + VestingRegistry3.new( + vestingFactory.address, + SOV.address, + ZERO_ADDRESS, + feeSharingProxy.address, + account1 + ), + "staking address invalid" + ); + + await expectRevert( + VestingRegistry3.new( + vestingFactory.address, + SOV.address, + staking.address, + ZERO_ADDRESS, + account1 + ), + "feeSharingProxy address invalid" + ); + + await expectRevert( + VestingRegistry3.new( + vestingFactory.address, + SOV.address, + staking.address, + feeSharingProxy.address, + ZERO_ADDRESS + ), + "vestingOwner address invalid" + ); + }); + + /// @dev Check require statements for stakeTokens method + it("should fail stakeTokens when parameters are address(0), 0", async () => { + let amount = new BN(1000); + await SOV.transfer(vestingRegistry3.address, amount); + + await expectRevert( + vestingRegistry3.stakeTokens(ZERO_ADDRESS, amount), + "vesting address invalid" + ); + }); + }); + + /// @dev Test coverage + /// Vesting.js also checks delegate, but on a mockup contract + describe("VestingLogic", () => { + it("should revert when setting delegate by no token owner", async () => { + // Try to set delegate from other account than token owner + await expectRevert(teamVesting.delegate(account1, { from: account3 }), "unauthorized"); + }); + + it("should revert when setting address 0 as delegate", async () => { + // Try to set delegate as address 0 + await expectRevert( + teamVesting.delegate(ZERO_ADDRESS, { from: account2 }), + "delegatee address invalid" + ); + }); + + it("Delegate should work ok", async () => { + // Delegate + let tx = await teamVesting.delegate(account1, { from: account2 }); + + expectEvent(tx, "VotesDelegated", { + caller: account2, + delegatee: account1, + }); + }); + + it("should revert when withdrawing tokens to address 0", async () => { + await expectRevert( + teamVesting.withdrawTokens(ZERO_ADDRESS, { from: root }), + "receiver address invalid" + ); + }); + }); }); diff --git a/tests/vesting/VestingRegistry.js b/tests/vesting/VestingRegistry.js index 26807bc3e..19f22b69c 100644 --- a/tests/vesting/VestingRegistry.js +++ b/tests/vesting/VestingRegistry.js @@ -45,609 +45,700 @@ const ZERO_ADDRESS = constants.ZERO_ADDRESS; const pricsSats = "2500"; contract("VestingRegistry", (accounts) => { - let root, account1, account2, account3; - let SOV, cSOV1, cSOV2; - let staking, stakingLogic, feeSharingProxy; - let vestingFactory, vestingLogic, vestingRegistry; - - async function deploymentAndInitFixture(_wallets, _provider) { - SOV = await SOV_ABI.new(TOTAL_SUPPLY); - cSOV1 = await TestToken.new("cSOV1", "cSOV1", 18, TOTAL_SUPPLY); - cSOV2 = await TestToken.new("cSOV2", "cSOV2", 18, TOTAL_SUPPLY); - - stakingLogic = await StakingLogic.new(SOV.address); - staking = await StakingProxy.new(SOV.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingLogic.at(staking.address); - - feeSharingProxy = await FeeSharingProxy.new(ZERO_ADDRESS, staking.address); - - vestingLogic = await VestingLogic.new(); - vestingFactory = await VestingFactory.new(vestingLogic.address); - vestingRegistry = await VestingRegistry.new( - vestingFactory.address, - SOV.address, - [cSOV1.address, cSOV2.address], - pricsSats, - staking.address, - feeSharingProxy.address, - account1 - ); - vestingFactory.transferOwnership(vestingRegistry.address); - } - - before(async () => { - [root, account1, account2, account3, ...accounts] = accounts; - }); - - beforeEach(async () => { - /// @dev Only some tests really require an initial redeployment - // await loadFixture(deploymentAndInitFixture); - }); - - describe("constructor", () => { - it("sets the expected values", async () => { - await loadFixture(deploymentAndInitFixture); - - let _sov = await vestingRegistry.SOV(); - let _CSOV1 = await vestingRegistry.CSOVtokens(0); - let _CSOV2 = await vestingRegistry.CSOVtokens(1); - let _stacking = await vestingRegistry.staking(); - let _feeSharingProxy = await vestingRegistry.feeSharingProxy(); - let _vestingOwner = await vestingRegistry.vestingOwner(); - - expect(_sov).equal(SOV.address); - expect(_CSOV1).equal(cSOV1.address); - expect(_CSOV2).equal(cSOV2.address); - expect(_stacking).equal(staking.address); - expect(_feeSharingProxy).equal(feeSharingProxy.address); - expect(_vestingOwner).equal(account1); - }); - - it("fails if the 0 address is passed as vestingFactory address", async () => { - await expectRevert( - VestingRegistry.new( - ZERO_ADDRESS, - SOV.address, - [cSOV1.address, cSOV2.address], - pricsSats, - staking.address, - feeSharingProxy.address, - account1 - ), - "vestingFactory address invalid" - ); - }); - - it("fails if the 0 address is passed as SOV address", async () => { - await expectRevert( - VestingRegistry.new( - vestingFactory.address, - ZERO_ADDRESS, - [cSOV1.address, cSOV2.address], - pricsSats, - staking.address, - feeSharingProxy.address, - account1 - ), - "SOV address invalid" - ); - }); - - it("fails if the 0 address is passed as cSOV address", async () => { - await expectRevert( - VestingRegistry.new( - vestingFactory.address, - SOV.address, - [cSOV1.address, cSOV2.address, ZERO_ADDRESS], - pricsSats, - staking.address, - feeSharingProxy.address, - account1 - ), - "CSOV address invalid" - ); - }); - - it("fails if the 0 address is passed as staking address", async () => { - await expectRevert( - VestingRegistry.new( - vestingFactory.address, - SOV.address, - [cSOV1.address, cSOV2.address], - pricsSats, - ZERO_ADDRESS, - feeSharingProxy.address, - account1 - ), - "staking address invalid" - ); - }); - - it("fails if the 0 address is passed as feeSharingProxy address", async () => { - await expectRevert( - VestingRegistry.new( - vestingFactory.address, - SOV.address, - [cSOV1.address, cSOV2.address], - pricsSats, - staking.address, - ZERO_ADDRESS, - account1 - ), - "feeSharingProxy address invalid" - ); - }); - - it("fails if the 0 address is passed as vestingOwner address", async () => { - await expectRevert( - VestingRegistry.new( - vestingFactory.address, - SOV.address, - [cSOV1.address, cSOV2.address], - pricsSats, - staking.address, - feeSharingProxy.address, - ZERO_ADDRESS - ), - "vestingOwner address invalid" - ); - }); - }); - - describe("setVestingFactory", () => { - it("sets vesting factory", async () => { - await vestingRegistry.setVestingFactory(account2); - - let vestingFactory = await vestingRegistry.vestingFactory(); - expect(vestingFactory).equal(account2); - }); - - it("fails if the 0 address is passed", async () => { - await expectRevert(vestingRegistry.setVestingFactory(ZERO_ADDRESS), "vestingFactory address invalid"); - }); - - it("fails if sender isn't an owner", async () => { - await expectRevert(vestingRegistry.setVestingFactory(account2, { from: account2 }), "unauthorized"); - }); - }); - - describe("addAdmin", () => { - it("adds admin", async () => { - let tx = await vestingRegistry.addAdmin(account1); - - expectEvent(tx, "AdminAdded", { - admin: account1, - }); - - let isAdmin = await vestingRegistry.admins(account1); - expect(isAdmin).equal(true); - }); - - it("fails sender isn't an owner", async () => { - await expectRevert(vestingRegistry.addAdmin(account1, { from: account1 }), "unauthorized"); - }); - }); - - describe("removeAdmin", () => { - it("adds admin", async () => { - await vestingRegistry.addAdmin(account1); - let tx = await vestingRegistry.removeAdmin(account1); - - expectEvent(tx, "AdminRemoved", { - admin: account1, - }); - - let isAdmin = await vestingRegistry.admins(account1); - expect(isAdmin).equal(false); - }); - - it("fails sender isn't an owner", async () => { - await expectRevert(vestingRegistry.removeAdmin(account1, { from: account1 }), "unauthorized"); - }); - }); - - describe("setCSOVtokens", () => { - it("sets the expected values", async () => { - await vestingRegistry.setCSOVtokens([account2, account3]); - - let _CSOV1 = await vestingRegistry.CSOVtokens(0); - let _CSOV2 = await vestingRegistry.CSOVtokens(1); - - expect(_CSOV1).equal(account2); - expect(_CSOV2).equal(account3); - }); - - it("fails if the 0 address is passed as cSOV address", async () => { - await expectRevert(vestingRegistry.setCSOVtokens([cSOV1.address, cSOV2.address, ZERO_ADDRESS]), "CSOV address invalid"); - }); - - it("fails if sender isn't an owner", async () => { - await expectRevert(vestingRegistry.setCSOVtokens([cSOV1.address, cSOV2.address], { from: account2 }), "unauthorized"); - }); - }); - - describe("transferSOV", () => { - it("should be able to transfer SOV", async () => { - let amount = new BN(1000); - await SOV.transfer(vestingRegistry.address, amount); - - let balanceBefore = await SOV.balanceOf(account1); - await vestingRegistry.transferSOV(account1, amount); - let balanceAfter = await SOV.balanceOf(account1); - - expect(amount).to.be.bignumber.equal(balanceAfter.sub(balanceBefore)); - }); - - it("only owner should be able to transfer", async () => { - await expectRevert(vestingRegistry.transferSOV(account1, 1000, { from: account1 }), "unauthorized"); - }); - - it("fails if the 0 address is passed as receiver address", async () => { - await expectRevert(vestingRegistry.transferSOV(ZERO_ADDRESS, 1000), "receiver address invalid"); - }); - - it("fails if the 0 is passed as an amount", async () => { - await expectRevert(vestingRegistry.transferSOV(account1, 0), "amount invalid"); - }); - }); - - describe("setBlacklistFlag", () => { - it("should be able to add user to blacklist", async () => { - await vestingRegistry.setBlacklistFlag(account2, true); - - let blacklisted = await vestingRegistry.blacklist(account2); - expect(blacklisted).equal(true); - }); - - it("fails if the 0 address is passed", async () => { - await expectRevert(vestingRegistry.setBlacklistFlag(ZERO_ADDRESS, true), "account address invalid"); - }); - }); - - describe("setLockedAmount", () => { - it("should be able to set locked amount", async () => { - let amount = new BN(123); - await vestingRegistry.setLockedAmount(account2, amount); - - let lockedAmount = await vestingRegistry.lockedAmount(account2); - expect(lockedAmount).to.be.bignumber.equal(amount); - }); - - it("fails if the 0 address is passed", async () => { - await expectRevert(vestingRegistry.setLockedAmount(ZERO_ADDRESS, 111), "account address invalid"); - }); - - it("fails if the 0 amount is passed", async () => { - await expectRevert(vestingRegistry.setLockedAmount(account2, 0), "amount invalid"); - }); - }); - - describe("exchangeAllCSOV", () => { - it("should be able to exchange CSOV", async () => { - /// @dev This test requires a hard reset of init fixture - await deploymentAndInitFixture(); - - let amount1 = new BN(1000); - let amount2 = new BN(2000); - await cSOV1.transfer(account2, amount1); - await cSOV2.transfer(account2, amount2); - let amount = amount1.add(amount2); - await SOV.transfer(vestingRegistry.address, amount); - - let tx = await vestingRegistry.exchangeAllCSOV({ from: account2 }); - - expectEvent(tx, "CSOVTokensExchanged", { - caller: account2, - amount: amount, - }); - - let processedList = await vestingRegistry.processedList(account2); - expect(processedList).equal(true); - - let balance = await SOV.balanceOf(vestingRegistry.address); - expect(balance.toString()).equal("0"); - - let cliff = await vestingRegistry.CSOV_VESTING_CLIFF(); - let duration = await vestingRegistry.CSOV_VESTING_DURATION(); - - let vestingAddress = await vestingRegistry.getVesting(account2); - let vesting = await VestingLogic.at(vestingAddress); - await checkVesting(vesting, account2, cliff, duration, amount); - - await expectRevert(vesting.governanceWithdrawTokens(account2), "revert"); - }); - - it("should be able to exchange CSOV partially", async () => { - /// @dev This test requires a hard reset of init fixture - await deploymentAndInitFixture(); - - let amount1 = new BN(1000); - let amount2 = new BN(2000); - let lockedAmount = new BN(700); - await cSOV1.transfer(account2, amount1); - await cSOV2.transfer(account2, amount2); - - let amount = amount1.add(amount2).sub(lockedAmount); - await SOV.transfer(vestingRegistry.address, amount); - - await vestingRegistry.setLockedAmount(account2, lockedAmount); - let tx = await vestingRegistry.exchangeAllCSOV({ from: account2 }); - - expectEvent(tx, "CSOVTokensExchanged", { - caller: account2, - amount: amount, - }); - - let processedList = await vestingRegistry.processedList(account2); - expect(processedList).equal(true); - - let balance = await SOV.balanceOf(vestingRegistry.address); - expect(balance.toString()).equal("0"); - - let cliff = await vestingRegistry.CSOV_VESTING_CLIFF(); - let duration = await vestingRegistry.CSOV_VESTING_DURATION(); - - let vestingAddress = await vestingRegistry.getVesting(account2); - let vesting = await VestingLogic.at(vestingAddress); - await checkVesting(vesting, account2, cliff, duration, amount); - - await expectRevert(vesting.governanceWithdrawTokens(account2), "revert"); - }); - - it("fails if trying to withdraw second time", async () => { - /// @dev This test requires a hard reset of init fixture - await deploymentAndInitFixture(); - - let amount = new BN(1000); - await cSOV1.transfer(account2, amount); - await SOV.transfer(vestingRegistry.address, amount); - - await vestingRegistry.exchangeAllCSOV({ from: account2 }); - await expectRevert(vestingRegistry.exchangeAllCSOV({ from: account2 }), "Address cannot be processed twice"); - }); - - it("fails if account blacklisted", async () => { - /// @dev This test requires a hard reset of init fixture - await deploymentAndInitFixture(); - - let amount = new BN(1000); - await cSOV1.transfer(account2, amount); - await SOV.transfer(vestingRegistry.address, amount); - - await vestingRegistry.setBlacklistFlag(account2, true); - await expectRevert(vestingRegistry.exchangeAllCSOV({ from: account2 }), "Address blacklisted"); - }); - - it("fails if the 0 is cSOV amount", async () => { - /// @dev This test requires a hard reset of init fixture - await deploymentAndInitFixture(); - - await expectRevert(vestingRegistry.exchangeAllCSOV({ from: account2 }), "amount invalid"); - }); - - it("fails if vestingRegistry doesn't have enough SOV", async () => { - /// @dev This test requires a hard reset of init fixture - await deploymentAndInitFixture(); - - let amount = new BN(1000); - await cSOV1.transfer(account2, amount); - - await expectRevert(vestingRegistry.exchangeAllCSOV({ from: account2 }), "ERC20: transfer amount exceeds balance"); - }); - }); - - describe("createVesting", () => { - it("should be able to create vesting", async () => { - let amount = new BN(1000000); - await SOV.transfer(vestingRegistry.address, amount); - - let cliff = FOUR_WEEKS; - let duration = FOUR_WEEKS.mul(new BN(20)); - let tx = await vestingRegistry.createVesting(account2, amount, cliff, duration); - let vestingAddress = await vestingRegistry.getVesting(account2); - await vestingRegistry.stakeTokens(vestingAddress, amount); - - expectEvent(tx, "VestingCreated", { - tokenOwner: account2, - vesting: vestingAddress, - cliff: cliff, - duration: duration, - amount: amount, - }); - - let balance = await SOV.balanceOf(vestingRegistry.address); - expect(balance.toString()).equal("0"); - - let vesting = await VestingLogic.at(vestingAddress); - await checkVesting(vesting, account2, cliff, duration, amount); - - await expectRevert(vesting.governanceWithdrawTokens(account2), "operation not supported"); - - let proxy = await UpgradableProxy.at(vestingAddress); - await expectRevert(proxy.setImplementation(account2), "revert"); - }); - - it("fails if vestingRegistry doesn't have enough SOV", async () => { - let amount = new BN(1000000); - let cliff = FOUR_WEEKS; - let duration = FOUR_WEEKS.mul(new BN(20)); - - await vestingRegistry.createVesting(account2, amount, cliff, duration); - let vestingAddress = await vestingRegistry.getVesting(account2); - - await expectRevert(vestingRegistry.stakeTokens(vestingAddress, amount), "ERC20: transfer amount exceeds balance"); - }); - - it("fails if sender is not an owner or admin", async () => { - let amount = new BN(1000000); - let cliff = TEAM_VESTING_CLIFF; - let duration = TEAM_VESTING_DURATION; - - await expectRevert(vestingRegistry.createVesting(account2, amount, cliff, duration, { from: account1 }), "unauthorized"); - - await vestingRegistry.addAdmin(account1); - await vestingRegistry.createVesting(account2, amount, cliff, duration, { from: account1 }); - }); - }); - - describe("createTeamVesting", () => { - it("should be able to create vesting", async () => { - /// @dev This test requires a hard reset of init fixture - await deploymentAndInitFixture(); - - let amount = new BN(1000000); - await SOV.transfer(vestingRegistry.address, amount); - - let cliff = TEAM_VESTING_CLIFF; - let duration = TEAM_VESTING_DURATION; - let tx = await vestingRegistry.createTeamVesting(account2, amount, cliff, duration); - let vestingAddress = await vestingRegistry.getTeamVesting(account2); - let tx2 = await vestingRegistry.stakeTokens(vestingAddress, amount); - - console.log("\ngasUsed = " + tx.receipt.gasUsed); - console.log("gasUsed = " + tx2.receipt.gasUsed); - - expectEvent(tx, "TeamVestingCreated", { - tokenOwner: account2, - vesting: vestingAddress, - cliff: cliff, - duration: duration, - amount: amount, - }); - - let balance = await SOV.balanceOf(vestingRegistry.address); - expect(balance.toString()).equal("0"); - - let vesting = await VestingLogic.at(vestingAddress); - await checkVesting(vesting, account2, cliff, duration, amount); - - await expectRevert(vesting.governanceWithdrawTokens(account2), "unauthorized"); - - let proxy = await UpgradableProxy.at(vestingAddress); - await expectRevert(proxy.setImplementation(account2), "revert"); - }); - - it("fails if vestingRegistry doesn't have enough SOV", async () => { - let amount = new BN(1000000); - let cliff = TEAM_VESTING_CLIFF; - let duration = TEAM_VESTING_DURATION; - - await vestingRegistry.createTeamVesting(account2, amount, cliff, duration); - let vestingAddress = await vestingRegistry.getTeamVesting(account2); - - await expectRevert(vestingRegistry.stakeTokens(vestingAddress, amount), "ERC20: transfer amount exceeds balance"); - }); - - it("fails if sender is not an owner or admin", async () => { - let amount = new BN(1000000); - let cliff = TEAM_VESTING_CLIFF; - let duration = TEAM_VESTING_DURATION; - - await expectRevert(vestingRegistry.createTeamVesting(account2, amount, cliff, duration, { from: account1 }), "unauthorized"); - - await vestingRegistry.addAdmin(account1); - await vestingRegistry.createTeamVesting(account2, amount, cliff, duration, { from: account1 }); - }); - }); - - describe("stakeTokens", () => { - it("fails if the 0 address is passed as vesting address", async () => { - await expectRevert(vestingRegistry.stakeTokens(ZERO_ADDRESS, new BN(1000000)), "vesting address invalid"); - }); - - it("fails if the 0 address is passed as an amount", async () => { - await expectRevert(vestingRegistry.stakeTokens(account1, 0), "amount invalid"); - }); - - it("only owner or admin should be able to stake tokens", async () => { - /// @dev This test requires a hard reset of init fixture - await deploymentAndInitFixture(); - - let amount = new BN(1000000); - await SOV.transfer(vestingRegistry.address, amount); - - let cliff = TEAM_VESTING_CLIFF; - let duration = TEAM_VESTING_DURATION; - await vestingRegistry.createTeamVesting(account2, amount, cliff, duration); - let vestingAddress = await vestingRegistry.getTeamVesting(account2); - - await expectRevert(vestingRegistry.stakeTokens(vestingAddress, new BN(1000000), { from: account1 }), "unauthorized"); - - await vestingRegistry.addAdmin(account1); - await vestingRegistry.stakeTokens(vestingAddress, new BN(1000000), { from: account1 }); - }); - }); - - describe("OrigingVestingCreator", () => { - it("should be able to create vesting", async () => { - await deploymentAndInitFixture(); - - let origingVestingCreator = await OrigingVestingCreator.new(vestingRegistry.address); - - let amount = new BN(1000000); - await SOV.transfer(vestingRegistry.address, amount); - - let cliff = FOUR_WEEKS; - let duration = FOUR_WEEKS.mul(new BN(20)); - - // Adding origingVestingCreator as an admin in the Vesting Registry. - await vestingRegistry.addAdmin(origingVestingCreator.address); - - // Try to use address(0) - await expectRevert(origingVestingCreator.createVesting(ZERO_ADDRESS, amount, cliff, duration), "Invalid address"); - - // Create the vesting contract - await origingVestingCreator.createVesting(account3, amount, cliff, duration); - - // Try to create it again w/ the same tokenOwner - await expectRevert(origingVestingCreator.createVesting(account3, amount, cliff, duration), "Already processed"); - }); - }); - - async function checkVesting(vesting, account, cliff, duration, amount) { - await mineBlock(); - - let vestingBalance = await staking.balanceOf(vesting.address); - expect(vestingBalance).to.be.bignumber.equal(amount); - - let accountVotes = await staking.getCurrentVotes(account); - expect(accountVotes).to.be.not.equal(new BN(0)); - let vestingVotes = await staking.getCurrentVotes(vesting.address); - expect(vestingVotes).to.be.bignumber.equal(new BN(0)); - - let startDate = await vesting.startDate(); - let start = startDate.toNumber() + cliff.toNumber(); - let end = startDate.toNumber() + duration.toNumber(); - - let numIntervals = Math.floor((end - start) / FOUR_WEEKS) + 1; - let stakedPerInterval = Math.floor(amount / numIntervals); - - let stakeForFirstInterval = amount - stakedPerInterval * (numIntervals - 1); - - expect(await vesting.cliff()).to.be.bignumber.equal(cliff); - expect(await vesting.duration()).to.be.bignumber.equal(duration); - - for (let i = start; i <= end; i += FOUR_WEEKS) { - let lockedTS = await staking.timestampToLockDate(i); - - let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints(vesting.address, lockedTS); - let userStakingCheckpoints = await staking.userStakingCheckpoints(vesting.address, lockedTS, numUserStakingCheckpoints - 1); - assert.equal(numUserStakingCheckpoints.toString(), "1"); - if (i === start) { - assert.equal(userStakingCheckpoints.stake.toString(), stakeForFirstInterval); - } else { - assert.equal(userStakingCheckpoints.stake.toString(), stakedPerInterval); - } - - let numDelegateStakingCheckpoints = await staking.numDelegateStakingCheckpoints(account, lockedTS); - let delegateStakingCheckpoints = await staking.delegateStakingCheckpoints(account, lockedTS, numUserStakingCheckpoints - 1); - assert.equal(numDelegateStakingCheckpoints.toString(), "1"); - if (i === start) { - assert.equal(delegateStakingCheckpoints.stake.toString(), stakeForFirstInterval); - } else { - assert.equal(delegateStakingCheckpoints.stake.toString(), stakedPerInterval); - } - } - } + let root, account1, account2, account3; + let SOV, cSOV1, cSOV2; + let staking, stakingLogic, feeSharingProxy; + let vestingFactory, vestingLogic, vestingRegistry; + + async function deploymentAndInitFixture(_wallets, _provider) { + SOV = await SOV_ABI.new(TOTAL_SUPPLY); + cSOV1 = await TestToken.new("cSOV1", "cSOV1", 18, TOTAL_SUPPLY); + cSOV2 = await TestToken.new("cSOV2", "cSOV2", 18, TOTAL_SUPPLY); + + stakingLogic = await StakingLogic.new(SOV.address); + staking = await StakingProxy.new(SOV.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); + + feeSharingProxy = await FeeSharingProxy.new(ZERO_ADDRESS, staking.address); + + vestingLogic = await VestingLogic.new(); + vestingFactory = await VestingFactory.new(vestingLogic.address); + vestingRegistry = await VestingRegistry.new( + vestingFactory.address, + SOV.address, + [cSOV1.address, cSOV2.address], + pricsSats, + staking.address, + feeSharingProxy.address, + account1 + ); + vestingFactory.transferOwnership(vestingRegistry.address); + } + + before(async () => { + [root, account1, account2, account3, ...accounts] = accounts; + }); + + beforeEach(async () => { + /// @dev Only some tests really require an initial redeployment + // await loadFixture(deploymentAndInitFixture); + }); + + describe("constructor", () => { + it("sets the expected values", async () => { + await loadFixture(deploymentAndInitFixture); + + let _sov = await vestingRegistry.SOV(); + let _CSOV1 = await vestingRegistry.CSOVtokens(0); + let _CSOV2 = await vestingRegistry.CSOVtokens(1); + let _stacking = await vestingRegistry.staking(); + let _feeSharingProxy = await vestingRegistry.feeSharingProxy(); + let _vestingOwner = await vestingRegistry.vestingOwner(); + + expect(_sov).equal(SOV.address); + expect(_CSOV1).equal(cSOV1.address); + expect(_CSOV2).equal(cSOV2.address); + expect(_stacking).equal(staking.address); + expect(_feeSharingProxy).equal(feeSharingProxy.address); + expect(_vestingOwner).equal(account1); + }); + + it("fails if the 0 address is passed as vestingFactory address", async () => { + await expectRevert( + VestingRegistry.new( + ZERO_ADDRESS, + SOV.address, + [cSOV1.address, cSOV2.address], + pricsSats, + staking.address, + feeSharingProxy.address, + account1 + ), + "vestingFactory address invalid" + ); + }); + + it("fails if the 0 address is passed as SOV address", async () => { + await expectRevert( + VestingRegistry.new( + vestingFactory.address, + ZERO_ADDRESS, + [cSOV1.address, cSOV2.address], + pricsSats, + staking.address, + feeSharingProxy.address, + account1 + ), + "SOV address invalid" + ); + }); + + it("fails if the 0 address is passed as cSOV address", async () => { + await expectRevert( + VestingRegistry.new( + vestingFactory.address, + SOV.address, + [cSOV1.address, cSOV2.address, ZERO_ADDRESS], + pricsSats, + staking.address, + feeSharingProxy.address, + account1 + ), + "CSOV address invalid" + ); + }); + + it("fails if the 0 address is passed as staking address", async () => { + await expectRevert( + VestingRegistry.new( + vestingFactory.address, + SOV.address, + [cSOV1.address, cSOV2.address], + pricsSats, + ZERO_ADDRESS, + feeSharingProxy.address, + account1 + ), + "staking address invalid" + ); + }); + + it("fails if the 0 address is passed as feeSharingProxy address", async () => { + await expectRevert( + VestingRegistry.new( + vestingFactory.address, + SOV.address, + [cSOV1.address, cSOV2.address], + pricsSats, + staking.address, + ZERO_ADDRESS, + account1 + ), + "feeSharingProxy address invalid" + ); + }); + + it("fails if the 0 address is passed as vestingOwner address", async () => { + await expectRevert( + VestingRegistry.new( + vestingFactory.address, + SOV.address, + [cSOV1.address, cSOV2.address], + pricsSats, + staking.address, + feeSharingProxy.address, + ZERO_ADDRESS + ), + "vestingOwner address invalid" + ); + }); + }); + + describe("setVestingFactory", () => { + it("sets vesting factory", async () => { + await vestingRegistry.setVestingFactory(account2); + + let vestingFactory = await vestingRegistry.vestingFactory(); + expect(vestingFactory).equal(account2); + }); + + it("fails if the 0 address is passed", async () => { + await expectRevert( + vestingRegistry.setVestingFactory(ZERO_ADDRESS), + "vestingFactory address invalid" + ); + }); + + it("fails if sender isn't an owner", async () => { + await expectRevert( + vestingRegistry.setVestingFactory(account2, { from: account2 }), + "unauthorized" + ); + }); + }); + + describe("addAdmin", () => { + it("adds admin", async () => { + let tx = await vestingRegistry.addAdmin(account1); + + expectEvent(tx, "AdminAdded", { + admin: account1, + }); + + let isAdmin = await vestingRegistry.admins(account1); + expect(isAdmin).equal(true); + }); + + it("fails sender isn't an owner", async () => { + await expectRevert( + vestingRegistry.addAdmin(account1, { from: account1 }), + "unauthorized" + ); + }); + }); + + describe("removeAdmin", () => { + it("adds admin", async () => { + await vestingRegistry.addAdmin(account1); + let tx = await vestingRegistry.removeAdmin(account1); + + expectEvent(tx, "AdminRemoved", { + admin: account1, + }); + + let isAdmin = await vestingRegistry.admins(account1); + expect(isAdmin).equal(false); + }); + + it("fails sender isn't an owner", async () => { + await expectRevert( + vestingRegistry.removeAdmin(account1, { from: account1 }), + "unauthorized" + ); + }); + }); + + describe("setCSOVtokens", () => { + it("sets the expected values", async () => { + await vestingRegistry.setCSOVtokens([account2, account3]); + + let _CSOV1 = await vestingRegistry.CSOVtokens(0); + let _CSOV2 = await vestingRegistry.CSOVtokens(1); + + expect(_CSOV1).equal(account2); + expect(_CSOV2).equal(account3); + }); + + it("fails if the 0 address is passed as cSOV address", async () => { + await expectRevert( + vestingRegistry.setCSOVtokens([cSOV1.address, cSOV2.address, ZERO_ADDRESS]), + "CSOV address invalid" + ); + }); + + it("fails if sender isn't an owner", async () => { + await expectRevert( + vestingRegistry.setCSOVtokens([cSOV1.address, cSOV2.address], { from: account2 }), + "unauthorized" + ); + }); + }); + + describe("transferSOV", () => { + it("should be able to transfer SOV", async () => { + let amount = new BN(1000); + await SOV.transfer(vestingRegistry.address, amount); + + let balanceBefore = await SOV.balanceOf(account1); + await vestingRegistry.transferSOV(account1, amount); + let balanceAfter = await SOV.balanceOf(account1); + + expect(amount).to.be.bignumber.equal(balanceAfter.sub(balanceBefore)); + }); + + it("only owner should be able to transfer", async () => { + await expectRevert( + vestingRegistry.transferSOV(account1, 1000, { from: account1 }), + "unauthorized" + ); + }); + + it("fails if the 0 address is passed as receiver address", async () => { + await expectRevert( + vestingRegistry.transferSOV(ZERO_ADDRESS, 1000), + "receiver address invalid" + ); + }); + + it("fails if the 0 is passed as an amount", async () => { + await expectRevert(vestingRegistry.transferSOV(account1, 0), "amount invalid"); + }); + }); + + describe("setBlacklistFlag", () => { + it("should be able to add user to blacklist", async () => { + await vestingRegistry.setBlacklistFlag(account2, true); + + let blacklisted = await vestingRegistry.blacklist(account2); + expect(blacklisted).equal(true); + }); + + it("fails if the 0 address is passed", async () => { + await expectRevert( + vestingRegistry.setBlacklistFlag(ZERO_ADDRESS, true), + "account address invalid" + ); + }); + }); + + describe("setLockedAmount", () => { + it("should be able to set locked amount", async () => { + let amount = new BN(123); + await vestingRegistry.setLockedAmount(account2, amount); + + let lockedAmount = await vestingRegistry.lockedAmount(account2); + expect(lockedAmount).to.be.bignumber.equal(amount); + }); + + it("fails if the 0 address is passed", async () => { + await expectRevert( + vestingRegistry.setLockedAmount(ZERO_ADDRESS, 111), + "account address invalid" + ); + }); + + it("fails if the 0 amount is passed", async () => { + await expectRevert(vestingRegistry.setLockedAmount(account2, 0), "amount invalid"); + }); + }); + + describe("exchangeAllCSOV", () => { + it("should be able to exchange CSOV", async () => { + /// @dev This test requires a hard reset of init fixture + await deploymentAndInitFixture(); + + let amount1 = new BN(1000); + let amount2 = new BN(2000); + await cSOV1.transfer(account2, amount1); + await cSOV2.transfer(account2, amount2); + let amount = amount1.add(amount2); + await SOV.transfer(vestingRegistry.address, amount); + + let tx = await vestingRegistry.exchangeAllCSOV({ from: account2 }); + + expectEvent(tx, "CSOVTokensExchanged", { + caller: account2, + amount: amount, + }); + + let processedList = await vestingRegistry.processedList(account2); + expect(processedList).equal(true); + + let balance = await SOV.balanceOf(vestingRegistry.address); + expect(balance.toString()).equal("0"); + + let cliff = await vestingRegistry.CSOV_VESTING_CLIFF(); + let duration = await vestingRegistry.CSOV_VESTING_DURATION(); + + let vestingAddress = await vestingRegistry.getVesting(account2); + let vesting = await VestingLogic.at(vestingAddress); + await checkVesting(vesting, account2, cliff, duration, amount); + + await expectRevert(vesting.governanceWithdrawTokens(account2), "revert"); + }); + + it("should be able to exchange CSOV partially", async () => { + /// @dev This test requires a hard reset of init fixture + await deploymentAndInitFixture(); + + let amount1 = new BN(1000); + let amount2 = new BN(2000); + let lockedAmount = new BN(700); + await cSOV1.transfer(account2, amount1); + await cSOV2.transfer(account2, amount2); + + let amount = amount1.add(amount2).sub(lockedAmount); + await SOV.transfer(vestingRegistry.address, amount); + + await vestingRegistry.setLockedAmount(account2, lockedAmount); + let tx = await vestingRegistry.exchangeAllCSOV({ from: account2 }); + + expectEvent(tx, "CSOVTokensExchanged", { + caller: account2, + amount: amount, + }); + + let processedList = await vestingRegistry.processedList(account2); + expect(processedList).equal(true); + + let balance = await SOV.balanceOf(vestingRegistry.address); + expect(balance.toString()).equal("0"); + + let cliff = await vestingRegistry.CSOV_VESTING_CLIFF(); + let duration = await vestingRegistry.CSOV_VESTING_DURATION(); + + let vestingAddress = await vestingRegistry.getVesting(account2); + let vesting = await VestingLogic.at(vestingAddress); + await checkVesting(vesting, account2, cliff, duration, amount); + + await expectRevert(vesting.governanceWithdrawTokens(account2), "revert"); + }); + + it("fails if trying to withdraw second time", async () => { + /// @dev This test requires a hard reset of init fixture + await deploymentAndInitFixture(); + + let amount = new BN(1000); + await cSOV1.transfer(account2, amount); + await SOV.transfer(vestingRegistry.address, amount); + + await vestingRegistry.exchangeAllCSOV({ from: account2 }); + await expectRevert( + vestingRegistry.exchangeAllCSOV({ from: account2 }), + "Address cannot be processed twice" + ); + }); + + it("fails if account blacklisted", async () => { + /// @dev This test requires a hard reset of init fixture + await deploymentAndInitFixture(); + + let amount = new BN(1000); + await cSOV1.transfer(account2, amount); + await SOV.transfer(vestingRegistry.address, amount); + + await vestingRegistry.setBlacklistFlag(account2, true); + await expectRevert( + vestingRegistry.exchangeAllCSOV({ from: account2 }), + "Address blacklisted" + ); + }); + + it("fails if the 0 is cSOV amount", async () => { + /// @dev This test requires a hard reset of init fixture + await deploymentAndInitFixture(); + + await expectRevert( + vestingRegistry.exchangeAllCSOV({ from: account2 }), + "amount invalid" + ); + }); + + it("fails if vestingRegistry doesn't have enough SOV", async () => { + /// @dev This test requires a hard reset of init fixture + await deploymentAndInitFixture(); + + let amount = new BN(1000); + await cSOV1.transfer(account2, amount); + + await expectRevert( + vestingRegistry.exchangeAllCSOV({ from: account2 }), + "ERC20: transfer amount exceeds balance" + ); + }); + }); + + describe("createVesting", () => { + it("should be able to create vesting", async () => { + let amount = new BN(1000000); + await SOV.transfer(vestingRegistry.address, amount); + + let cliff = FOUR_WEEKS; + let duration = FOUR_WEEKS.mul(new BN(20)); + let tx = await vestingRegistry.createVesting(account2, amount, cliff, duration); + let vestingAddress = await vestingRegistry.getVesting(account2); + await vestingRegistry.stakeTokens(vestingAddress, amount); + + expectEvent(tx, "VestingCreated", { + tokenOwner: account2, + vesting: vestingAddress, + cliff: cliff, + duration: duration, + amount: amount, + }); + + let balance = await SOV.balanceOf(vestingRegistry.address); + expect(balance.toString()).equal("0"); + + let vesting = await VestingLogic.at(vestingAddress); + await checkVesting(vesting, account2, cliff, duration, amount); + + await expectRevert( + vesting.governanceWithdrawTokens(account2), + "operation not supported" + ); + + let proxy = await UpgradableProxy.at(vestingAddress); + await expectRevert(proxy.setImplementation(account2), "revert"); + }); + + it("fails if vestingRegistry doesn't have enough SOV", async () => { + let amount = new BN(1000000); + let cliff = FOUR_WEEKS; + let duration = FOUR_WEEKS.mul(new BN(20)); + + await vestingRegistry.createVesting(account2, amount, cliff, duration); + let vestingAddress = await vestingRegistry.getVesting(account2); + + await expectRevert( + vestingRegistry.stakeTokens(vestingAddress, amount), + "ERC20: transfer amount exceeds balance" + ); + }); + + it("fails if sender is not an owner or admin", async () => { + let amount = new BN(1000000); + let cliff = TEAM_VESTING_CLIFF; + let duration = TEAM_VESTING_DURATION; + + await expectRevert( + vestingRegistry.createVesting(account2, amount, cliff, duration, { + from: account1, + }), + "unauthorized" + ); + + await vestingRegistry.addAdmin(account1); + await vestingRegistry.createVesting(account2, amount, cliff, duration, { + from: account1, + }); + }); + }); + + describe("createTeamVesting", () => { + it("should be able to create vesting", async () => { + /// @dev This test requires a hard reset of init fixture + await deploymentAndInitFixture(); + + let amount = new BN(1000000); + await SOV.transfer(vestingRegistry.address, amount); + + let cliff = TEAM_VESTING_CLIFF; + let duration = TEAM_VESTING_DURATION; + let tx = await vestingRegistry.createTeamVesting(account2, amount, cliff, duration); + let vestingAddress = await vestingRegistry.getTeamVesting(account2); + let tx2 = await vestingRegistry.stakeTokens(vestingAddress, amount); + + console.log("\ngasUsed = " + tx.receipt.gasUsed); + console.log("gasUsed = " + tx2.receipt.gasUsed); + + expectEvent(tx, "TeamVestingCreated", { + tokenOwner: account2, + vesting: vestingAddress, + cliff: cliff, + duration: duration, + amount: amount, + }); + + let balance = await SOV.balanceOf(vestingRegistry.address); + expect(balance.toString()).equal("0"); + + let vesting = await VestingLogic.at(vestingAddress); + await checkVesting(vesting, account2, cliff, duration, amount); + + await expectRevert(vesting.governanceWithdrawTokens(account2), "unauthorized"); + + let proxy = await UpgradableProxy.at(vestingAddress); + await expectRevert(proxy.setImplementation(account2), "revert"); + }); + + it("fails if vestingRegistry doesn't have enough SOV", async () => { + let amount = new BN(1000000); + let cliff = TEAM_VESTING_CLIFF; + let duration = TEAM_VESTING_DURATION; + + await vestingRegistry.createTeamVesting(account2, amount, cliff, duration); + let vestingAddress = await vestingRegistry.getTeamVesting(account2); + + await expectRevert( + vestingRegistry.stakeTokens(vestingAddress, amount), + "ERC20: transfer amount exceeds balance" + ); + }); + + it("fails if sender is not an owner or admin", async () => { + let amount = new BN(1000000); + let cliff = TEAM_VESTING_CLIFF; + let duration = TEAM_VESTING_DURATION; + + await expectRevert( + vestingRegistry.createTeamVesting(account2, amount, cliff, duration, { + from: account1, + }), + "unauthorized" + ); + + await vestingRegistry.addAdmin(account1); + await vestingRegistry.createTeamVesting(account2, amount, cliff, duration, { + from: account1, + }); + }); + }); + + describe("stakeTokens", () => { + it("fails if the 0 address is passed as vesting address", async () => { + await expectRevert( + vestingRegistry.stakeTokens(ZERO_ADDRESS, new BN(1000000)), + "vesting address invalid" + ); + }); + + it("fails if the 0 address is passed as an amount", async () => { + await expectRevert(vestingRegistry.stakeTokens(account1, 0), "amount invalid"); + }); + + it("only owner or admin should be able to stake tokens", async () => { + /// @dev This test requires a hard reset of init fixture + await deploymentAndInitFixture(); + + let amount = new BN(1000000); + await SOV.transfer(vestingRegistry.address, amount); + + let cliff = TEAM_VESTING_CLIFF; + let duration = TEAM_VESTING_DURATION; + await vestingRegistry.createTeamVesting(account2, amount, cliff, duration); + let vestingAddress = await vestingRegistry.getTeamVesting(account2); + + await expectRevert( + vestingRegistry.stakeTokens(vestingAddress, new BN(1000000), { from: account1 }), + "unauthorized" + ); + + await vestingRegistry.addAdmin(account1); + await vestingRegistry.stakeTokens(vestingAddress, new BN(1000000), { from: account1 }); + }); + }); + + describe("OrigingVestingCreator", () => { + it("should be able to create vesting", async () => { + await deploymentAndInitFixture(); + + let origingVestingCreator = await OrigingVestingCreator.new(vestingRegistry.address); + + let amount = new BN(1000000); + await SOV.transfer(vestingRegistry.address, amount); + + let cliff = FOUR_WEEKS; + let duration = FOUR_WEEKS.mul(new BN(20)); + + // Adding origingVestingCreator as an admin in the Vesting Registry. + await vestingRegistry.addAdmin(origingVestingCreator.address); + + // Try to use address(0) + await expectRevert( + origingVestingCreator.createVesting(ZERO_ADDRESS, amount, cliff, duration), + "Invalid address" + ); + + // Create the vesting contract + await origingVestingCreator.createVesting(account3, amount, cliff, duration); + + // Try to create it again w/ the same tokenOwner + await expectRevert( + origingVestingCreator.createVesting(account3, amount, cliff, duration), + "Already processed" + ); + }); + }); + + async function checkVesting(vesting, account, cliff, duration, amount) { + await mineBlock(); + + let vestingBalance = await staking.balanceOf(vesting.address); + expect(vestingBalance).to.be.bignumber.equal(amount); + + let accountVotes = await staking.getCurrentVotes(account); + expect(accountVotes).to.be.not.equal(new BN(0)); + let vestingVotes = await staking.getCurrentVotes(vesting.address); + expect(vestingVotes).to.be.bignumber.equal(new BN(0)); + + let startDate = await vesting.startDate(); + let start = startDate.toNumber() + cliff.toNumber(); + let end = startDate.toNumber() + duration.toNumber(); + + let numIntervals = Math.floor((end - start) / FOUR_WEEKS) + 1; + let stakedPerInterval = Math.floor(amount / numIntervals); + + let stakeForFirstInterval = amount - stakedPerInterval * (numIntervals - 1); + + expect(await vesting.cliff()).to.be.bignumber.equal(cliff); + expect(await vesting.duration()).to.be.bignumber.equal(duration); + + for (let i = start; i <= end; i += FOUR_WEEKS) { + let lockedTS = await staking.timestampToLockDate(i); + + let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints( + vesting.address, + lockedTS + ); + let userStakingCheckpoints = await staking.userStakingCheckpoints( + vesting.address, + lockedTS, + numUserStakingCheckpoints - 1 + ); + assert.equal(numUserStakingCheckpoints.toString(), "1"); + if (i === start) { + assert.equal(userStakingCheckpoints.stake.toString(), stakeForFirstInterval); + } else { + assert.equal(userStakingCheckpoints.stake.toString(), stakedPerInterval); + } + + let numDelegateStakingCheckpoints = await staking.numDelegateStakingCheckpoints( + account, + lockedTS + ); + let delegateStakingCheckpoints = await staking.delegateStakingCheckpoints( + account, + lockedTS, + numUserStakingCheckpoints - 1 + ); + assert.equal(numDelegateStakingCheckpoints.toString(), "1"); + if (i === start) { + assert.equal(delegateStakingCheckpoints.stake.toString(), stakeForFirstInterval); + } else { + assert.equal(delegateStakingCheckpoints.stake.toString(), stakedPerInterval); + } + } + } }); diff --git a/tests/vesting/VestingRegistry2.js b/tests/vesting/VestingRegistry2.js index 21c174dd5..0aec1c197 100644 --- a/tests/vesting/VestingRegistry2.js +++ b/tests/vesting/VestingRegistry2.js @@ -45,507 +45,580 @@ const ZERO_ADDRESS = constants.ZERO_ADDRESS; const pricsSats = "2500"; contract("VestingRegistry", (accounts) => { - let root, account1, account2, account3; - let SOV, cSOV1, cSOV2; - let staking, stakingLogic, feeSharingProxy; - let vestingFactory, vestingLogic, vestingRegistry; - - async function deploymentAndInitFixture(_wallets, _provider) { - SOV = await SOV_ABI.new(TOTAL_SUPPLY); - cSOV1 = await TestToken.new("cSOV1", "cSOV1", 18, TOTAL_SUPPLY); - cSOV2 = await TestToken.new("cSOV2", "cSOV2", 18, TOTAL_SUPPLY); - - stakingLogic = await StakingLogic.new(SOV.address); - staking = await StakingProxy.new(SOV.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingLogic.at(staking.address); - - feeSharingProxy = await FeeSharingProxy.new(ZERO_ADDRESS, staking.address); - - vestingLogic = await VestingLogic.new(); - vestingFactory = await VestingFactory.new(vestingLogic.address); - vestingRegistry = await VestingRegistry.new( - vestingFactory.address, - SOV.address, - [cSOV1.address, cSOV2.address], - pricsSats, - staking.address, - feeSharingProxy.address, - account1 - ); - vestingFactory.transferOwnership(vestingRegistry.address); - } - - before(async () => { - [root, account1, account2, account3, ...accounts] = accounts; - }); - - beforeEach(async () => { - /// @dev Only some tests really require an initial redeployment - // await loadFixture(deploymentAndInitFixture); - }); - - describe("constructor", () => { - it("sets the expected values", async () => { - await loadFixture(deploymentAndInitFixture); - - let _sov = await vestingRegistry.SOV(); - let _CSOV1 = await vestingRegistry.CSOVtokens(0); - let _CSOV2 = await vestingRegistry.CSOVtokens(1); - let _stacking = await vestingRegistry.staking(); - let _feeSharingProxy = await vestingRegistry.feeSharingProxy(); - let _vestingOwner = await vestingRegistry.vestingOwner(); - - expect(_sov).equal(SOV.address); - expect(_CSOV1).equal(cSOV1.address); - expect(_CSOV2).equal(cSOV2.address); - expect(_stacking).equal(staking.address); - expect(_feeSharingProxy).equal(feeSharingProxy.address); - expect(_vestingOwner).equal(account1); - }); - - it("fails if the 0 address is passed as vestingFactory address", async () => { - await expectRevert( - VestingRegistry.new( - ZERO_ADDRESS, - SOV.address, - [cSOV1.address, cSOV2.address], - pricsSats, - staking.address, - feeSharingProxy.address, - account1 - ), - "vestingFactory address invalid" - ); - }); - - it("fails if the 0 address is passed as SOV address", async () => { - await expectRevert( - VestingRegistry.new( - vestingFactory.address, - ZERO_ADDRESS, - [cSOV1.address, cSOV2.address], - pricsSats, - staking.address, - feeSharingProxy.address, - account1 - ), - "SOV address invalid" - ); - }); - - it("fails if the 0 address is passed as cSOV address", async () => { - await expectRevert( - VestingRegistry.new( - vestingFactory.address, - SOV.address, - [cSOV1.address, cSOV2.address, ZERO_ADDRESS], - pricsSats, - staking.address, - feeSharingProxy.address, - account1 - ), - "CSOV address invalid" - ); - }); - - it("fails if the 0 address is passed as staking address", async () => { - await expectRevert( - VestingRegistry.new( - vestingFactory.address, - SOV.address, - [cSOV1.address, cSOV2.address], - pricsSats, - ZERO_ADDRESS, - feeSharingProxy.address, - account1 - ), - "staking address invalid" - ); - }); - - it("fails if the 0 address is passed as feeSharingProxy address", async () => { - await expectRevert( - VestingRegistry.new( - vestingFactory.address, - SOV.address, - [cSOV1.address, cSOV2.address], - pricsSats, - staking.address, - ZERO_ADDRESS, - account1 - ), - "feeSharingProxy address invalid" - ); - }); - - it("fails if the 0 address is passed as vestingOwner address", async () => { - await expectRevert( - VestingRegistry.new( - vestingFactory.address, - SOV.address, - [cSOV1.address, cSOV2.address], - pricsSats, - staking.address, - feeSharingProxy.address, - ZERO_ADDRESS - ), - "vestingOwner address invalid" - ); - }); - }); - - describe("setVestingFactory", () => { - it("sets vesting factory", async () => { - await vestingRegistry.setVestingFactory(account2); - - let vestingFactory = await vestingRegistry.vestingFactory(); - expect(vestingFactory).equal(account2); - }); - - it("fails if the 0 address is passed", async () => { - await expectRevert(vestingRegistry.setVestingFactory(ZERO_ADDRESS), "vestingFactory address invalid"); - }); - - it("fails if sender isn't an owner", async () => { - await expectRevert(vestingRegistry.setVestingFactory(account2, { from: account2 }), "unauthorized"); - }); - }); - - describe("addAdmin", () => { - it("adds admin", async () => { - let tx = await vestingRegistry.addAdmin(account1); - - expectEvent(tx, "AdminAdded", { - admin: account1, - }); - - let isAdmin = await vestingRegistry.admins(account1); - expect(isAdmin).equal(true); - }); - - it("fails sender isn't an owner", async () => { - await expectRevert(vestingRegistry.addAdmin(account1, { from: account1 }), "unauthorized"); - }); - }); - - describe("removeAdmin", () => { - it("adds admin", async () => { - await vestingRegistry.addAdmin(account1); - let tx = await vestingRegistry.removeAdmin(account1); - - expectEvent(tx, "AdminRemoved", { - admin: account1, - }); - - let isAdmin = await vestingRegistry.admins(account1); - expect(isAdmin).equal(false); - }); - - it("fails sender isn't an owner", async () => { - await expectRevert(vestingRegistry.removeAdmin(account1, { from: account1 }), "unauthorized"); - }); - }); - - describe("setCSOVtokens", () => { - it("sets the expected values", async () => { - await vestingRegistry.setCSOVtokens([account2, account3]); - - let _CSOV1 = await vestingRegistry.CSOVtokens(0); - let _CSOV2 = await vestingRegistry.CSOVtokens(1); - - expect(_CSOV1).equal(account2); - expect(_CSOV2).equal(account3); - }); - - it("fails if the 0 address is passed as cSOV address", async () => { - await expectRevert(vestingRegistry.setCSOVtokens([cSOV1.address, cSOV2.address, ZERO_ADDRESS]), "CSOV address invalid"); - }); - - it("fails if sender isn't an owner", async () => { - await expectRevert(vestingRegistry.setCSOVtokens([cSOV1.address, cSOV2.address], { from: account2 }), "unauthorized"); - }); - }); - - describe("transferSOV", () => { - it("should be able to transfer SOV", async () => { - let amount = new BN(1000); - await SOV.transfer(vestingRegistry.address, amount); - - let balanceBefore = await SOV.balanceOf(account1); - await vestingRegistry.transferSOV(account1, amount); - let balanceAfter = await SOV.balanceOf(account1); - - expect(amount).to.be.bignumber.equal(balanceAfter.sub(balanceBefore)); - }); - - it("only owner should be able to transfer", async () => { - await expectRevert(vestingRegistry.transferSOV(account1, 1000, { from: account1 }), "unauthorized"); - }); - - it("fails if the 0 address is passed as receiver address", async () => { - await expectRevert(vestingRegistry.transferSOV(ZERO_ADDRESS, 1000), "receiver address invalid"); - }); - - it("fails if the 0 is passed as an amount", async () => { - await expectRevert(vestingRegistry.transferSOV(account1, 0), "amount invalid"); - }); - }); - - describe("setBlacklistFlag", () => { - it("should be able to add user to blacklist", async () => { - await vestingRegistry.setBlacklistFlag(account2, true); - - let blacklisted = await vestingRegistry.blacklist(account2); - expect(blacklisted).equal(true); - }); - - it("fails if the 0 address is passed", async () => { - await expectRevert(vestingRegistry.setBlacklistFlag(ZERO_ADDRESS, true), "account address invalid"); - }); - }); - - describe("setLockedAmount", () => { - it("should be able to set locked amount", async () => { - let amount = new BN(123); - await vestingRegistry.setLockedAmount(account2, amount); - - let lockedAmount = await vestingRegistry.lockedAmount(account2); - expect(lockedAmount).to.be.bignumber.equal(amount); - }); - - it("fails if the 0 address is passed", async () => { - await expectRevert(vestingRegistry.setLockedAmount(ZERO_ADDRESS, 111), "account address invalid"); - }); - - it("fails if the 0 amount is passed", async () => { - await expectRevert(vestingRegistry.setLockedAmount(account2, 0), "amount invalid"); - }); - }); - - describe("createVesting", () => { - it("should be able to create vesting", async () => { - /// @dev This test requires a hard reset of init fixture - await deploymentAndInitFixture(); - - let amount = new BN(1000000); - await SOV.transfer(vestingRegistry.address, amount); - - let cliff = FOUR_WEEKS; - let duration = FOUR_WEEKS.mul(new BN(20)); - let tx = await vestingRegistry.createVesting(account2, amount, cliff, duration); - let vestingAddress = await vestingRegistry.getVesting(account2); - await vestingRegistry.stakeTokens(vestingAddress, amount); - - expectEvent(tx, "VestingCreated", { - tokenOwner: account2, - vesting: vestingAddress, - cliff: cliff, - duration: duration, - amount: amount, - }); - - let balance = await SOV.balanceOf(vestingRegistry.address); - expect(balance.toString()).equal("0"); - - let vesting = await VestingLogic.at(vestingAddress); - await checkVesting(vesting, account2, cliff, duration, amount); - - await expectRevert(vesting.governanceWithdrawTokens(account2), "operation not supported"); - - let proxy = await UpgradableProxy.at(vestingAddress); - await expectRevert(proxy.setImplementation(account2), "revert"); - }); - - it("fails if vestingRegistry doesn't have enough SOV", async () => { - let amount = new BN(1000000); - let cliff = FOUR_WEEKS; - let duration = FOUR_WEEKS.mul(new BN(20)); - - await vestingRegistry.createVesting(account2, amount, cliff, duration); - let vestingAddress = await vestingRegistry.getVesting(account2); - - await expectRevert(vestingRegistry.stakeTokens(vestingAddress, amount), "ERC20: transfer amount exceeds balance"); - }); - - it("fails if sender is not an owner or admin", async () => { - let amount = new BN(1000000); - let cliff = TEAM_VESTING_CLIFF; - let duration = TEAM_VESTING_DURATION; - - await expectRevert(vestingRegistry.createVesting(account2, amount, cliff, duration, { from: account1 }), "unauthorized"); - - await vestingRegistry.addAdmin(account1); - await vestingRegistry.createVesting(account2, amount, cliff, duration, { from: account1 }); - }); - }); - - describe("createTeamVesting", () => { - it("should be able to create vesting", async () => { - /// @dev This test requires a hard reset of init fixture - await deploymentAndInitFixture(); - - let amount = new BN(1000000); - await SOV.transfer(vestingRegistry.address, amount); - - let cliff = TEAM_VESTING_CLIFF; - let duration = TEAM_VESTING_DURATION; - let tx = await vestingRegistry.createTeamVesting(account2, amount, cliff, duration); - let vestingAddress = await vestingRegistry.getTeamVesting(account2); - let tx2 = await vestingRegistry.stakeTokens(vestingAddress, amount); - - console.log("\ngasUsed = " + tx.receipt.gasUsed); - console.log("gasUsed = " + tx2.receipt.gasUsed); - - expectEvent(tx, "TeamVestingCreated", { - tokenOwner: account2, - vesting: vestingAddress, - cliff: cliff, - duration: duration, - amount: amount, - }); - - let balance = await SOV.balanceOf(vestingRegistry.address); - expect(balance.toString()).equal("0"); - - let vesting = await VestingLogic.at(vestingAddress); - await checkVesting(vesting, account2, cliff, duration, amount); - - await expectRevert(vesting.governanceWithdrawTokens(account2), "unauthorized"); - - let proxy = await UpgradableProxy.at(vestingAddress); - await expectRevert(proxy.setImplementation(account2), "revert"); - }); - - it("fails if vestingRegistry doesn't have enough SOV", async () => { - let amount = new BN(1000000); - let cliff = TEAM_VESTING_CLIFF; - let duration = TEAM_VESTING_DURATION; - - await vestingRegistry.createTeamVesting(account2, amount, cliff, duration); - let vestingAddress = await vestingRegistry.getTeamVesting(account2); - - await expectRevert(vestingRegistry.stakeTokens(vestingAddress, amount), "ERC20: transfer amount exceeds balance"); - }); - - it("fails if sender is not an owner or admin", async () => { - let amount = new BN(1000000); - let cliff = TEAM_VESTING_CLIFF; - let duration = TEAM_VESTING_DURATION; - - await expectRevert(vestingRegistry.createTeamVesting(account2, amount, cliff, duration, { from: account1 }), "unauthorized"); - - await vestingRegistry.addAdmin(account1); - await vestingRegistry.createTeamVesting(account2, amount, cliff, duration, { from: account1 }); - }); - }); - - describe("stakeTokens", () => { - it("fails if the 0 address is passed as vesting address", async () => { - await expectRevert(vestingRegistry.stakeTokens(ZERO_ADDRESS, new BN(1000000)), "vesting address invalid"); - }); - - it("fails if the 0 address is passed as an amount", async () => { - await expectRevert(vestingRegistry.stakeTokens(account1, 0), "amount invalid"); - }); - - it("only owner or admin should be able to stake tokens", async () => { - /// @dev This test requires a hard reset of init fixture - await deploymentAndInitFixture(); - - let amount = new BN(1000000); - await SOV.transfer(vestingRegistry.address, amount); - - let cliff = TEAM_VESTING_CLIFF; - let duration = TEAM_VESTING_DURATION; - await vestingRegistry.createTeamVesting(account2, amount, cliff, duration); - let vestingAddress = await vestingRegistry.getTeamVesting(account2); - - await expectRevert(vestingRegistry.stakeTokens(vestingAddress, new BN(1000000), { from: account1 }), "unauthorized"); - - await vestingRegistry.addAdmin(account1); - await vestingRegistry.stakeTokens(vestingAddress, new BN(1000000), { from: account1 }); - }); - }); - - describe("deposit funds", () => { - it("should get budget and deposit", async () => { - await deploymentAndInitFixture(); - - // Check budget is 0 - let budget = await vestingRegistry.budget(); - // console.log("budget: " + budget); - expect(budget).to.be.bignumber.equal(new BN(0)); - - // Deposit funds - const amount = web3.utils.toWei("3"); - await vestingRegistry.deposit({ from: accounts[1], value: amount }); - - // Check budget is not 0 - budget = await vestingRegistry.budget(); - // console.log("budget: " + budget); - expect(budget).to.be.bignumber.equal(amount); - - // Get recipient's balance before withdrawal - let balance2before = await provider.getBalance(accounts[2]); - // console.log("balance2before: " + balance2before); - - // Check budget after withdrawal - await vestingRegistry.withdrawAll(accounts[2]); - budget = await vestingRegistry.budget(); - // console.log("budget: " + budget); - expect(budget).to.be.bignumber.equal(new BN(0)); - - // Get recipient's balance after withdrawal - let balance2after = await provider.getBalance(accounts[2]); - // console.log("balance2after: " + balance2after); - expect(balance2after.sub(balance2before)).to.be.equal(amount); - }); - }); - - async function checkVesting(vesting, account, cliff, duration, amount) { - await mineBlock(); - - let vestingBalance = await staking.balanceOf(vesting.address); - expect(vestingBalance).to.be.bignumber.equal(amount); - - let accountVotes = await staking.getCurrentVotes(account); - expect(accountVotes).to.be.not.equal(new BN(0)); - let vestingVotes = await staking.getCurrentVotes(vesting.address); - expect(vestingVotes).to.be.bignumber.equal(new BN(0)); - - let startDate = await vesting.startDate(); - let start = startDate.toNumber() + cliff.toNumber(); - let end = startDate.toNumber() + duration.toNumber(); - - let numIntervals = Math.floor((end - start) / FOUR_WEEKS) + 1; - let stakedPerInterval = Math.floor(amount / numIntervals); - - let stakeForFirstInterval = amount - stakedPerInterval * (numIntervals - 1); - - expect(await vesting.cliff()).to.be.bignumber.equal(cliff); - expect(await vesting.duration()).to.be.bignumber.equal(duration); - - for (let i = start; i <= end; i += FOUR_WEEKS) { - let lockedTS = await staking.timestampToLockDate(i); - - let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints(vesting.address, lockedTS); - let userStakingCheckpoints = await staking.userStakingCheckpoints(vesting.address, lockedTS, numUserStakingCheckpoints - 1); - assert.equal(numUserStakingCheckpoints.toString(), "1"); - if (i === start) { - assert.equal(userStakingCheckpoints.stake.toString(), stakeForFirstInterval); - } else { - assert.equal(userStakingCheckpoints.stake.toString(), stakedPerInterval); - } - - let numDelegateStakingCheckpoints = await staking.numDelegateStakingCheckpoints(account, lockedTS); - let delegateStakingCheckpoints = await staking.delegateStakingCheckpoints(account, lockedTS, numUserStakingCheckpoints - 1); - assert.equal(numDelegateStakingCheckpoints.toString(), "1"); - if (i === start) { - assert.equal(delegateStakingCheckpoints.stake.toString(), stakeForFirstInterval); - } else { - assert.equal(delegateStakingCheckpoints.stake.toString(), stakedPerInterval); - } - } - } + let root, account1, account2, account3; + let SOV, cSOV1, cSOV2; + let staking, stakingLogic, feeSharingProxy; + let vestingFactory, vestingLogic, vestingRegistry; + + async function deploymentAndInitFixture(_wallets, _provider) { + SOV = await SOV_ABI.new(TOTAL_SUPPLY); + cSOV1 = await TestToken.new("cSOV1", "cSOV1", 18, TOTAL_SUPPLY); + cSOV2 = await TestToken.new("cSOV2", "cSOV2", 18, TOTAL_SUPPLY); + + stakingLogic = await StakingLogic.new(SOV.address); + staking = await StakingProxy.new(SOV.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); + + feeSharingProxy = await FeeSharingProxy.new(ZERO_ADDRESS, staking.address); + + vestingLogic = await VestingLogic.new(); + vestingFactory = await VestingFactory.new(vestingLogic.address); + vestingRegistry = await VestingRegistry.new( + vestingFactory.address, + SOV.address, + [cSOV1.address, cSOV2.address], + pricsSats, + staking.address, + feeSharingProxy.address, + account1 + ); + vestingFactory.transferOwnership(vestingRegistry.address); + } + + before(async () => { + [root, account1, account2, account3, ...accounts] = accounts; + }); + + beforeEach(async () => { + /// @dev Only some tests really require an initial redeployment + // await loadFixture(deploymentAndInitFixture); + }); + + describe("constructor", () => { + it("sets the expected values", async () => { + await loadFixture(deploymentAndInitFixture); + + let _sov = await vestingRegistry.SOV(); + let _CSOV1 = await vestingRegistry.CSOVtokens(0); + let _CSOV2 = await vestingRegistry.CSOVtokens(1); + let _stacking = await vestingRegistry.staking(); + let _feeSharingProxy = await vestingRegistry.feeSharingProxy(); + let _vestingOwner = await vestingRegistry.vestingOwner(); + + expect(_sov).equal(SOV.address); + expect(_CSOV1).equal(cSOV1.address); + expect(_CSOV2).equal(cSOV2.address); + expect(_stacking).equal(staking.address); + expect(_feeSharingProxy).equal(feeSharingProxy.address); + expect(_vestingOwner).equal(account1); + }); + + it("fails if the 0 address is passed as vestingFactory address", async () => { + await expectRevert( + VestingRegistry.new( + ZERO_ADDRESS, + SOV.address, + [cSOV1.address, cSOV2.address], + pricsSats, + staking.address, + feeSharingProxy.address, + account1 + ), + "vestingFactory address invalid" + ); + }); + + it("fails if the 0 address is passed as SOV address", async () => { + await expectRevert( + VestingRegistry.new( + vestingFactory.address, + ZERO_ADDRESS, + [cSOV1.address, cSOV2.address], + pricsSats, + staking.address, + feeSharingProxy.address, + account1 + ), + "SOV address invalid" + ); + }); + + it("fails if the 0 address is passed as cSOV address", async () => { + await expectRevert( + VestingRegistry.new( + vestingFactory.address, + SOV.address, + [cSOV1.address, cSOV2.address, ZERO_ADDRESS], + pricsSats, + staking.address, + feeSharingProxy.address, + account1 + ), + "CSOV address invalid" + ); + }); + + it("fails if the 0 address is passed as staking address", async () => { + await expectRevert( + VestingRegistry.new( + vestingFactory.address, + SOV.address, + [cSOV1.address, cSOV2.address], + pricsSats, + ZERO_ADDRESS, + feeSharingProxy.address, + account1 + ), + "staking address invalid" + ); + }); + + it("fails if the 0 address is passed as feeSharingProxy address", async () => { + await expectRevert( + VestingRegistry.new( + vestingFactory.address, + SOV.address, + [cSOV1.address, cSOV2.address], + pricsSats, + staking.address, + ZERO_ADDRESS, + account1 + ), + "feeSharingProxy address invalid" + ); + }); + + it("fails if the 0 address is passed as vestingOwner address", async () => { + await expectRevert( + VestingRegistry.new( + vestingFactory.address, + SOV.address, + [cSOV1.address, cSOV2.address], + pricsSats, + staking.address, + feeSharingProxy.address, + ZERO_ADDRESS + ), + "vestingOwner address invalid" + ); + }); + }); + + describe("setVestingFactory", () => { + it("sets vesting factory", async () => { + await vestingRegistry.setVestingFactory(account2); + + let vestingFactory = await vestingRegistry.vestingFactory(); + expect(vestingFactory).equal(account2); + }); + + it("fails if the 0 address is passed", async () => { + await expectRevert( + vestingRegistry.setVestingFactory(ZERO_ADDRESS), + "vestingFactory address invalid" + ); + }); + + it("fails if sender isn't an owner", async () => { + await expectRevert( + vestingRegistry.setVestingFactory(account2, { from: account2 }), + "unauthorized" + ); + }); + }); + + describe("addAdmin", () => { + it("adds admin", async () => { + let tx = await vestingRegistry.addAdmin(account1); + + expectEvent(tx, "AdminAdded", { + admin: account1, + }); + + let isAdmin = await vestingRegistry.admins(account1); + expect(isAdmin).equal(true); + }); + + it("fails sender isn't an owner", async () => { + await expectRevert( + vestingRegistry.addAdmin(account1, { from: account1 }), + "unauthorized" + ); + }); + }); + + describe("removeAdmin", () => { + it("adds admin", async () => { + await vestingRegistry.addAdmin(account1); + let tx = await vestingRegistry.removeAdmin(account1); + + expectEvent(tx, "AdminRemoved", { + admin: account1, + }); + + let isAdmin = await vestingRegistry.admins(account1); + expect(isAdmin).equal(false); + }); + + it("fails sender isn't an owner", async () => { + await expectRevert( + vestingRegistry.removeAdmin(account1, { from: account1 }), + "unauthorized" + ); + }); + }); + + describe("setCSOVtokens", () => { + it("sets the expected values", async () => { + await vestingRegistry.setCSOVtokens([account2, account3]); + + let _CSOV1 = await vestingRegistry.CSOVtokens(0); + let _CSOV2 = await vestingRegistry.CSOVtokens(1); + + expect(_CSOV1).equal(account2); + expect(_CSOV2).equal(account3); + }); + + it("fails if the 0 address is passed as cSOV address", async () => { + await expectRevert( + vestingRegistry.setCSOVtokens([cSOV1.address, cSOV2.address, ZERO_ADDRESS]), + "CSOV address invalid" + ); + }); + + it("fails if sender isn't an owner", async () => { + await expectRevert( + vestingRegistry.setCSOVtokens([cSOV1.address, cSOV2.address], { from: account2 }), + "unauthorized" + ); + }); + }); + + describe("transferSOV", () => { + it("should be able to transfer SOV", async () => { + let amount = new BN(1000); + await SOV.transfer(vestingRegistry.address, amount); + + let balanceBefore = await SOV.balanceOf(account1); + await vestingRegistry.transferSOV(account1, amount); + let balanceAfter = await SOV.balanceOf(account1); + + expect(amount).to.be.bignumber.equal(balanceAfter.sub(balanceBefore)); + }); + + it("only owner should be able to transfer", async () => { + await expectRevert( + vestingRegistry.transferSOV(account1, 1000, { from: account1 }), + "unauthorized" + ); + }); + + it("fails if the 0 address is passed as receiver address", async () => { + await expectRevert( + vestingRegistry.transferSOV(ZERO_ADDRESS, 1000), + "receiver address invalid" + ); + }); + + it("fails if the 0 is passed as an amount", async () => { + await expectRevert(vestingRegistry.transferSOV(account1, 0), "amount invalid"); + }); + }); + + describe("setBlacklistFlag", () => { + it("should be able to add user to blacklist", async () => { + await vestingRegistry.setBlacklistFlag(account2, true); + + let blacklisted = await vestingRegistry.blacklist(account2); + expect(blacklisted).equal(true); + }); + + it("fails if the 0 address is passed", async () => { + await expectRevert( + vestingRegistry.setBlacklistFlag(ZERO_ADDRESS, true), + "account address invalid" + ); + }); + }); + + describe("setLockedAmount", () => { + it("should be able to set locked amount", async () => { + let amount = new BN(123); + await vestingRegistry.setLockedAmount(account2, amount); + + let lockedAmount = await vestingRegistry.lockedAmount(account2); + expect(lockedAmount).to.be.bignumber.equal(amount); + }); + + it("fails if the 0 address is passed", async () => { + await expectRevert( + vestingRegistry.setLockedAmount(ZERO_ADDRESS, 111), + "account address invalid" + ); + }); + + it("fails if the 0 amount is passed", async () => { + await expectRevert(vestingRegistry.setLockedAmount(account2, 0), "amount invalid"); + }); + }); + + describe("createVesting", () => { + it("should be able to create vesting", async () => { + /// @dev This test requires a hard reset of init fixture + await deploymentAndInitFixture(); + + let amount = new BN(1000000); + await SOV.transfer(vestingRegistry.address, amount); + + let cliff = FOUR_WEEKS; + let duration = FOUR_WEEKS.mul(new BN(20)); + let tx = await vestingRegistry.createVesting(account2, amount, cliff, duration); + let vestingAddress = await vestingRegistry.getVesting(account2); + await vestingRegistry.stakeTokens(vestingAddress, amount); + + expectEvent(tx, "VestingCreated", { + tokenOwner: account2, + vesting: vestingAddress, + cliff: cliff, + duration: duration, + amount: amount, + }); + + let balance = await SOV.balanceOf(vestingRegistry.address); + expect(balance.toString()).equal("0"); + + let vesting = await VestingLogic.at(vestingAddress); + await checkVesting(vesting, account2, cliff, duration, amount); + + await expectRevert( + vesting.governanceWithdrawTokens(account2), + "operation not supported" + ); + + let proxy = await UpgradableProxy.at(vestingAddress); + await expectRevert(proxy.setImplementation(account2), "revert"); + }); + + it("fails if vestingRegistry doesn't have enough SOV", async () => { + let amount = new BN(1000000); + let cliff = FOUR_WEEKS; + let duration = FOUR_WEEKS.mul(new BN(20)); + + await vestingRegistry.createVesting(account2, amount, cliff, duration); + let vestingAddress = await vestingRegistry.getVesting(account2); + + await expectRevert( + vestingRegistry.stakeTokens(vestingAddress, amount), + "ERC20: transfer amount exceeds balance" + ); + }); + + it("fails if sender is not an owner or admin", async () => { + let amount = new BN(1000000); + let cliff = TEAM_VESTING_CLIFF; + let duration = TEAM_VESTING_DURATION; + + await expectRevert( + vestingRegistry.createVesting(account2, amount, cliff, duration, { + from: account1, + }), + "unauthorized" + ); + + await vestingRegistry.addAdmin(account1); + await vestingRegistry.createVesting(account2, amount, cliff, duration, { + from: account1, + }); + }); + }); + + describe("createTeamVesting", () => { + it("should be able to create vesting", async () => { + /// @dev This test requires a hard reset of init fixture + await deploymentAndInitFixture(); + + let amount = new BN(1000000); + await SOV.transfer(vestingRegistry.address, amount); + + let cliff = TEAM_VESTING_CLIFF; + let duration = TEAM_VESTING_DURATION; + let tx = await vestingRegistry.createTeamVesting(account2, amount, cliff, duration); + let vestingAddress = await vestingRegistry.getTeamVesting(account2); + let tx2 = await vestingRegistry.stakeTokens(vestingAddress, amount); + + console.log("\ngasUsed = " + tx.receipt.gasUsed); + console.log("gasUsed = " + tx2.receipt.gasUsed); + + expectEvent(tx, "TeamVestingCreated", { + tokenOwner: account2, + vesting: vestingAddress, + cliff: cliff, + duration: duration, + amount: amount, + }); + + let balance = await SOV.balanceOf(vestingRegistry.address); + expect(balance.toString()).equal("0"); + + let vesting = await VestingLogic.at(vestingAddress); + await checkVesting(vesting, account2, cliff, duration, amount); + + await expectRevert(vesting.governanceWithdrawTokens(account2), "unauthorized"); + + let proxy = await UpgradableProxy.at(vestingAddress); + await expectRevert(proxy.setImplementation(account2), "revert"); + }); + + it("fails if vestingRegistry doesn't have enough SOV", async () => { + let amount = new BN(1000000); + let cliff = TEAM_VESTING_CLIFF; + let duration = TEAM_VESTING_DURATION; + + await vestingRegistry.createTeamVesting(account2, amount, cliff, duration); + let vestingAddress = await vestingRegistry.getTeamVesting(account2); + + await expectRevert( + vestingRegistry.stakeTokens(vestingAddress, amount), + "ERC20: transfer amount exceeds balance" + ); + }); + + it("fails if sender is not an owner or admin", async () => { + let amount = new BN(1000000); + let cliff = TEAM_VESTING_CLIFF; + let duration = TEAM_VESTING_DURATION; + + await expectRevert( + vestingRegistry.createTeamVesting(account2, amount, cliff, duration, { + from: account1, + }), + "unauthorized" + ); + + await vestingRegistry.addAdmin(account1); + await vestingRegistry.createTeamVesting(account2, amount, cliff, duration, { + from: account1, + }); + }); + }); + + describe("stakeTokens", () => { + it("fails if the 0 address is passed as vesting address", async () => { + await expectRevert( + vestingRegistry.stakeTokens(ZERO_ADDRESS, new BN(1000000)), + "vesting address invalid" + ); + }); + + it("fails if the 0 address is passed as an amount", async () => { + await expectRevert(vestingRegistry.stakeTokens(account1, 0), "amount invalid"); + }); + + it("only owner or admin should be able to stake tokens", async () => { + /// @dev This test requires a hard reset of init fixture + await deploymentAndInitFixture(); + + let amount = new BN(1000000); + await SOV.transfer(vestingRegistry.address, amount); + + let cliff = TEAM_VESTING_CLIFF; + let duration = TEAM_VESTING_DURATION; + await vestingRegistry.createTeamVesting(account2, amount, cliff, duration); + let vestingAddress = await vestingRegistry.getTeamVesting(account2); + + await expectRevert( + vestingRegistry.stakeTokens(vestingAddress, new BN(1000000), { from: account1 }), + "unauthorized" + ); + + await vestingRegistry.addAdmin(account1); + await vestingRegistry.stakeTokens(vestingAddress, new BN(1000000), { from: account1 }); + }); + }); + + describe("deposit funds", () => { + it("should get budget and deposit", async () => { + await deploymentAndInitFixture(); + + // Check budget is 0 + let budget = await vestingRegistry.budget(); + // console.log("budget: " + budget); + expect(budget).to.be.bignumber.equal(new BN(0)); + + // Deposit funds + const amount = web3.utils.toWei("3"); + await vestingRegistry.deposit({ from: accounts[1], value: amount }); + + // Check budget is not 0 + budget = await vestingRegistry.budget(); + // console.log("budget: " + budget); + expect(budget).to.be.bignumber.equal(amount); + + // Get recipient's balance before withdrawal + let balance2before = await provider.getBalance(accounts[2]); + // console.log("balance2before: " + balance2before); + + // Check budget after withdrawal + await vestingRegistry.withdrawAll(accounts[2]); + budget = await vestingRegistry.budget(); + // console.log("budget: " + budget); + expect(budget).to.be.bignumber.equal(new BN(0)); + + // Get recipient's balance after withdrawal + let balance2after = await provider.getBalance(accounts[2]); + // console.log("balance2after: " + balance2after); + expect(balance2after.sub(balance2before)).to.be.equal(amount); + }); + }); + + async function checkVesting(vesting, account, cliff, duration, amount) { + await mineBlock(); + + let vestingBalance = await staking.balanceOf(vesting.address); + expect(vestingBalance).to.be.bignumber.equal(amount); + + let accountVotes = await staking.getCurrentVotes(account); + expect(accountVotes).to.be.not.equal(new BN(0)); + let vestingVotes = await staking.getCurrentVotes(vesting.address); + expect(vestingVotes).to.be.bignumber.equal(new BN(0)); + + let startDate = await vesting.startDate(); + let start = startDate.toNumber() + cliff.toNumber(); + let end = startDate.toNumber() + duration.toNumber(); + + let numIntervals = Math.floor((end - start) / FOUR_WEEKS) + 1; + let stakedPerInterval = Math.floor(amount / numIntervals); + + let stakeForFirstInterval = amount - stakedPerInterval * (numIntervals - 1); + + expect(await vesting.cliff()).to.be.bignumber.equal(cliff); + expect(await vesting.duration()).to.be.bignumber.equal(duration); + + for (let i = start; i <= end; i += FOUR_WEEKS) { + let lockedTS = await staking.timestampToLockDate(i); + + let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints( + vesting.address, + lockedTS + ); + let userStakingCheckpoints = await staking.userStakingCheckpoints( + vesting.address, + lockedTS, + numUserStakingCheckpoints - 1 + ); + assert.equal(numUserStakingCheckpoints.toString(), "1"); + if (i === start) { + assert.equal(userStakingCheckpoints.stake.toString(), stakeForFirstInterval); + } else { + assert.equal(userStakingCheckpoints.stake.toString(), stakedPerInterval); + } + + let numDelegateStakingCheckpoints = await staking.numDelegateStakingCheckpoints( + account, + lockedTS + ); + let delegateStakingCheckpoints = await staking.delegateStakingCheckpoints( + account, + lockedTS, + numUserStakingCheckpoints - 1 + ); + assert.equal(numDelegateStakingCheckpoints.toString(), "1"); + if (i === start) { + assert.equal(delegateStakingCheckpoints.stake.toString(), stakeForFirstInterval); + } else { + assert.equal(delegateStakingCheckpoints.stake.toString(), stakedPerInterval); + } + } + } }); diff --git a/tests/vesting/VestingRegistryLogic.js b/tests/vesting/VestingRegistryLogic.js index 497778381..b76a996be 100644 --- a/tests/vesting/VestingRegistryLogic.js +++ b/tests/vesting/VestingRegistryLogic.js @@ -26,754 +26,883 @@ const ZERO_ADDRESS = constants.ZERO_ADDRESS; const pricsSats = "2500"; contract("VestingRegistryLogic", (accounts) => { - let root, account1, account2, account3, account4; - let SOV, lockedSOV; - let staking, stakingLogic, feeSharingProxy; - let vesting, vestingFactory, vestingLogic, vestingRegistryLogic; - let vestingRegistry, vestingRegistry2, vestingRegistry3; - - let cliff = 1; // This is in 4 weeks. i.e. 1 * 4 weeks. - let duration = 11; // This is in 4 weeks. i.e. 11 * 4 weeks. - - before(async () => { - [root, account1, account2, account3, accounts4, ...accounts] = accounts; - }); - - beforeEach(async () => { - SOV = await SOV_ABI.new(TOTAL_SUPPLY); - cSOV1 = await TestToken.new("cSOV1", "cSOV1", 18, TOTAL_SUPPLY); - cSOV2 = await TestToken.new("cSOV2", "cSOV2", 18, TOTAL_SUPPLY); - - stakingLogic = await StakingLogic.new(); - staking = await StakingProxy.new(SOV.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingLogic.at(staking.address); - - feeSharingProxy = await FeeSharingProxy.new(ZERO_ADDRESS, staking.address); - - vestingLogic = await VestingLogic.new(); - vestingFactory = await VestingFactory.new(vestingLogic.address); - - vestingRegistryLogic = await VestingRegistryLogic.new(); - vesting = await VestingRegistryProxy.new(); - await vesting.setImplementation(vestingRegistryLogic.address); - vesting = await VestingRegistryLogic.at(vesting.address); - vestingFactory.transferOwnership(vesting.address); - - lockedSOV = await LockedSOV.new(SOV.address, vesting.address, cliff, duration, [root]); - await vesting.addAdmin(lockedSOV.address); - - vestingRegistry = await VestingRegistry.new( - vestingFactory.address, - SOV.address, - [cSOV1.address, cSOV2.address], - pricsSats, - staking.address, - feeSharingProxy.address, - account1 - ); - - vestingRegistry2 = await VestingRegistry2.new( - vestingFactory.address, - SOV.address, - [cSOV1.address, cSOV2.address], - pricsSats, - staking.address, - feeSharingProxy.address, - account1 - ); - - vestingRegistry3 = await VestingRegistry3.new( - vestingFactory.address, - SOV.address, - staking.address, - feeSharingProxy.address, - account1 - ); - }); - - describe("initialize", () => { - it("fails if the 0 address is passed as vestingFactory address", async () => { - await expectRevert( - vesting.initialize(ZERO_ADDRESS, SOV.address, staking.address, feeSharingProxy.address, account1, lockedSOV.address, [ - vestingRegistry.address, - vestingRegistry2.address, - vestingRegistry3.address, - ]), - "vestingFactory address invalid" - ); - }); - - it("fails if the 0 address is passed as SOV address", async () => { - await expectRevert( - vesting.initialize( - vestingFactory.address, - ZERO_ADDRESS, - staking.address, - feeSharingProxy.address, - account1, - lockedSOV.address, - [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] - ), - "SOV address invalid" - ); - }); - - it("fails if the 0 address is passed as staking address", async () => { - await expectRevert( - vesting.initialize( - vestingFactory.address, - SOV.address, - ZERO_ADDRESS, - feeSharingProxy.address, - account1, - lockedSOV.address, - [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] - ), - "staking address invalid" - ); - }); - - it("fails if the 0 address is passed as feeSharingProxy address", async () => { - await expectRevert( - vesting.initialize(vestingFactory.address, SOV.address, staking.address, ZERO_ADDRESS, account1, lockedSOV.address, [ - vestingRegistry.address, - vestingRegistry2.address, - vestingRegistry3.address, - ]), - "feeSharingProxy address invalid" - ); - }); - - it("fails if the 0 address is passed as vestingOwner address", async () => { - await expectRevert( - vesting.initialize( - vestingFactory.address, - SOV.address, - staking.address, - feeSharingProxy.address, - ZERO_ADDRESS, - lockedSOV.address, - [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] - ), - "vestingOwner address invalid" - ); - }); - - it("fails if the 0 address is passed as LockedSOV address", async () => { - await expectRevert( - vesting.initialize(vestingFactory.address, SOV.address, staking.address, feeSharingProxy.address, account1, ZERO_ADDRESS, [ - vestingRegistry.address, - vestingRegistry2.address, - vestingRegistry3.address, - ]), - "LockedSOV address invalid" - ); - }); - - it("fails if the 0 address is passed as VestingRegistry address", async () => { - await expectRevert( - vesting.initialize( - vestingFactory.address, - SOV.address, - staking.address, - feeSharingProxy.address, - account1, - lockedSOV.address, - [ZERO_ADDRESS, vestingRegistry2.address, vestingRegistry3.address] - ), - "Vesting registry address invalid" - ); - }); - - it("fails if the 0 address is passed as VestingRegistry2 address", async () => { - await expectRevert( - vesting.initialize( - vestingFactory.address, - SOV.address, - staking.address, - feeSharingProxy.address, - account1, - lockedSOV.address, - [vestingRegistry.address, ZERO_ADDRESS, vestingRegistry3.address] - ), - "Vesting registry address invalid" - ); - }); - - it("fails if the 0 address is passed as VestingRegistry3 address", async () => { - await expectRevert( - vesting.initialize( - vestingFactory.address, - SOV.address, - staking.address, - feeSharingProxy.address, - account1, - lockedSOV.address, - [vestingRegistry.address, vestingRegistry2.address, ZERO_ADDRESS] - ), - "Vesting registry address invalid" - ); - }); - - it("sets the expected values", async () => { - await vesting.initialize( - vestingFactory.address, - SOV.address, - staking.address, - feeSharingProxy.address, - account1, - lockedSOV.address, - [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] - ); - - let _sov = await vesting.SOV(); - let _staking = await vesting.staking(); - let _feeSharingProxy = await vesting.feeSharingProxy(); - let _vestingOwner = await vesting.vestingOwner(); - - expect(_sov).equal(SOV.address); - expect(_staking).equal(staking.address); - expect(_feeSharingProxy).equal(feeSharingProxy.address); - expect(_vestingOwner).equal(account1); - }); - - it("fails if initialize is called twice", async () => { - await vesting.initialize( - vestingFactory.address, - SOV.address, - staking.address, - feeSharingProxy.address, - account1, - lockedSOV.address, - [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] - ); - await expectRevert( - vesting.initialize( - vestingFactory.address, - SOV.address, - staking.address, - feeSharingProxy.address, - account1, - lockedSOV.address, - [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] - ), - "contract is already initialized" - ); - }); - }); - - describe("setVestingFactory", () => { - it("sets vesting factory", async () => { - await vesting.setVestingFactory(account2); - - let vestingFactory = await vesting.vestingFactory(); - expect(vestingFactory).equal(account2); - }); - - it("fails if the 0 address is passed", async () => { - await expectRevert(vesting.setVestingFactory(ZERO_ADDRESS), "vestingFactory address invalid"); - }); - - it("fails if sender isn't an owner", async () => { - await expectRevert(vesting.setVestingFactory(account2, { from: account2 }), "unauthorized"); - }); - }); - - describe("addAdmin", () => { - it("adds admin", async () => { - let tx = await vesting.addAdmin(account1); - - expectEvent(tx, "AdminAdded", { - admin: account1, - }); - - let isAdmin = await vesting.admins(account1); - expect(isAdmin).equal(true); - }); - - it("fails sender isn't an owner", async () => { - await expectRevert(vesting.addAdmin(account1, { from: account1 }), "unauthorized"); - }); - }); - - describe("removeAdmin", () => { - it("removes admin", async () => { - await vesting.addAdmin(account1); - let tx = await vesting.removeAdmin(account1); - - expectEvent(tx, "AdminRemoved", { - admin: account1, - }); - - let isAdmin = await vesting.admins(account1); - expect(isAdmin).equal(false); - }); - - it("fails sender isn't an owner", async () => { - await expectRevert(vesting.removeAdmin(account1, { from: account1 }), "unauthorized"); - }); - }); - - describe("transferSOV", () => { - it("should be able to transfer SOV", async () => { - await vesting.initialize( - vestingFactory.address, - SOV.address, - staking.address, - feeSharingProxy.address, - account1, - lockedSOV.address, - [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] - ); - - let amount = new BN(1000); - await SOV.transfer(vesting.address, amount); - let balanceBefore = await SOV.balanceOf(account1); - let tx = await vesting.transferSOV(account1, amount); - expectEvent(tx, "SOVTransferred", { - receiver: account1, - amount: amount, - }); - let balanceAfter = await SOV.balanceOf(account1); - - expect(amount).to.be.bignumber.equal(balanceAfter.sub(balanceBefore)); - }); - - it("only owner should be able to transfer", async () => { - await expectRevert(vesting.transferSOV(account1, 1000, { from: account1 }), "unauthorized"); - }); - - it("fails if the 0 address is passed as receiver address", async () => { - await expectRevert(vesting.transferSOV(ZERO_ADDRESS, 1000), "receiver address invalid"); - }); - - it("fails if the 0 is passed as an amount", async () => { - await expectRevert(vesting.transferSOV(account1, 0), "amount invalid"); - }); - }); - - describe("createVesting", () => { - it("should be able to create vesting - Bug Bounty", async () => { - await vesting.initialize( - vestingFactory.address, - SOV.address, - staking.address, - feeSharingProxy.address, - account1, - lockedSOV.address, - [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] - ); - - let amount = new BN(1000000); - await SOV.transfer(vesting.address, amount); - - let cliff = FOUR_WEEKS; - let duration = FOUR_WEEKS.mul(new BN(20)); - let vestingType = new BN(2); //Bug Bounty - let tx = await vesting.createVestingAddr(account2, amount, cliff, duration, vestingType); - let vestingAddress = await vesting.getVestingAddr(account2, cliff, duration, vestingType); - expect(await vesting.isVestingAdress(vestingAddress)).equal(true); - await vesting.stakeTokens(vestingAddress, amount); - - expectEvent(tx, "VestingCreated", { - tokenOwner: account2, - vesting: vestingAddress, - cliff: cliff, - duration: duration, - amount: amount, - vestingCreationType: vestingType, - }); - - let balance = await SOV.balanceOf(vesting.address); - expect(balance.toString()).equal("0"); - - let vestingAddr = await VestingLogic.at(vestingAddress); - await checkVesting(vestingAddr, account2, cliff, duration, amount); - - await expectRevert(vestingAddr.governanceWithdrawTokens(account2), "operation not supported"); - - let proxy = await UpgradableProxy.at(vestingAddress); - await expectRevert(proxy.setImplementation(account2), "revert"); - }); - - it("should be able to create vesting - Team Salary", async () => { - await vesting.initialize( - vestingFactory.address, - SOV.address, - staking.address, - feeSharingProxy.address, - account1, - lockedSOV.address, - [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] - ); - - let amount = new BN(1000000); - await SOV.transfer(vesting.address, amount); - - let cliff = FOUR_WEEKS; - let duration = FOUR_WEEKS.mul(new BN(20)); - let vestingType = new BN(3); //Team Salary - let tx = await vesting.createVestingAddr(account2, amount, cliff, duration, vestingType); - let vestingAddress = await vesting.getVestingAddr(account2, cliff, duration, vestingType); - expect(await vesting.isVestingAdress(vestingAddress)).equal(true); - await vesting.stakeTokens(vestingAddress, amount); - - expectEvent(tx, "VestingCreated", { - tokenOwner: account2, - vesting: vestingAddress, - cliff: cliff, - duration: duration, - amount: amount, - vestingCreationType: vestingType, - }); - - let balance = await SOV.balanceOf(vesting.address); - expect(balance.toString()).equal("0"); - - let vestingAddr = await VestingLogic.at(vestingAddress); - await checkVesting(vestingAddr, account2, cliff, duration, amount); - - await expectRevert(vestingAddr.governanceWithdrawTokens(account2), "operation not supported"); - - let proxy = await UpgradableProxy.at(vestingAddress); - await expectRevert(proxy.setImplementation(account2), "revert"); - }); - - it("fails if vestingRegistryLogic doesn't have enough SOV", async () => { - await vesting.initialize( - vestingFactory.address, - SOV.address, - staking.address, - feeSharingProxy.address, - account1, - lockedSOV.address, - [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] - ); - - let amount = new BN(1000000); - let cliff = FOUR_WEEKS; - let duration = FOUR_WEEKS.mul(new BN(20)); - let vestingType = new BN(3); //Team Salary - - await vesting.createVestingAddr(account2, amount, cliff, duration, vestingType); - let vestingAddress = await vesting.getVestingAddr(account2, cliff, duration, vestingType); - - await expectRevert(vesting.stakeTokens(vestingAddress, amount), "ERC20: transfer amount exceeds balance"); - }); - - it("fails if sender is not an owner or admin", async () => { - await vesting.initialize( - vestingFactory.address, - SOV.address, - staking.address, - feeSharingProxy.address, - account1, - lockedSOV.address, - [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] - ); - - let amount = new BN(1000000); - let cliff = TEAM_VESTING_CLIFF; - let duration = TEAM_VESTING_DURATION; - let vestingType = new BN(3); //Team Salary - - await expectRevert( - vesting.createVestingAddr(account2, amount, cliff, duration, vestingType, { from: account1 }), - "unauthorized" - ); - - await vesting.addAdmin(account1); - await vesting.createVestingAddr(account2, amount, cliff, duration, vestingType, { from: account1 }); - }); - }); - - describe("createVesting and getVesting - LockedSOV", () => { - it("Should create vesting and return the address for LockedSOV", async () => { - await vesting.initialize( - vestingFactory.address, - SOV.address, - staking.address, - feeSharingProxy.address, - account1, - lockedSOV.address, - [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] - ); - - let amount = new BN(1000000); - await SOV.transfer(vesting.address, amount); - await lockedSOV.createVesting({ from: accounts4 }); - let vestingAddr = await vesting.getVesting(accounts4); - expect(await vesting.isVestingAdress(vestingAddr)).equal(true); - assert.notEqual(vestingAddr, ZERO_ADDRESS, "Vesting Address should not be zero."); - }); - }); - - describe("createTeamVesting", () => { - it("should be able to create team vesting", async () => { - await vesting.initialize( - vestingFactory.address, - SOV.address, - staking.address, - feeSharingProxy.address, - account1, - lockedSOV.address, - [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] - ); - - let amount = new BN(1000000); - await SOV.transfer(vesting.address, amount); - - let cliff = TEAM_VESTING_CLIFF; - let duration = TEAM_VESTING_DURATION; - let vestingType = new BN(3); //Team Salary - let tx = await vesting.createTeamVesting(account2, amount, cliff, duration, vestingType); - let vestingAddress = await vesting.getTeamVesting(account2, cliff, duration, vestingType); - expect(await vesting.isVestingAdress(vestingAddress)).equal(true); - expectEvent(tx, "TeamVestingCreated", { - tokenOwner: account2, - vesting: vestingAddress, - cliff: cliff, - duration: duration, - amount: amount, - vestingCreationType: vestingType, - }); - let tx2 = await vesting.stakeTokens(vestingAddress, amount); - expectEvent(tx2, "TokensStaked", { - vesting: vestingAddress, - amount: amount, - }); - let balance = await SOV.balanceOf(vestingRegistryLogic.address); - expect(balance.toString()).equal("0"); - - let vestingAddr = await VestingLogic.at(vestingAddress); - await checkVesting(vestingAddr, account2, cliff, duration, amount); - - await expectRevert(vestingAddr.governanceWithdrawTokens(account2), "unauthorized"); - - let proxy = await UpgradableProxy.at(vestingAddress); - await expectRevert(proxy.setImplementation(account2), "revert"); - }); - - it("fails if vestingRegistryLogic doesn't have enough SOV", async () => { - await vesting.initialize( - vestingFactory.address, - SOV.address, - staking.address, - feeSharingProxy.address, - account1, - lockedSOV.address, - [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] - ); - - let amount = new BN(1000000); - let cliff = TEAM_VESTING_CLIFF; - let duration = TEAM_VESTING_DURATION; - let vestingType = new BN(3); //Team Salary - - await vesting.createTeamVesting(account2, amount, cliff, duration, vestingType); - let vestingAddress = await vesting.getTeamVesting(account2, cliff, duration, vestingType); - - await expectRevert(vesting.stakeTokens(vestingAddress, amount), "ERC20: transfer amount exceeds balance"); - }); - - it("fails if sender is not an owner or admin", async () => { - await vesting.initialize( - vestingFactory.address, - SOV.address, - staking.address, - feeSharingProxy.address, - account1, - lockedSOV.address, - [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] - ); - - let amount = new BN(1000000); - let cliff = TEAM_VESTING_CLIFF; - let duration = TEAM_VESTING_DURATION; - let vestingType = new BN(3); //Team Salary - - await expectRevert( - vesting.createTeamVesting(account2, amount, cliff, duration, vestingType, { from: account1 }), - "unauthorized" - ); - - await vesting.addAdmin(account1); - await vesting.createTeamVesting(account2, amount, cliff, duration, vestingType, { from: account1 }); - }); - }); - - describe("stakeTokens", () => { - it("fails if the 0 address is passed as vesting address", async () => { - await expectRevert(vesting.stakeTokens(ZERO_ADDRESS, new BN(1000000)), "vesting address invalid"); - }); - - it("fails if the 0 address is passed as an amount", async () => { - await expectRevert(vesting.stakeTokens(account1, 0), "amount invalid"); - }); - - it("only owner or admin should be able to stake tokens", async () => { - await vesting.initialize( - vestingFactory.address, - SOV.address, - staking.address, - feeSharingProxy.address, - account1, - lockedSOV.address, - [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] - ); - - let amount = new BN(1000000); - await SOV.transfer(vesting.address, amount); - - let cliff = TEAM_VESTING_CLIFF; - let duration = TEAM_VESTING_DURATION; - let vestingType = new BN(3); //Team Salary - await vesting.createTeamVesting(account2, amount, cliff, duration, vestingType); - let vestingAddress = await vesting.getTeamVesting(account2, cliff, duration, vestingType); - - await expectRevert(vesting.stakeTokens(vestingAddress, new BN(1000000), { from: account1 }), "unauthorized"); - - await vesting.addAdmin(account1); - await vesting.stakeTokens(vestingAddress, new BN(1000000), { from: account1 }); - }); - }); - - describe("getVestingsOf", () => { - it("gets vesting of a user", async () => { - await vesting.initialize( - vestingFactory.address, - SOV.address, - staking.address, - feeSharingProxy.address, - account1, - lockedSOV.address, - [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] - ); - - let amount = new BN(1000000); - await SOV.transfer(vesting.address, amount); - - //Vesting - let cliff = FOUR_WEEKS; - let duration = FOUR_WEEKS.mul(new BN(20)); - let vestingType = new BN(2); //Bug Bounty - await vesting.createVestingAddr(account2, amount, cliff, duration, vestingType); - - //TeamVesting - let teamCliff = TEAM_VESTING_CLIFF; - let teamDuration = TEAM_VESTING_DURATION; - vestingType = new BN(3); //Team Salary - await vesting.createTeamVesting(account2, amount, teamCliff, teamDuration, vestingType); - - let vestingAddresses = await vesting.getVestingsOf(account2); - assert.equal(vestingAddresses.length.toString(), "2"); - assert.equal(vestingAddresses[0].vestingCreationType, "2"); - assert.equal(vestingAddresses[1].vestingCreationType, "3"); - }); - }); - - describe("getVestingDetails", () => { - it("gets cliff, duration and amount for vesting address", async () => { - await vesting.initialize( - vestingFactory.address, - SOV.address, - staking.address, - feeSharingProxy.address, - account1, - lockedSOV.address, - [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] - ); - - let amount = new BN(1000000); - await SOV.transfer(vesting.address, amount); - - //Vesting - let cliff = FOUR_WEEKS; - let duration = FOUR_WEEKS.mul(new BN(20)); - let vestingType = new BN(2); //Bug Bounty - await vesting.createVestingAddr(account2, amount, cliff, duration, vestingType); - let vestingAddr = await vesting.getVestingAddr(account2, cliff, duration, vestingType); - let fields = await vesting.getVestingDetails(vestingAddr); - expect(cliff).to.be.bignumber.equal(fields.cliff); - expect(duration).to.be.bignumber.equal(fields.duration); - }); - - it("gets cliff, duration and amount for team vesting address", async () => { - await vesting.initialize( - vestingFactory.address, - SOV.address, - staking.address, - feeSharingProxy.address, - account1, - lockedSOV.address, - [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] - ); - - let amount = new BN(1000000); - await SOV.transfer(vesting.address, amount); - - //TeamVesting - let teamCliff = TEAM_VESTING_CLIFF; - let teamDuration = TEAM_VESTING_DURATION; - let vestingType = new BN(3); //Team Salary - await vesting.createTeamVesting(account2, amount, teamCliff, teamDuration, vestingType); - let vestingAddr = await vesting.getTeamVesting(account2, teamCliff, teamDuration, vestingType); - let fields = await vesting.getVestingDetails(vestingAddr); - expect(teamCliff).to.be.bignumber.equal(fields.cliff); - expect(teamDuration).to.be.bignumber.equal(fields.duration); - }); - }); - - describe("isVestingAdress", () => { - it("should return false if the address isn't a vesting address", async () => { - expect(await vesting.isVestingAdress(account1)).equal(false); - }); - }); - - async function checkVesting(vesting, account, cliff, duration, amount) { - await mineBlock(); - - let vestingBalance = await staking.balanceOf(vesting.address); - expect(vestingBalance).to.be.bignumber.equal(amount); - - let accountVotes = await staking.getCurrentVotes(account); - expect(accountVotes).to.be.not.equal(new BN(0)); - let vestingVotes = await staking.getCurrentVotes(vesting.address); - expect(vestingVotes).to.be.bignumber.equal(new BN(0)); - - let startDate = await vesting.startDate(); - let start = startDate.toNumber() + cliff.toNumber(); - let end = startDate.toNumber() + duration.toNumber(); - - let numIntervals = Math.floor((end - start) / FOUR_WEEKS) + 1; - let stakedPerInterval = Math.floor(amount / numIntervals); - - let stakeForFirstInterval = amount - stakedPerInterval * (numIntervals - 1); - - expect(await vesting.cliff()).to.be.bignumber.equal(cliff); - expect(await vesting.duration()).to.be.bignumber.equal(duration); - - for (let i = start; i <= end; i += FOUR_WEEKS) { - let lockedTS = await staking.timestampToLockDate(i); - - let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints(vesting.address, lockedTS); - let userStakingCheckpoints = await staking.userStakingCheckpoints(vesting.address, lockedTS, numUserStakingCheckpoints - 1); - assert.equal(numUserStakingCheckpoints.toString(), "1"); - if (i === start) { - assert.equal(userStakingCheckpoints.stake.toString(), stakeForFirstInterval); - } else { - assert.equal(userStakingCheckpoints.stake.toString(), stakedPerInterval); - } - - let numDelegateStakingCheckpoints = await staking.numDelegateStakingCheckpoints(account, lockedTS); - let delegateStakingCheckpoints = await staking.delegateStakingCheckpoints(account, lockedTS, numUserStakingCheckpoints - 1); - assert.equal(numDelegateStakingCheckpoints.toString(), "1"); - if (i === start) { - assert.equal(delegateStakingCheckpoints.stake.toString(), stakeForFirstInterval); - } else { - assert.equal(delegateStakingCheckpoints.stake.toString(), stakedPerInterval); - } - } - } + let root, account1, account2, account3, account4; + let SOV, lockedSOV; + let staking, stakingLogic, feeSharingProxy; + let vesting, vestingFactory, vestingLogic, vestingRegistryLogic; + let vestingRegistry, vestingRegistry2, vestingRegistry3; + + let cliff = 1; // This is in 4 weeks. i.e. 1 * 4 weeks. + let duration = 11; // This is in 4 weeks. i.e. 11 * 4 weeks. + + before(async () => { + [root, account1, account2, account3, accounts4, ...accounts] = accounts; + }); + + beforeEach(async () => { + SOV = await SOV_ABI.new(TOTAL_SUPPLY); + cSOV1 = await TestToken.new("cSOV1", "cSOV1", 18, TOTAL_SUPPLY); + cSOV2 = await TestToken.new("cSOV2", "cSOV2", 18, TOTAL_SUPPLY); + + stakingLogic = await StakingLogic.new(); + staking = await StakingProxy.new(SOV.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); + + feeSharingProxy = await FeeSharingProxy.new(ZERO_ADDRESS, staking.address); + + vestingLogic = await VestingLogic.new(); + vestingFactory = await VestingFactory.new(vestingLogic.address); + + vestingRegistryLogic = await VestingRegistryLogic.new(); + vesting = await VestingRegistryProxy.new(); + await vesting.setImplementation(vestingRegistryLogic.address); + vesting = await VestingRegistryLogic.at(vesting.address); + vestingFactory.transferOwnership(vesting.address); + + lockedSOV = await LockedSOV.new(SOV.address, vesting.address, cliff, duration, [root]); + await vesting.addAdmin(lockedSOV.address); + + vestingRegistry = await VestingRegistry.new( + vestingFactory.address, + SOV.address, + [cSOV1.address, cSOV2.address], + pricsSats, + staking.address, + feeSharingProxy.address, + account1 + ); + + vestingRegistry2 = await VestingRegistry2.new( + vestingFactory.address, + SOV.address, + [cSOV1.address, cSOV2.address], + pricsSats, + staking.address, + feeSharingProxy.address, + account1 + ); + + vestingRegistry3 = await VestingRegistry3.new( + vestingFactory.address, + SOV.address, + staking.address, + feeSharingProxy.address, + account1 + ); + }); + + describe("initialize", () => { + it("fails if the 0 address is passed as vestingFactory address", async () => { + await expectRevert( + vesting.initialize( + ZERO_ADDRESS, + SOV.address, + staking.address, + feeSharingProxy.address, + account1, + lockedSOV.address, + [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] + ), + "vestingFactory address invalid" + ); + }); + + it("fails if the 0 address is passed as SOV address", async () => { + await expectRevert( + vesting.initialize( + vestingFactory.address, + ZERO_ADDRESS, + staking.address, + feeSharingProxy.address, + account1, + lockedSOV.address, + [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] + ), + "SOV address invalid" + ); + }); + + it("fails if the 0 address is passed as staking address", async () => { + await expectRevert( + vesting.initialize( + vestingFactory.address, + SOV.address, + ZERO_ADDRESS, + feeSharingProxy.address, + account1, + lockedSOV.address, + [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] + ), + "staking address invalid" + ); + }); + + it("fails if the 0 address is passed as feeSharingProxy address", async () => { + await expectRevert( + vesting.initialize( + vestingFactory.address, + SOV.address, + staking.address, + ZERO_ADDRESS, + account1, + lockedSOV.address, + [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] + ), + "feeSharingProxy address invalid" + ); + }); + + it("fails if the 0 address is passed as vestingOwner address", async () => { + await expectRevert( + vesting.initialize( + vestingFactory.address, + SOV.address, + staking.address, + feeSharingProxy.address, + ZERO_ADDRESS, + lockedSOV.address, + [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] + ), + "vestingOwner address invalid" + ); + }); + + it("fails if the 0 address is passed as LockedSOV address", async () => { + await expectRevert( + vesting.initialize( + vestingFactory.address, + SOV.address, + staking.address, + feeSharingProxy.address, + account1, + ZERO_ADDRESS, + [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] + ), + "LockedSOV address invalid" + ); + }); + + it("fails if the 0 address is passed as VestingRegistry address", async () => { + await expectRevert( + vesting.initialize( + vestingFactory.address, + SOV.address, + staking.address, + feeSharingProxy.address, + account1, + lockedSOV.address, + [ZERO_ADDRESS, vestingRegistry2.address, vestingRegistry3.address] + ), + "Vesting registry address invalid" + ); + }); + + it("fails if the 0 address is passed as VestingRegistry2 address", async () => { + await expectRevert( + vesting.initialize( + vestingFactory.address, + SOV.address, + staking.address, + feeSharingProxy.address, + account1, + lockedSOV.address, + [vestingRegistry.address, ZERO_ADDRESS, vestingRegistry3.address] + ), + "Vesting registry address invalid" + ); + }); + + it("fails if the 0 address is passed as VestingRegistry3 address", async () => { + await expectRevert( + vesting.initialize( + vestingFactory.address, + SOV.address, + staking.address, + feeSharingProxy.address, + account1, + lockedSOV.address, + [vestingRegistry.address, vestingRegistry2.address, ZERO_ADDRESS] + ), + "Vesting registry address invalid" + ); + }); + + it("sets the expected values", async () => { + await vesting.initialize( + vestingFactory.address, + SOV.address, + staking.address, + feeSharingProxy.address, + account1, + lockedSOV.address, + [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] + ); + + let _sov = await vesting.SOV(); + let _staking = await vesting.staking(); + let _feeSharingProxy = await vesting.feeSharingProxy(); + let _vestingOwner = await vesting.vestingOwner(); + + expect(_sov).equal(SOV.address); + expect(_staking).equal(staking.address); + expect(_feeSharingProxy).equal(feeSharingProxy.address); + expect(_vestingOwner).equal(account1); + }); + + it("fails if initialize is called twice", async () => { + await vesting.initialize( + vestingFactory.address, + SOV.address, + staking.address, + feeSharingProxy.address, + account1, + lockedSOV.address, + [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] + ); + await expectRevert( + vesting.initialize( + vestingFactory.address, + SOV.address, + staking.address, + feeSharingProxy.address, + account1, + lockedSOV.address, + [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] + ), + "contract is already initialized" + ); + }); + }); + + describe("setVestingFactory", () => { + it("sets vesting factory", async () => { + await vesting.setVestingFactory(account2); + + let vestingFactory = await vesting.vestingFactory(); + expect(vestingFactory).equal(account2); + }); + + it("fails if the 0 address is passed", async () => { + await expectRevert( + vesting.setVestingFactory(ZERO_ADDRESS), + "vestingFactory address invalid" + ); + }); + + it("fails if sender isn't an owner", async () => { + await expectRevert( + vesting.setVestingFactory(account2, { from: account2 }), + "unauthorized" + ); + }); + }); + + describe("addAdmin", () => { + it("adds admin", async () => { + let tx = await vesting.addAdmin(account1); + + expectEvent(tx, "AdminAdded", { + admin: account1, + }); + + let isAdmin = await vesting.admins(account1); + expect(isAdmin).equal(true); + }); + + it("fails sender isn't an owner", async () => { + await expectRevert(vesting.addAdmin(account1, { from: account1 }), "unauthorized"); + }); + }); + + describe("removeAdmin", () => { + it("removes admin", async () => { + await vesting.addAdmin(account1); + let tx = await vesting.removeAdmin(account1); + + expectEvent(tx, "AdminRemoved", { + admin: account1, + }); + + let isAdmin = await vesting.admins(account1); + expect(isAdmin).equal(false); + }); + + it("fails sender isn't an owner", async () => { + await expectRevert(vesting.removeAdmin(account1, { from: account1 }), "unauthorized"); + }); + }); + + describe("transferSOV", () => { + it("should be able to transfer SOV", async () => { + await vesting.initialize( + vestingFactory.address, + SOV.address, + staking.address, + feeSharingProxy.address, + account1, + lockedSOV.address, + [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] + ); + + let amount = new BN(1000); + await SOV.transfer(vesting.address, amount); + let balanceBefore = await SOV.balanceOf(account1); + let tx = await vesting.transferSOV(account1, amount); + expectEvent(tx, "SOVTransferred", { + receiver: account1, + amount: amount, + }); + let balanceAfter = await SOV.balanceOf(account1); + + expect(amount).to.be.bignumber.equal(balanceAfter.sub(balanceBefore)); + }); + + it("only owner should be able to transfer", async () => { + await expectRevert( + vesting.transferSOV(account1, 1000, { from: account1 }), + "unauthorized" + ); + }); + + it("fails if the 0 address is passed as receiver address", async () => { + await expectRevert( + vesting.transferSOV(ZERO_ADDRESS, 1000), + "receiver address invalid" + ); + }); + + it("fails if the 0 is passed as an amount", async () => { + await expectRevert(vesting.transferSOV(account1, 0), "amount invalid"); + }); + }); + + describe("createVesting", () => { + it("should be able to create vesting - Bug Bounty", async () => { + await vesting.initialize( + vestingFactory.address, + SOV.address, + staking.address, + feeSharingProxy.address, + account1, + lockedSOV.address, + [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] + ); + + let amount = new BN(1000000); + await SOV.transfer(vesting.address, amount); + + let cliff = FOUR_WEEKS; + let duration = FOUR_WEEKS.mul(new BN(20)); + let vestingType = new BN(2); //Bug Bounty + let tx = await vesting.createVestingAddr( + account2, + amount, + cliff, + duration, + vestingType + ); + let vestingAddress = await vesting.getVestingAddr( + account2, + cliff, + duration, + vestingType + ); + expect(await vesting.isVestingAdress(vestingAddress)).equal(true); + await vesting.stakeTokens(vestingAddress, amount); + + expectEvent(tx, "VestingCreated", { + tokenOwner: account2, + vesting: vestingAddress, + cliff: cliff, + duration: duration, + amount: amount, + vestingCreationType: vestingType, + }); + + let balance = await SOV.balanceOf(vesting.address); + expect(balance.toString()).equal("0"); + + let vestingAddr = await VestingLogic.at(vestingAddress); + await checkVesting(vestingAddr, account2, cliff, duration, amount); + + await expectRevert( + vestingAddr.governanceWithdrawTokens(account2), + "operation not supported" + ); + + let proxy = await UpgradableProxy.at(vestingAddress); + await expectRevert(proxy.setImplementation(account2), "revert"); + }); + + it("should be able to create vesting - Team Salary", async () => { + await vesting.initialize( + vestingFactory.address, + SOV.address, + staking.address, + feeSharingProxy.address, + account1, + lockedSOV.address, + [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] + ); + + let amount = new BN(1000000); + await SOV.transfer(vesting.address, amount); + + let cliff = FOUR_WEEKS; + let duration = FOUR_WEEKS.mul(new BN(20)); + let vestingType = new BN(3); //Team Salary + let tx = await vesting.createVestingAddr( + account2, + amount, + cliff, + duration, + vestingType + ); + let vestingAddress = await vesting.getVestingAddr( + account2, + cliff, + duration, + vestingType + ); + expect(await vesting.isVestingAdress(vestingAddress)).equal(true); + await vesting.stakeTokens(vestingAddress, amount); + + expectEvent(tx, "VestingCreated", { + tokenOwner: account2, + vesting: vestingAddress, + cliff: cliff, + duration: duration, + amount: amount, + vestingCreationType: vestingType, + }); + + let balance = await SOV.balanceOf(vesting.address); + expect(balance.toString()).equal("0"); + + let vestingAddr = await VestingLogic.at(vestingAddress); + await checkVesting(vestingAddr, account2, cliff, duration, amount); + + await expectRevert( + vestingAddr.governanceWithdrawTokens(account2), + "operation not supported" + ); + + let proxy = await UpgradableProxy.at(vestingAddress); + await expectRevert(proxy.setImplementation(account2), "revert"); + }); + + it("fails if vestingRegistryLogic doesn't have enough SOV", async () => { + await vesting.initialize( + vestingFactory.address, + SOV.address, + staking.address, + feeSharingProxy.address, + account1, + lockedSOV.address, + [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] + ); + + let amount = new BN(1000000); + let cliff = FOUR_WEEKS; + let duration = FOUR_WEEKS.mul(new BN(20)); + let vestingType = new BN(3); //Team Salary + + await vesting.createVestingAddr(account2, amount, cliff, duration, vestingType); + let vestingAddress = await vesting.getVestingAddr( + account2, + cliff, + duration, + vestingType + ); + + await expectRevert( + vesting.stakeTokens(vestingAddress, amount), + "ERC20: transfer amount exceeds balance" + ); + }); + + it("fails if sender is not an owner or admin", async () => { + await vesting.initialize( + vestingFactory.address, + SOV.address, + staking.address, + feeSharingProxy.address, + account1, + lockedSOV.address, + [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] + ); + + let amount = new BN(1000000); + let cliff = TEAM_VESTING_CLIFF; + let duration = TEAM_VESTING_DURATION; + let vestingType = new BN(3); //Team Salary + + await expectRevert( + vesting.createVestingAddr(account2, amount, cliff, duration, vestingType, { + from: account1, + }), + "unauthorized" + ); + + await vesting.addAdmin(account1); + await vesting.createVestingAddr(account2, amount, cliff, duration, vestingType, { + from: account1, + }); + }); + }); + + describe("createVesting and getVesting - LockedSOV", () => { + it("Should create vesting and return the address for LockedSOV", async () => { + await vesting.initialize( + vestingFactory.address, + SOV.address, + staking.address, + feeSharingProxy.address, + account1, + lockedSOV.address, + [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] + ); + + let amount = new BN(1000000); + await SOV.transfer(vesting.address, amount); + await lockedSOV.createVesting({ from: accounts4 }); + let vestingAddr = await vesting.getVesting(accounts4); + expect(await vesting.isVestingAdress(vestingAddr)).equal(true); + assert.notEqual(vestingAddr, ZERO_ADDRESS, "Vesting Address should not be zero."); + }); + }); + + describe("createTeamVesting", () => { + it("should be able to create team vesting", async () => { + await vesting.initialize( + vestingFactory.address, + SOV.address, + staking.address, + feeSharingProxy.address, + account1, + lockedSOV.address, + [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] + ); + + let amount = new BN(1000000); + await SOV.transfer(vesting.address, amount); + + let cliff = TEAM_VESTING_CLIFF; + let duration = TEAM_VESTING_DURATION; + let vestingType = new BN(3); //Team Salary + let tx = await vesting.createTeamVesting( + account2, + amount, + cliff, + duration, + vestingType + ); + let vestingAddress = await vesting.getTeamVesting( + account2, + cliff, + duration, + vestingType + ); + expect(await vesting.isVestingAdress(vestingAddress)).equal(true); + expectEvent(tx, "TeamVestingCreated", { + tokenOwner: account2, + vesting: vestingAddress, + cliff: cliff, + duration: duration, + amount: amount, + vestingCreationType: vestingType, + }); + let tx2 = await vesting.stakeTokens(vestingAddress, amount); + expectEvent(tx2, "TokensStaked", { + vesting: vestingAddress, + amount: amount, + }); + let balance = await SOV.balanceOf(vestingRegistryLogic.address); + expect(balance.toString()).equal("0"); + + let vestingAddr = await VestingLogic.at(vestingAddress); + await checkVesting(vestingAddr, account2, cliff, duration, amount); + + await expectRevert(vestingAddr.governanceWithdrawTokens(account2), "unauthorized"); + + let proxy = await UpgradableProxy.at(vestingAddress); + await expectRevert(proxy.setImplementation(account2), "revert"); + }); + + it("fails if vestingRegistryLogic doesn't have enough SOV", async () => { + await vesting.initialize( + vestingFactory.address, + SOV.address, + staking.address, + feeSharingProxy.address, + account1, + lockedSOV.address, + [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] + ); + + let amount = new BN(1000000); + let cliff = TEAM_VESTING_CLIFF; + let duration = TEAM_VESTING_DURATION; + let vestingType = new BN(3); //Team Salary + + await vesting.createTeamVesting(account2, amount, cliff, duration, vestingType); + let vestingAddress = await vesting.getTeamVesting( + account2, + cliff, + duration, + vestingType + ); + + await expectRevert( + vesting.stakeTokens(vestingAddress, amount), + "ERC20: transfer amount exceeds balance" + ); + }); + + it("fails if sender is not an owner or admin", async () => { + await vesting.initialize( + vestingFactory.address, + SOV.address, + staking.address, + feeSharingProxy.address, + account1, + lockedSOV.address, + [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] + ); + + let amount = new BN(1000000); + let cliff = TEAM_VESTING_CLIFF; + let duration = TEAM_VESTING_DURATION; + let vestingType = new BN(3); //Team Salary + + await expectRevert( + vesting.createTeamVesting(account2, amount, cliff, duration, vestingType, { + from: account1, + }), + "unauthorized" + ); + + await vesting.addAdmin(account1); + await vesting.createTeamVesting(account2, amount, cliff, duration, vestingType, { + from: account1, + }); + }); + }); + + describe("stakeTokens", () => { + it("fails if the 0 address is passed as vesting address", async () => { + await expectRevert( + vesting.stakeTokens(ZERO_ADDRESS, new BN(1000000)), + "vesting address invalid" + ); + }); + + it("fails if the 0 address is passed as an amount", async () => { + await expectRevert(vesting.stakeTokens(account1, 0), "amount invalid"); + }); + + it("only owner or admin should be able to stake tokens", async () => { + await vesting.initialize( + vestingFactory.address, + SOV.address, + staking.address, + feeSharingProxy.address, + account1, + lockedSOV.address, + [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] + ); + + let amount = new BN(1000000); + await SOV.transfer(vesting.address, amount); + + let cliff = TEAM_VESTING_CLIFF; + let duration = TEAM_VESTING_DURATION; + let vestingType = new BN(3); //Team Salary + await vesting.createTeamVesting(account2, amount, cliff, duration, vestingType); + let vestingAddress = await vesting.getTeamVesting( + account2, + cliff, + duration, + vestingType + ); + + await expectRevert( + vesting.stakeTokens(vestingAddress, new BN(1000000), { from: account1 }), + "unauthorized" + ); + + await vesting.addAdmin(account1); + await vesting.stakeTokens(vestingAddress, new BN(1000000), { from: account1 }); + }); + }); + + describe("getVestingsOf", () => { + it("gets vesting of a user", async () => { + await vesting.initialize( + vestingFactory.address, + SOV.address, + staking.address, + feeSharingProxy.address, + account1, + lockedSOV.address, + [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] + ); + + let amount = new BN(1000000); + await SOV.transfer(vesting.address, amount); + + //Vesting + let cliff = FOUR_WEEKS; + let duration = FOUR_WEEKS.mul(new BN(20)); + let vestingType = new BN(2); //Bug Bounty + await vesting.createVestingAddr(account2, amount, cliff, duration, vestingType); + + //TeamVesting + let teamCliff = TEAM_VESTING_CLIFF; + let teamDuration = TEAM_VESTING_DURATION; + vestingType = new BN(3); //Team Salary + await vesting.createTeamVesting( + account2, + amount, + teamCliff, + teamDuration, + vestingType + ); + + let vestingAddresses = await vesting.getVestingsOf(account2); + assert.equal(vestingAddresses.length.toString(), "2"); + assert.equal(vestingAddresses[0].vestingCreationType, "2"); + assert.equal(vestingAddresses[1].vestingCreationType, "3"); + }); + }); + + describe("getVestingDetails", () => { + it("gets cliff, duration and amount for vesting address", async () => { + await vesting.initialize( + vestingFactory.address, + SOV.address, + staking.address, + feeSharingProxy.address, + account1, + lockedSOV.address, + [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] + ); + + let amount = new BN(1000000); + await SOV.transfer(vesting.address, amount); + + //Vesting + let cliff = FOUR_WEEKS; + let duration = FOUR_WEEKS.mul(new BN(20)); + let vestingType = new BN(2); //Bug Bounty + await vesting.createVestingAddr(account2, amount, cliff, duration, vestingType); + let vestingAddr = await vesting.getVestingAddr(account2, cliff, duration, vestingType); + let fields = await vesting.getVestingDetails(vestingAddr); + expect(cliff).to.be.bignumber.equal(fields.cliff); + expect(duration).to.be.bignumber.equal(fields.duration); + }); + + it("gets cliff, duration and amount for team vesting address", async () => { + await vesting.initialize( + vestingFactory.address, + SOV.address, + staking.address, + feeSharingProxy.address, + account1, + lockedSOV.address, + [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] + ); + + let amount = new BN(1000000); + await SOV.transfer(vesting.address, amount); + + //TeamVesting + let teamCliff = TEAM_VESTING_CLIFF; + let teamDuration = TEAM_VESTING_DURATION; + let vestingType = new BN(3); //Team Salary + await vesting.createTeamVesting( + account2, + amount, + teamCliff, + teamDuration, + vestingType + ); + let vestingAddr = await vesting.getTeamVesting( + account2, + teamCliff, + teamDuration, + vestingType + ); + let fields = await vesting.getVestingDetails(vestingAddr); + expect(teamCliff).to.be.bignumber.equal(fields.cliff); + expect(teamDuration).to.be.bignumber.equal(fields.duration); + }); + }); + + describe("isVestingAdress", () => { + it("should return false if the address isn't a vesting address", async () => { + expect(await vesting.isVestingAdress(account1)).equal(false); + }); + }); + + async function checkVesting(vesting, account, cliff, duration, amount) { + await mineBlock(); + + let vestingBalance = await staking.balanceOf(vesting.address); + expect(vestingBalance).to.be.bignumber.equal(amount); + + let accountVotes = await staking.getCurrentVotes(account); + expect(accountVotes).to.be.not.equal(new BN(0)); + let vestingVotes = await staking.getCurrentVotes(vesting.address); + expect(vestingVotes).to.be.bignumber.equal(new BN(0)); + + let startDate = await vesting.startDate(); + let start = startDate.toNumber() + cliff.toNumber(); + let end = startDate.toNumber() + duration.toNumber(); + + let numIntervals = Math.floor((end - start) / FOUR_WEEKS) + 1; + let stakedPerInterval = Math.floor(amount / numIntervals); + + let stakeForFirstInterval = amount - stakedPerInterval * (numIntervals - 1); + + expect(await vesting.cliff()).to.be.bignumber.equal(cliff); + expect(await vesting.duration()).to.be.bignumber.equal(duration); + + for (let i = start; i <= end; i += FOUR_WEEKS) { + let lockedTS = await staking.timestampToLockDate(i); + + let numUserStakingCheckpoints = await staking.numUserStakingCheckpoints( + vesting.address, + lockedTS + ); + let userStakingCheckpoints = await staking.userStakingCheckpoints( + vesting.address, + lockedTS, + numUserStakingCheckpoints - 1 + ); + assert.equal(numUserStakingCheckpoints.toString(), "1"); + if (i === start) { + assert.equal(userStakingCheckpoints.stake.toString(), stakeForFirstInterval); + } else { + assert.equal(userStakingCheckpoints.stake.toString(), stakedPerInterval); + } + + let numDelegateStakingCheckpoints = await staking.numDelegateStakingCheckpoints( + account, + lockedTS + ); + let delegateStakingCheckpoints = await staking.delegateStakingCheckpoints( + account, + lockedTS, + numUserStakingCheckpoints - 1 + ); + assert.equal(numDelegateStakingCheckpoints.toString(), "1"); + if (i === start) { + assert.equal(delegateStakingCheckpoints.stake.toString(), stakeForFirstInterval); + } else { + assert.equal(delegateStakingCheckpoints.stake.toString(), stakedPerInterval); + } + } + } }); diff --git a/tests/vesting/VestingRegistryMigrations.js b/tests/vesting/VestingRegistryMigrations.js index 274a399ff..2184dd36e 100644 --- a/tests/vesting/VestingRegistryMigrations.js +++ b/tests/vesting/VestingRegistryMigrations.js @@ -28,273 +28,320 @@ const pricsSats = "2500"; const ONE_MILLON = "1000000000000000000000000"; contract("VestingRegistryMigrations", (accounts) => { - let root, account1, account2, account3, account4; - let SOV, lockedSOV; - let staking, stakingLogic, feeSharingProxy; - let vesting, vestingFactory, vestingLogic, vestingRegistryLogic; - let vestingRegistry, vestingRegistry2, vestingRegistry3; - let vestingAddress, vestingAddress2, vestingAddress3; - let vestingTeamAddress, vestingTeamAddress2, vestingTeamAddress3; - let newVestingAddress, newVestingAddress2, newVestingAddress3; - let newTeamVestingAddress, newTeamVestingAddress2, newTeamVestingAddress3; - let fourYearVestingLogic, fourYearVesting; - - let cliff = 1; // This is in 4 weeks. i.e. 1 * 4 weeks. - let duration = 11; // This is in 4 weeks. i.e. 11 * 4 weeks. - - before(async () => { - [root, account1, account2, account3, account4, ...accounts] = accounts; - }); - - beforeEach(async () => { - SOV = await SOV_ABI.new(TOTAL_SUPPLY); - cSOV1 = await TestToken.new("cSOV1", "cSOV1", 18, TOTAL_SUPPLY); - cSOV2 = await TestToken.new("cSOV2", "cSOV2", 18, TOTAL_SUPPLY); - - stakingLogic = await StakingLogic.new(); - staking = await StakingProxy.new(SOV.address); - await staking.setImplementation(stakingLogic.address); - staking = await StakingLogic.at(staking.address); - - feeSharingProxy = await FeeSharingProxy.new(ZERO_ADDRESS, staking.address); - - vestingLogic = await VestingLogic.new(); - vestingFactory = await VestingFactory.new(vestingLogic.address); - - vestingRegistryLogic = await VestingRegistryLogic.new(); - vesting = await VestingRegistryProxy.new(); - await vesting.setImplementation(vestingRegistryLogic.address); - vesting = await VestingRegistryLogic.at(vesting.address); - await staking.setVestingRegistry(vesting.address); - - lockedSOV = await LockedSOV.new(SOV.address, vesting.address, cliff, duration, [root]); - await vesting.addAdmin(lockedSOV.address); - - // Deploy four year vesting contracts - fourYearVestingLogic = await FourYearVestingLogic.new(); - fourYearVesting = await FourYearVesting.new( - fourYearVestingLogic.address, - SOV.address, - staking.address, - account4, - feeSharingProxy.address - ); - fourYearVesting = await FourYearVestingLogic.at(fourYearVesting.address); - }); - - describe("addDeployedVestings", () => { - it("adds deployed vestings from VestingRegistry ", async () => { - vestingRegistry = await VestingRegistry.new( - vestingFactory.address, - SOV.address, - [cSOV1.address, cSOV2.address], - pricsSats, - staking.address, - feeSharingProxy.address, - account1 - ); - - let amt = new BN(2000000); - let amount = new BN(200000); - await SOV.transfer(vestingRegistry.address, amt); - - let cliff = FOUR_WEEKS; - let duration = FOUR_WEEKS.mul(new BN(20)); - let teamCliff = TEAM_VESTING_CLIFF; - let teamDuration = TEAM_VESTING_DURATION; - - await vestingFactory.transferOwnership(vestingRegistry.address); - await vestingRegistry.createVesting(account1, amount, cliff, duration); - vestingAddress = await vestingRegistry.getVesting(account1); - await vestingRegistry.stakeTokens(vestingAddress, amount); - await vestingRegistry.createTeamVesting(account1, amount, teamCliff, teamDuration); - vestingTeamAddress = await vestingRegistry.getTeamVesting(account1); - await vestingRegistry.stakeTokens(vestingTeamAddress, amount); - assert.notEqual(vestingAddress, ZERO_ADDRESS, "Vesting Address should not be zero."); - assert.notEqual(vestingTeamAddress, ZERO_ADDRESS, "Vesting Team Address should not be zero."); - }); - - it("adds deployed vestings from VestingRegistry2 ", async () => { - vestingRegistry2 = await VestingRegistry2.new( - vestingFactory.address, - SOV.address, - [cSOV1.address, cSOV2.address], - pricsSats, - staking.address, - feeSharingProxy.address, - account1 - ); - - let amt = new BN(2000000); - let amount = new BN(200000); - await SOV.transfer(vestingRegistry2.address, amt); - - let cliff = FOUR_WEEKS; - let duration = FOUR_WEEKS.mul(new BN(20)); - let teamCliff = TEAM_VESTING_CLIFF; - let teamDuration = TEAM_VESTING_DURATION; - - await vestingFactory.transferOwnership(vestingRegistry2.address); - await vestingRegistry2.createVesting(account2, amount, cliff, duration); - vestingAddress2 = await vestingRegistry2.getVesting(account2); - let tx = await vestingRegistry2.stakeTokens(vestingAddress2, amount); - expectEvent(tx, "TokensStaked", { - vesting: vestingAddress2, - amount: amount, - }); - await staking.balanceOf(vestingAddress2); - await vestingRegistry2.createTeamVesting(account2, amount, teamCliff, teamDuration); - vestingTeamAddress2 = await vestingRegistry2.getTeamVesting(account2); - let tx2 = await vestingRegistry2.stakeTokens(vestingTeamAddress2, amount); - expectEvent(tx2, "TokensStaked", { - vesting: vestingTeamAddress2, - amount: amount, - }); - assert.notEqual(vestingAddress2, ZERO_ADDRESS, "Vesting Address should not be zero."); - assert.notEqual(vestingTeamAddress2, ZERO_ADDRESS, "Vesting Team Address should not be zero."); - }); - - it("adds deployed vestings from VestingRegistry3 ", async () => { - vestingRegistry3 = await VestingRegistry3.new( - vestingFactory.address, - SOV.address, - staking.address, - feeSharingProxy.address, - account1 - ); - - let amt = new BN(2000000); - let amount = new BN(200000); - await SOV.transfer(vestingRegistry3.address, amt); - - let cliff = FOUR_WEEKS; - let duration = FOUR_WEEKS.mul(new BN(20)); - let teamCliff = TEAM_VESTING_CLIFF; - let teamDuration = TEAM_VESTING_DURATION; - - await vestingFactory.transferOwnership(vestingRegistry3.address); - await vestingRegistry3.createVesting(account3, amount, cliff, duration); - vestingAddress3 = await vestingRegistry3.getVesting(account3); - await vestingRegistry3.stakeTokens(vestingAddress3, amount); - await vestingRegistry3.createTeamVesting(account3, amount, teamCliff, teamDuration); - vestingTeamAddress3 = await vestingRegistry3.getTeamVesting(account3); - await vestingRegistry3.stakeTokens(vestingTeamAddress3, amount); - assert.notEqual(vestingAddress3, ZERO_ADDRESS, "Vesting Address should not be zero."); - assert.notEqual(vestingTeamAddress3, ZERO_ADDRESS, "Vesting Team Address should not be zero."); - }); - - it("adds deployed vestings to new Vesting Registry ", async () => { - await vesting.initialize( - vestingFactory.address, - SOV.address, - staking.address, - feeSharingProxy.address, - account1, - lockedSOV.address, - [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] - ); - - let cliff = FOUR_WEEKS; - let duration = FOUR_WEEKS.mul(new BN(20)); - let teamCliff = TEAM_VESTING_CLIFF; - let teamDuration = TEAM_VESTING_DURATION; - - vestingFactory.transferOwnership(vesting.address); - let tx = await vesting.addDeployedVestings([account1, account2, account3], [1, 2, 3]); - console.log("gasUsed = " + tx.receipt.gasUsed); - - newVestingAddress = await vesting.getVestingAddr(account1, cliff, duration, 1); - expect(await vesting.isVestingAdress(newVestingAddress)).equal(true); - newVestingAddress2 = await vesting.getVestingAddr(account2, cliff, duration, 2); - expect(await vesting.isVestingAdress(newVestingAddress2)).equal(true); - newVestingAddress3 = await vesting.getVestingAddr(account3, cliff, duration, 3); - expect(await vesting.isVestingAdress(newVestingAddress3)).equal(true); - newTeamVestingAddress = await vesting.getTeamVesting(account1, teamCliff, teamDuration, 1); - expect(await vesting.isVestingAdress(newTeamVestingAddress)).equal(true); - newTeamVestingAddress2 = await vesting.getTeamVesting(account2, teamCliff, teamDuration, 2); - expect(await vesting.isVestingAdress(newTeamVestingAddress2)).equal(true); - newTeamVestingAddress3 = await vesting.getTeamVesting(account3, teamCliff, teamDuration, 3); - expect(await vesting.isVestingAdress(newTeamVestingAddress3)).equal(true); - - expect(vestingAddress).equal(newVestingAddress); - expect(vestingAddress2).equal(newVestingAddress2); - expect(vestingAddress3).equal(newVestingAddress3); - expect(vestingTeamAddress).equal(newTeamVestingAddress); - expect(vestingTeamAddress2).equal(newTeamVestingAddress2); - expect(vestingTeamAddress3).equal(newTeamVestingAddress3); - - let vestingAddresses = await vesting.getVestingsOf(account2); - assert.equal(vestingAddresses.length.toString(), "2"); - assert.equal(vestingAddresses[0].vestingType, 1); - assert.equal(vestingAddresses[0].vestingCreationType, 2); - assert.equal(vestingAddresses[0].vestingAddress, newVestingAddress2); - assert.equal(vestingAddresses[1].vestingType, 0); - assert.equal(vestingAddresses[1].vestingCreationType, 2); - assert.equal(vestingAddresses[1].vestingAddress, newTeamVestingAddress2); - }); - - it("fails if the 0 address is passed", async () => { - await expectRevert(vesting.addDeployedVestings([ZERO_ADDRESS], [1]), "token owner cannot be 0 address"); - }); - - it("fails if the 0 address is passed", async () => { - await expectRevert(vesting.addDeployedVestings([account1], [0]), "vesting creation type must be greater than 0"); - }); - - it("fails if sender isn't an owner", async () => { - await expectRevert(vesting.addDeployedVestings([account1], [1], { from: account2 }), "unauthorized"); - }); - - it("adds deployed four year vestings ", async () => { - // Stake tokens - await SOV.approve(fourYearVesting.address, ONE_MILLON); - - let remainingStakeAmount = ONE_MILLON; - let lastStakingSchedule = 0; - while (remainingStakeAmount > 0) { - await fourYearVesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); - lastStakingSchedule = await fourYearVesting.lastStakingSchedule(); - remainingStakeAmount = await fourYearVesting.remainingStakeAmount(); - } - - // Verify the vesting is created correctly - let data = await staking.getStakes.call(fourYearVesting.address); - assert.equal(data.dates.length, 39); - assert.equal(data.stakes.length, 39); - expect(data.stakes[0]).to.be.bignumber.equal(data.stakes[38]); - - // Add deployed four year vesting to registry - let tx = await vesting.addFourYearVestings([account4], [fourYearVesting.address]); - console.log("gasUsed = " + tx.receipt.gasUsed); - - // Verify that it is added to the registry - let cliff = FOUR_WEEKS; - let duration = FOUR_WEEKS.mul(new BN(39)); - let newVestingAddress = await vesting.getVestingAddr(account4, cliff, duration, 4); - expect(fourYearVesting.address).equal(newVestingAddress); - expect(await vesting.isVestingAdress(newVestingAddress)).equal(true); - - let vestingAddresses = await vesting.getVestingsOf(account4); - assert.equal(vestingAddresses.length.toString(), "1"); - assert.equal(vestingAddresses[0].vestingType, 1); - assert.equal(vestingAddresses[0].vestingCreationType, 4); - assert.equal(vestingAddresses[0].vestingAddress, newVestingAddress); - }); - - it("fails adding four year vesting if array mismatch", async () => { - await expectRevert(vesting.addFourYearVestings([account4], []), "arrays mismatch"); - }); - - it("fails adding four year vesting if token owner is zero address", async () => { - await expectRevert(vesting.addFourYearVestings([ZERO_ADDRESS], [fourYearVesting.address]), "token owner cannot be 0 address"); - }); - - it("fails adding four year vesting if vesting is zero address", async () => { - await expectRevert(vesting.addFourYearVestings([account4], [ZERO_ADDRESS]), "vesting cannot be 0 address"); - }); - - it("fails adding four year vesting if sender isn't an owner", async () => { - await expectRevert(vesting.addFourYearVestings([account1], [fourYearVesting.address], { from: account2 }), "unauthorized"); - }); - }); + let root, account1, account2, account3, account4; + let SOV, lockedSOV; + let staking, stakingLogic, feeSharingProxy; + let vesting, vestingFactory, vestingLogic, vestingRegistryLogic; + let vestingRegistry, vestingRegistry2, vestingRegistry3; + let vestingAddress, vestingAddress2, vestingAddress3; + let vestingTeamAddress, vestingTeamAddress2, vestingTeamAddress3; + let newVestingAddress, newVestingAddress2, newVestingAddress3; + let newTeamVestingAddress, newTeamVestingAddress2, newTeamVestingAddress3; + let fourYearVestingLogic, fourYearVesting; + + let cliff = 1; // This is in 4 weeks. i.e. 1 * 4 weeks. + let duration = 11; // This is in 4 weeks. i.e. 11 * 4 weeks. + + before(async () => { + [root, account1, account2, account3, account4, ...accounts] = accounts; + }); + + beforeEach(async () => { + SOV = await SOV_ABI.new(TOTAL_SUPPLY); + cSOV1 = await TestToken.new("cSOV1", "cSOV1", 18, TOTAL_SUPPLY); + cSOV2 = await TestToken.new("cSOV2", "cSOV2", 18, TOTAL_SUPPLY); + + stakingLogic = await StakingLogic.new(); + staking = await StakingProxy.new(SOV.address); + await staking.setImplementation(stakingLogic.address); + staking = await StakingLogic.at(staking.address); + + feeSharingProxy = await FeeSharingProxy.new(ZERO_ADDRESS, staking.address); + + vestingLogic = await VestingLogic.new(); + vestingFactory = await VestingFactory.new(vestingLogic.address); + + vestingRegistryLogic = await VestingRegistryLogic.new(); + vesting = await VestingRegistryProxy.new(); + await vesting.setImplementation(vestingRegistryLogic.address); + vesting = await VestingRegistryLogic.at(vesting.address); + await staking.setVestingRegistry(vesting.address); + + lockedSOV = await LockedSOV.new(SOV.address, vesting.address, cliff, duration, [root]); + await vesting.addAdmin(lockedSOV.address); + + // Deploy four year vesting contracts + fourYearVestingLogic = await FourYearVestingLogic.new(); + fourYearVesting = await FourYearVesting.new( + fourYearVestingLogic.address, + SOV.address, + staking.address, + account4, + feeSharingProxy.address + ); + fourYearVesting = await FourYearVestingLogic.at(fourYearVesting.address); + }); + + describe("addDeployedVestings", () => { + it("adds deployed vestings from VestingRegistry ", async () => { + vestingRegistry = await VestingRegistry.new( + vestingFactory.address, + SOV.address, + [cSOV1.address, cSOV2.address], + pricsSats, + staking.address, + feeSharingProxy.address, + account1 + ); + + let amt = new BN(2000000); + let amount = new BN(200000); + await SOV.transfer(vestingRegistry.address, amt); + + let cliff = FOUR_WEEKS; + let duration = FOUR_WEEKS.mul(new BN(20)); + let teamCliff = TEAM_VESTING_CLIFF; + let teamDuration = TEAM_VESTING_DURATION; + + await vestingFactory.transferOwnership(vestingRegistry.address); + await vestingRegistry.createVesting(account1, amount, cliff, duration); + vestingAddress = await vestingRegistry.getVesting(account1); + await vestingRegistry.stakeTokens(vestingAddress, amount); + await vestingRegistry.createTeamVesting(account1, amount, teamCliff, teamDuration); + vestingTeamAddress = await vestingRegistry.getTeamVesting(account1); + await vestingRegistry.stakeTokens(vestingTeamAddress, amount); + assert.notEqual(vestingAddress, ZERO_ADDRESS, "Vesting Address should not be zero."); + assert.notEqual( + vestingTeamAddress, + ZERO_ADDRESS, + "Vesting Team Address should not be zero." + ); + }); + + it("adds deployed vestings from VestingRegistry2 ", async () => { + vestingRegistry2 = await VestingRegistry2.new( + vestingFactory.address, + SOV.address, + [cSOV1.address, cSOV2.address], + pricsSats, + staking.address, + feeSharingProxy.address, + account1 + ); + + let amt = new BN(2000000); + let amount = new BN(200000); + await SOV.transfer(vestingRegistry2.address, amt); + + let cliff = FOUR_WEEKS; + let duration = FOUR_WEEKS.mul(new BN(20)); + let teamCliff = TEAM_VESTING_CLIFF; + let teamDuration = TEAM_VESTING_DURATION; + + await vestingFactory.transferOwnership(vestingRegistry2.address); + await vestingRegistry2.createVesting(account2, amount, cliff, duration); + vestingAddress2 = await vestingRegistry2.getVesting(account2); + let tx = await vestingRegistry2.stakeTokens(vestingAddress2, amount); + expectEvent(tx, "TokensStaked", { + vesting: vestingAddress2, + amount: amount, + }); + await staking.balanceOf(vestingAddress2); + await vestingRegistry2.createTeamVesting(account2, amount, teamCliff, teamDuration); + vestingTeamAddress2 = await vestingRegistry2.getTeamVesting(account2); + let tx2 = await vestingRegistry2.stakeTokens(vestingTeamAddress2, amount); + expectEvent(tx2, "TokensStaked", { + vesting: vestingTeamAddress2, + amount: amount, + }); + assert.notEqual(vestingAddress2, ZERO_ADDRESS, "Vesting Address should not be zero."); + assert.notEqual( + vestingTeamAddress2, + ZERO_ADDRESS, + "Vesting Team Address should not be zero." + ); + }); + + it("adds deployed vestings from VestingRegistry3 ", async () => { + vestingRegistry3 = await VestingRegistry3.new( + vestingFactory.address, + SOV.address, + staking.address, + feeSharingProxy.address, + account1 + ); + + let amt = new BN(2000000); + let amount = new BN(200000); + await SOV.transfer(vestingRegistry3.address, amt); + + let cliff = FOUR_WEEKS; + let duration = FOUR_WEEKS.mul(new BN(20)); + let teamCliff = TEAM_VESTING_CLIFF; + let teamDuration = TEAM_VESTING_DURATION; + + await vestingFactory.transferOwnership(vestingRegistry3.address); + await vestingRegistry3.createVesting(account3, amount, cliff, duration); + vestingAddress3 = await vestingRegistry3.getVesting(account3); + await vestingRegistry3.stakeTokens(vestingAddress3, amount); + await vestingRegistry3.createTeamVesting(account3, amount, teamCliff, teamDuration); + vestingTeamAddress3 = await vestingRegistry3.getTeamVesting(account3); + await vestingRegistry3.stakeTokens(vestingTeamAddress3, amount); + assert.notEqual(vestingAddress3, ZERO_ADDRESS, "Vesting Address should not be zero."); + assert.notEqual( + vestingTeamAddress3, + ZERO_ADDRESS, + "Vesting Team Address should not be zero." + ); + }); + + it("adds deployed vestings to new Vesting Registry ", async () => { + await vesting.initialize( + vestingFactory.address, + SOV.address, + staking.address, + feeSharingProxy.address, + account1, + lockedSOV.address, + [vestingRegistry.address, vestingRegistry2.address, vestingRegistry3.address] + ); + + let cliff = FOUR_WEEKS; + let duration = FOUR_WEEKS.mul(new BN(20)); + let teamCliff = TEAM_VESTING_CLIFF; + let teamDuration = TEAM_VESTING_DURATION; + + vestingFactory.transferOwnership(vesting.address); + let tx = await vesting.addDeployedVestings([account1, account2, account3], [1, 2, 3]); + console.log("gasUsed = " + tx.receipt.gasUsed); + + newVestingAddress = await vesting.getVestingAddr(account1, cliff, duration, 1); + expect(await vesting.isVestingAdress(newVestingAddress)).equal(true); + newVestingAddress2 = await vesting.getVestingAddr(account2, cliff, duration, 2); + expect(await vesting.isVestingAdress(newVestingAddress2)).equal(true); + newVestingAddress3 = await vesting.getVestingAddr(account3, cliff, duration, 3); + expect(await vesting.isVestingAdress(newVestingAddress3)).equal(true); + newTeamVestingAddress = await vesting.getTeamVesting( + account1, + teamCliff, + teamDuration, + 1 + ); + expect(await vesting.isVestingAdress(newTeamVestingAddress)).equal(true); + newTeamVestingAddress2 = await vesting.getTeamVesting( + account2, + teamCliff, + teamDuration, + 2 + ); + expect(await vesting.isVestingAdress(newTeamVestingAddress2)).equal(true); + newTeamVestingAddress3 = await vesting.getTeamVesting( + account3, + teamCliff, + teamDuration, + 3 + ); + expect(await vesting.isVestingAdress(newTeamVestingAddress3)).equal(true); + + expect(vestingAddress).equal(newVestingAddress); + expect(vestingAddress2).equal(newVestingAddress2); + expect(vestingAddress3).equal(newVestingAddress3); + expect(vestingTeamAddress).equal(newTeamVestingAddress); + expect(vestingTeamAddress2).equal(newTeamVestingAddress2); + expect(vestingTeamAddress3).equal(newTeamVestingAddress3); + + let vestingAddresses = await vesting.getVestingsOf(account2); + assert.equal(vestingAddresses.length.toString(), "2"); + assert.equal(vestingAddresses[0].vestingType, 1); + assert.equal(vestingAddresses[0].vestingCreationType, 2); + assert.equal(vestingAddresses[0].vestingAddress, newVestingAddress2); + assert.equal(vestingAddresses[1].vestingType, 0); + assert.equal(vestingAddresses[1].vestingCreationType, 2); + assert.equal(vestingAddresses[1].vestingAddress, newTeamVestingAddress2); + }); + + it("fails if the 0 address is passed", async () => { + await expectRevert( + vesting.addDeployedVestings([ZERO_ADDRESS], [1]), + "token owner cannot be 0 address" + ); + }); + + it("fails if the 0 address is passed", async () => { + await expectRevert( + vesting.addDeployedVestings([account1], [0]), + "vesting creation type must be greater than 0" + ); + }); + + it("fails if sender isn't an owner", async () => { + await expectRevert( + vesting.addDeployedVestings([account1], [1], { from: account2 }), + "unauthorized" + ); + }); + + it("adds deployed four year vestings ", async () => { + // Stake tokens + await SOV.approve(fourYearVesting.address, ONE_MILLON); + + let remainingStakeAmount = ONE_MILLON; + let lastStakingSchedule = 0; + while (remainingStakeAmount > 0) { + await fourYearVesting.stakeTokens(remainingStakeAmount, lastStakingSchedule); + lastStakingSchedule = await fourYearVesting.lastStakingSchedule(); + remainingStakeAmount = await fourYearVesting.remainingStakeAmount(); + } + + // Verify the vesting is created correctly + let data = await staking.getStakes.call(fourYearVesting.address); + assert.equal(data.dates.length, 39); + assert.equal(data.stakes.length, 39); + expect(data.stakes[0]).to.be.bignumber.equal(data.stakes[38]); + + // Add deployed four year vesting to registry + let tx = await vesting.addFourYearVestings([account4], [fourYearVesting.address]); + console.log("gasUsed = " + tx.receipt.gasUsed); + + // Verify that it is added to the registry + let cliff = FOUR_WEEKS; + let duration = FOUR_WEEKS.mul(new BN(39)); + let newVestingAddress = await vesting.getVestingAddr(account4, cliff, duration, 4); + expect(fourYearVesting.address).equal(newVestingAddress); + expect(await vesting.isVestingAdress(newVestingAddress)).equal(true); + + let vestingAddresses = await vesting.getVestingsOf(account4); + assert.equal(vestingAddresses.length.toString(), "1"); + assert.equal(vestingAddresses[0].vestingType, 1); + assert.equal(vestingAddresses[0].vestingCreationType, 4); + assert.equal(vestingAddresses[0].vestingAddress, newVestingAddress); + }); + + it("fails adding four year vesting if array mismatch", async () => { + await expectRevert(vesting.addFourYearVestings([account4], []), "arrays mismatch"); + }); + + it("fails adding four year vesting if token owner is zero address", async () => { + await expectRevert( + vesting.addFourYearVestings([ZERO_ADDRESS], [fourYearVesting.address]), + "token owner cannot be 0 address" + ); + }); + + it("fails adding four year vesting if vesting is zero address", async () => { + await expectRevert( + vesting.addFourYearVestings([account4], [ZERO_ADDRESS]), + "vesting cannot be 0 address" + ); + }); + + it("fails adding four year vesting if sender isn't an owner", async () => { + await expectRevert( + vesting.addFourYearVestings([account1], [fourYearVesting.address], { + from: account2, + }), + "unauthorized" + ); + }); + }); }); From d18e764df1657a26cefb3819906aaa3e1787250d Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Tue, 26 Apr 2022 17:03:06 +0530 Subject: [PATCH 52/86] Prettier - auto run --- .../loantoken/LoanTokenLogicStandard.sol | 153 +++++++++--------- contracts/governance/Staking/Staking.sol | 20 +-- .../Vesting/VestingRegistryLogic.sol | 84 +++++----- contracts/modules/LoanMaintenance.sol | 139 ++++++++-------- .../mainnet_contracts.json | 2 +- .../testnet_contracts.json | 2 +- 6 files changed, 209 insertions(+), 191 deletions(-) diff --git a/contracts/connectors/loantoken/LoanTokenLogicStandard.sol b/contracts/connectors/loantoken/LoanTokenLogicStandard.sol index ce976624e..8d699b264 100644 --- a/contracts/connectors/loantoken/LoanTokenLogicStandard.sol +++ b/contracts/connectors/loantoken/LoanTokenLogicStandard.sol @@ -354,11 +354,8 @@ contract LoanTokenLogicStandard is LoanTokenLogicStorage { /// @dev Compute the worth of the total deposit in loan tokens. /// (loanTokenSent + convert(collateralTokenSent)) /// No actual swap happening here. - uint256 totalDeposit = _totalDeposit( - collateralTokenAddress, - collateralTokenSent, - loanTokenSent - ); + uint256 totalDeposit = + _totalDeposit(collateralTokenAddress, collateralTokenSent, loanTokenSent); require(totalDeposit != 0, "12"); address[4] memory sentAddresses; @@ -819,11 +816,8 @@ contract LoanTokenLogicStandard is LoanTokenLogicStorage { collateralTokenAddress = wrbtcTokenAddress; } - uint256 totalDeposit = _totalDeposit( - collateralTokenAddress, - collateralTokenSent, - loanTokenSent - ); + uint256 totalDeposit = + _totalDeposit(collateralTokenAddress, collateralTokenSent, loanTokenSent); (principal, interestRate) = _getMarginBorrowAmountAndRate(leverageAmount, totalDeposit); if (principal > _underlyingBalance()) { @@ -864,29 +858,29 @@ contract LoanTokenLogicStandard is LoanTokenLogicStorage { address collateralTokenAddress /// address(0) means rBTC ) public view returns (uint256 depositAmount) { if (borrowAmount != 0) { - (, , uint256 newBorrowAmount) = _getInterestRateAndBorrowAmount( - borrowAmount, - totalAssetSupply(), - initialLoanDuration - ); + (, , uint256 newBorrowAmount) = + _getInterestRateAndBorrowAmount( + borrowAmount, + totalAssetSupply(), + initialLoanDuration + ); if (newBorrowAmount <= _underlyingBalance()) { if (collateralTokenAddress == address(0)) collateralTokenAddress = wrbtcTokenAddress; - bytes32 loanParamsId = loanParamsIds[ - uint256(keccak256(abi.encodePacked(collateralTokenAddress, true))) - ]; + bytes32 loanParamsId = + loanParamsIds[ + uint256(keccak256(abi.encodePacked(collateralTokenAddress, true))) + ]; return ProtocolLike(sovrynContractAddress) .getRequiredCollateral( - loanTokenAddress, - collateralTokenAddress, - newBorrowAmount, - ProtocolSettingsLike(sovrynContractAddress).minInitialMargin( - loanParamsId - ), /// initialMargin - true /// isTorqueLoan - ) + loanTokenAddress, + collateralTokenAddress, + newBorrowAmount, + ProtocolSettingsLike(sovrynContractAddress).minInitialMargin(loanParamsId), /// initialMargin + true /// isTorqueLoan + ) .add(10); /// Some dust to compensate for rounding errors. } } @@ -915,9 +909,8 @@ contract LoanTokenLogicStandard is LoanTokenLogicStorage { ) public view returns (uint256 borrowAmount) { if (depositAmount != 0) { if (collateralTokenAddress == address(0)) collateralTokenAddress = wrbtcTokenAddress; - bytes32 loanParamsId = loanParamsIds[ - uint256(keccak256(abi.encodePacked(collateralTokenAddress, true))) - ]; + bytes32 loanParamsId = + loanParamsIds[uint256(keccak256(abi.encodePacked(collateralTokenAddress, true)))]; borrowAmount = ProtocolLike(sovrynContractAddress).getBorrowAmount( loanTokenAddress, collateralTokenAddress, @@ -951,11 +944,14 @@ contract LoanTokenLogicStandard is LoanTokenLogicStorage { uint256 minEntryPrice ) public view { /// @dev See how many collateralTokens we would get if exchanging this amount of loan tokens to collateral tokens. - uint256 collateralTokensReceived = ProtocolLike(sovrynContractAddress) - .getSwapExpectedReturn(loanTokenAddress, collateralTokenAddress, loanTokenSent); - uint256 collateralTokenPrice = (collateralTokensReceived.mul(WEI_PRECISION)).div( - loanTokenSent - ); + uint256 collateralTokensReceived = + ProtocolLike(sovrynContractAddress).getSwapExpectedReturn( + loanTokenAddress, + collateralTokenAddress, + loanTokenSent + ); + uint256 collateralTokenPrice = + (collateralTokensReceived.mul(WEI_PRECISION)).div(loanTokenSent); require(collateralTokenPrice >= minEntryPrice, "entry price above the minimum"); } @@ -1095,22 +1091,27 @@ contract LoanTokenLogicStandard is LoanTokenLogicStorage { if (collateralTokenSent != 0) { /// @dev Get the oracle rate from collateral -> loan - (uint256 collateralToLoanRate, uint256 collateralToLoanPrecision) = FeedsLike( - ProtocolLike(sovrynContractAddress).priceFeeds() - ).queryRate(collateralTokenAddress, loanTokenAddress); + (uint256 collateralToLoanRate, uint256 collateralToLoanPrecision) = + FeedsLike(ProtocolLike(sovrynContractAddress).priceFeeds()).queryRate( + collateralTokenAddress, + loanTokenAddress + ); require( (collateralToLoanRate != 0) && (collateralToLoanPrecision != 0), "invalid rate collateral token" ); /// @dev Compute the loan token amount with the oracle rate. - uint256 loanTokenAmount = collateralTokenSent.mul(collateralToLoanRate).div( - collateralToLoanPrecision - ); + uint256 loanTokenAmount = + collateralTokenSent.mul(collateralToLoanRate).div(collateralToLoanPrecision); /// @dev See how many collateralTokens we would get if exchanging this amount of loan tokens to collateral tokens. - uint256 collateralTokenAmount = ProtocolLike(sovrynContractAddress) - .getSwapExpectedReturn(loanTokenAddress, collateralTokenAddress, loanTokenAmount); + uint256 collateralTokenAmount = + ProtocolLike(sovrynContractAddress).getSwapExpectedReturn( + loanTokenAddress, + collateralTokenAddress, + loanTokenAmount + ); /// @dev Probably not the same due to the price difference. if (collateralTokenAmount != collateralTokenSent) { @@ -1131,9 +1132,11 @@ contract LoanTokenLogicStandard is LoanTokenLogicStorage { * @return amount in RBTC * */ function _getAmountInRbtc(address asset, uint256 amount) internal returns (uint256) { - (uint256 rbtcRate, uint256 rbtcPrecision) = FeedsLike( - ProtocolLike(sovrynContractAddress).priceFeeds() - ).queryRate(asset, wrbtcTokenAddress); + (uint256 rbtcRate, uint256 rbtcPrecision) = + FeedsLike(ProtocolLike(sovrynContractAddress).priceFeeds()).queryRate( + asset, + wrbtcTokenAddress + ); return amount.mul(rbtcRate).div(rbtcPrecision); } @@ -1212,12 +1215,8 @@ contract LoanTokenLogicStandard is LoanTokenLogicStorage { } /// @dev Handle transfers prior to adding newPrincipal to loanTokenSent - uint256 msgValue = _verifyTransfers( - collateralTokenAddress, - sentAddresses, - sentAmounts, - withdrawAmount - ); + uint256 msgValue = + _verifyTransfers(collateralTokenAddress, sentAddresses, sentAmounts, withdrawAmount); /** * @dev Adding the loan token portion from the lender to loanTokenSent @@ -1236,9 +1235,10 @@ contract LoanTokenLogicStandard is LoanTokenLogicStorage { withdrawAmountExist = true; } - bytes32 loanParamsId = loanParamsIds[ - uint256(keccak256(abi.encodePacked(collateralTokenAddress, withdrawAmountExist))) - ]; + bytes32 loanParamsId = + loanParamsIds[ + uint256(keccak256(abi.encodePacked(collateralTokenAddress, withdrawAmountExist))) + ]; (sentAmounts[1], sentAmounts[4]) = ProtocolLike(sovrynContractAddress) .borrowOrTradeFromPool @@ -1486,11 +1486,8 @@ contract LoanTokenLogicStandard is LoanTokenLogicStorage { _avgBorrowInterestRate(assetBorrow) .mul(_utilizationRate(assetBorrow, assetSupply)) .mul( - SafeMath.sub( - 10**20, - ProtocolLike(sovrynContractAddress).lendingFeePercent() - ) - ) + SafeMath.sub(10**20, ProtocolLike(sovrynContractAddress).lendingFeePercent()) + ) .div(10**40); } } @@ -1598,7 +1595,8 @@ contract LoanTokenLogicStandard is LoanTokenLogicStorage { uint256 interestFeePercent; (, , interestOwedPerDay, interestUnPaid, interestFeePercent, ) = ProtocolLike( sovrynContractAddress - ).getLenderInterestData(address(this), loanTokenAddress); + ) + .getLenderInterestData(address(this), loanTokenAddress); interestUnPaid = interestUnPaid.mul(SafeMath.sub(10**20, interestFeePercent)).div(10**20); } @@ -1657,12 +1655,13 @@ contract LoanTokenLogicStandard is LoanTokenLogicStorage { * */ function checkPause(string memory funcId) public view returns (bool isPaused) { bytes4 sig = bytes4(keccak256(abi.encodePacked(funcId))); - bytes32 slot = keccak256( - abi.encodePacked( - sig, - uint256(0xd46a704bc285dbd6ff5ad3863506260b1df02812f4f857c8cc852317a6ac64f2) - ) - ); + bytes32 slot = + keccak256( + abi.encodePacked( + sig, + uint256(0xd46a704bc285dbd6ff5ad3863506260b1df02812f4f857c8cc852317a6ac64f2) + ) + ); assembly { isPaused := sload(slot) } @@ -1676,12 +1675,13 @@ contract LoanTokenLogicStandard is LoanTokenLogicStorage { * */ function _checkPause() internal view { /// keccak256("iToken_FunctionPause") - bytes32 slot = keccak256( - abi.encodePacked( - msg.sig, - uint256(0xd46a704bc285dbd6ff5ad3863506260b1df02812f4f857c8cc852317a6ac64f2) - ) - ); + bytes32 slot = + keccak256( + abi.encodePacked( + msg.sig, + uint256(0xd46a704bc285dbd6ff5ad3863506260b1df02812f4f857c8cc852317a6ac64f2) + ) + ); bool isPaused; assembly { isPaused := sload(slot) @@ -1757,10 +1757,11 @@ contract LoanTokenLogicStandard is LoanTokenLogicStorage { } function _burnFromLM(uint256 burnAmount) internal returns (uint256) { - uint256 balanceOnLM = ILiquidityMining(liquidityMiningAddress).getUserPoolTokenBalance( - address(this), - msg.sender - ); + uint256 balanceOnLM = + ILiquidityMining(liquidityMiningAddress).getUserPoolTokenBalance( + address(this), + msg.sender + ); require(balanceOnLM.add(balanceOf(msg.sender)) >= burnAmount, "not enough balance"); if (balanceOnLM > 0) { diff --git a/contracts/governance/Staking/Staking.sol b/contracts/governance/Staking/Staking.sol index 4afc2ef3d..40b868829 100644 --- a/contracts/governance/Staking/Staking.sol +++ b/contracts/governance/Staking/Staking.sol @@ -438,7 +438,8 @@ contract Staking is return userStakingCheckpoints[account][lockDate][ numUserStakingCheckpoints[account][lockDate] - 1 - ].stake; + ] + .stake; } /** @@ -515,14 +516,14 @@ contract Staking is * the chainId in case it changes, and the address that the * contract is deployed at. * */ - bytes32 domainSeparator = keccak256( - abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(name)), getChainId(), address(this)) - ); + bytes32 domainSeparator = + keccak256( + abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(name)), getChainId(), address(this)) + ); /// @dev GovernorAlpha uses BALLOT_TYPEHASH, while Staking uses DELEGATION_TYPEHASH - bytes32 structHash = keccak256( - abi.encode(DELEGATION_TYPEHASH, delegatee, lockDate, nonce, expiry) - ); + bytes32 structHash = + keccak256(abi.encode(DELEGATION_TYPEHASH, delegatee, lockDate, nonce, expiry)); bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); address signatory = ecrecover(digest, v, r, s); @@ -768,8 +769,9 @@ contract Staking is function _notSameBlockAsStakingCheckpoint(uint256 lockDate) internal view { uint32 nCheckpoints = numUserStakingCheckpoints[msg.sender][lockDate]; - bool notSameBlock = userStakingCheckpoints[msg.sender][lockDate][nCheckpoints - 1] - .fromBlock != block.number; + bool notSameBlock = + userStakingCheckpoints[msg.sender][lockDate][nCheckpoints - 1].fromBlock != + block.number; require(notSameBlock, "S20"); //S20 : "cannot be mined in the same block as last stake" } } diff --git a/contracts/governance/Vesting/VestingRegistryLogic.sol b/contracts/governance/Vesting/VestingRegistryLogic.sol index eb1bfc42b..dca87e39c 100644 --- a/contracts/governance/Vesting/VestingRegistryLogic.sol +++ b/contracts/governance/Vesting/VestingRegistryLogic.sol @@ -118,17 +118,18 @@ contract VestingRegistryLogic is VestingRegistryStorage { for (uint256 i = 0; i < _tokenOwners.length; i++) { require(_tokenOwners[i] != address(0), "token owner cannot be 0 address"); require(_vestingAddresses[i] != address(0), "vesting cannot be 0 address"); - uint256 uid = uint256( - keccak256( - abi.encodePacked( - _tokenOwners[i], - uint256(VestingType.Vesting), - cliff, - duration, - vestingCreationType + uint256 uid = + uint256( + keccak256( + abi.encodePacked( + _tokenOwners[i], + uint256(VestingType.Vesting), + cliff, + duration, + vestingCreationType + ) ) - ) - ); + ); vestings[uid] = Vesting( uint256(VestingType.Vesting), vestingCreationType, @@ -172,13 +173,14 @@ contract VestingRegistryLogic is VestingRegistryStorage { uint256 _duration, uint256 _vestingCreationType ) public onlyAuthorized { - address vesting = _getOrCreateVesting( - _tokenOwner, - _cliff, - _duration, - uint256(VestingType.Vesting), - _vestingCreationType - ); + address vesting = + _getOrCreateVesting( + _tokenOwner, + _cliff, + _duration, + uint256(VestingType.Vesting), + _vestingCreationType + ); emit VestingCreated( _tokenOwner, vesting, @@ -204,13 +206,14 @@ contract VestingRegistryLogic is VestingRegistryStorage { uint256 _duration, uint256 _vestingCreationType ) external onlyAuthorized { - address vesting = _getOrCreateVesting( - _tokenOwner, - _cliff, - _duration, - uint256(VestingType.TeamVesting), - _vestingCreationType - ); + address vesting = + _getOrCreateVesting( + _tokenOwner, + _cliff, + _duration, + uint256(VestingType.TeamVesting), + _vestingCreationType + ); emit TeamVestingCreated( _tokenOwner, vesting, @@ -257,11 +260,12 @@ contract VestingRegistryLogic is VestingRegistryStorage { uint256 _vestingCreationType ) public view returns (address) { uint256 type_ = uint256(VestingType.Vesting); - uint256 uid = uint256( - keccak256( - abi.encodePacked(_tokenOwner, type_, _cliff, _duration, _vestingCreationType) - ) - ); + uint256 uid = + uint256( + keccak256( + abi.encodePacked(_tokenOwner, type_, _cliff, _duration, _vestingCreationType) + ) + ); return vestings[uid].vestingAddress; } @@ -275,11 +279,12 @@ contract VestingRegistryLogic is VestingRegistryStorage { uint256 _vestingCreationType ) public view returns (address) { uint256 type_ = uint256(VestingType.TeamVesting); - uint256 uid = uint256( - keccak256( - abi.encodePacked(_tokenOwner, type_, _cliff, _duration, _vestingCreationType) - ) - ); + uint256 uid = + uint256( + keccak256( + abi.encodePacked(_tokenOwner, type_, _cliff, _duration, _vestingCreationType) + ) + ); return vestings[uid].vestingAddress; } @@ -299,11 +304,12 @@ contract VestingRegistryLogic is VestingRegistryStorage { uint256 _vestingCreationType ) internal returns (address) { address vesting; - uint256 uid = uint256( - keccak256( - abi.encodePacked(_tokenOwner, _type, _cliff, _duration, _vestingCreationType) - ) - ); + uint256 uid = + uint256( + keccak256( + abi.encodePacked(_tokenOwner, _type, _cliff, _duration, _vestingCreationType) + ) + ); if (vestings[uid].vestingAddress == address(0)) { if (_type == 1) { vesting = vestingFactory.deployVesting( diff --git a/contracts/modules/LoanMaintenance.sol b/contracts/modules/LoanMaintenance.sol index 8e993393b..d0ea0d6e7 100644 --- a/contracts/modules/LoanMaintenance.sol +++ b/contracts/modules/LoanMaintenance.sol @@ -139,10 +139,11 @@ contract LoanMaintenance is vaultEtherDeposit(msg.sender, msg.value); } - (uint256 collateralToLoanRate, ) = IPriceFeeds(priceFeeds).queryRate( - loanParamsLocal.collateralToken, - loanParamsLocal.loanToken - ); + (uint256 collateralToLoanRate, ) = + IPriceFeeds(priceFeeds).queryRate( + loanParamsLocal.collateralToken, + loanParamsLocal.loanToken + ); emit DepositCollateral(loanId, depositAmount, collateralToLoanRate); } @@ -171,13 +172,14 @@ contract LoanMaintenance is "unauthorized" ); - uint256 maxDrawdown = IPriceFeeds(priceFeeds).getMaxDrawdown( - loanParamsLocal.loanToken, - loanParamsLocal.collateralToken, - loanLocal.principal, - loanLocal.collateral, - loanParamsLocal.maintenanceMargin - ); + uint256 maxDrawdown = + IPriceFeeds(priceFeeds).getMaxDrawdown( + loanParamsLocal.loanToken, + loanParamsLocal.collateralToken, + loanLocal.principal, + loanLocal.collateral, + loanParamsLocal.maintenanceMargin + ); if (withdrawAmount > maxDrawdown) { actualWithdrawAmount = maxDrawdown; @@ -303,7 +305,9 @@ contract LoanMaintenance is lenderInterest[loanLocal.lender][loanParamsLocal.loanToken].owedTotal = lenderInterest[ loanLocal.lender - ][loanParamsLocal.loanToken].owedTotal.add(depositAmount); + ][loanParamsLocal.loanToken] + .owedTotal + .add(depositAmount); } /** @@ -346,11 +350,10 @@ contract LoanMaintenance is block.timestamp ); - uint256 interestDepositRemaining = loanLocal - .endTimestamp - .sub(block.timestamp) - .mul(loanInterestLocal.owedPerDay) - .div(86400); + uint256 interestDepositRemaining = + loanLocal.endTimestamp.sub(block.timestamp).mul(loanInterestLocal.owedPerDay).div( + 86400 + ); require(withdrawAmount < interestDepositRemaining, "withdraw amount too high"); /// Withdraw interest. @@ -377,7 +380,9 @@ contract LoanMaintenance is lenderInterest[loanLocal.lender][loanParamsLocal.loanToken].owedTotal = lenderInterest[ loanLocal.lender - ][loanParamsLocal.loanToken].owedTotal.sub(withdrawAmount); + ][loanParamsLocal.loanToken] + .owedTotal + .sub(withdrawAmount); } /** @@ -482,9 +487,8 @@ contract LoanMaintenance is bool isLender, bool unsafeOnly ) external view returns (LoanReturnData[] memory loansData) { - EnumerableBytes32Set.Bytes32Set storage set = isLender - ? lenderLoanSets[user] - : borrowerLoanSets[user]; + EnumerableBytes32Set.Bytes32Set storage set = + isLender ? lenderLoanSets[user] : borrowerLoanSets[user]; uint256 end = start.add(count).min256(set.length()); if (start >= end) { @@ -497,11 +501,12 @@ contract LoanMaintenance is if (itemCount == count) { break; } - LoanReturnData memory loanData = _getLoan( - set.get(i + start - 1), /// loanId - loanType, - unsafeOnly - ); + LoanReturnData memory loanData = + _getLoan( + set.get(i + start - 1), /// loanId + loanType, + unsafeOnly + ); if (loanData.loanId == 0) continue; loansData[itemCount] = loanData; @@ -540,9 +545,8 @@ contract LoanMaintenance is bool isLender, bool unsafeOnly ) external view returns (LoanReturnDataV2[] memory loansDataV2) { - EnumerableBytes32Set.Bytes32Set storage set = isLender - ? lenderLoanSets[user] - : borrowerLoanSets[user]; + EnumerableBytes32Set.Bytes32Set storage set = + isLender ? lenderLoanSets[user] : borrowerLoanSets[user]; uint256 end = start.add(count).min256(set.length()); if (start >= end) { @@ -555,11 +559,12 @@ contract LoanMaintenance is if (itemCount == count) { break; } - LoanReturnDataV2 memory loanDataV2 = _getLoanV2( - set.get(i + start - 1), /// loanId - loanType, - unsafeOnly - ); + LoanReturnDataV2 memory loanDataV2 = + _getLoanV2( + set.get(i + start - 1), /// loanId + loanType, + unsafeOnly + ); if (loanDataV2.loanId == 0) continue; loansDataV2[itemCount] = loanDataV2; @@ -634,11 +639,12 @@ contract LoanMaintenance is if (itemCount == count) { break; } - LoanReturnData memory loanData = _getLoan( - activeLoansSet.get(i + start - 1), /// loanId - 0, /// loanType - unsafeOnly - ); + LoanReturnData memory loanData = + _getLoan( + activeLoansSet.get(i + start - 1), /// loanId + 0, /// loanType + unsafeOnly + ); if (loanData.loanId == 0) continue; loansData[itemCount] = loanData; @@ -680,11 +686,12 @@ contract LoanMaintenance is if (itemCount == count) { break; } - LoanReturnDataV2 memory loanDataV2 = _getLoanV2( - activeLoansSet.get(i + start - 1), /// loanId - 0, /// loanType - unsafeOnly - ); + LoanReturnDataV2 memory loanDataV2 = + _getLoanV2( + activeLoansSet.get(i + start - 1), /// loanId + 0, /// loanType + unsafeOnly + ); if (loanDataV2.loanId == 0) continue; loansDataV2[itemCount] = loanDataV2; @@ -729,8 +736,8 @@ contract LoanMaintenance is LoanInterest memory loanInterestLocal = loanInterest[loanId]; - (uint256 currentMargin, uint256 collateralToLoanRate) = IPriceFeeds(priceFeeds) - .getCurrentMargin( + (uint256 currentMargin, uint256 collateralToLoanRate) = + IPriceFeeds(priceFeeds).getCurrentMargin( loanParamsLocal.loanToken, loanParamsLocal.collateralToken, loanLocal.principal, @@ -808,8 +815,8 @@ contract LoanMaintenance is LoanInterest memory loanInterestLocal = loanInterest[loanId]; - (uint256 currentMargin, uint256 collateralToLoanRate) = IPriceFeeds(priceFeeds) - .getCurrentMargin( + (uint256 currentMargin, uint256 collateralToLoanRate) = + IPriceFeeds(priceFeeds).getCurrentMargin( loanParamsLocal.loanToken, loanParamsLocal.collateralToken, loanLocal.principal, @@ -871,26 +878,28 @@ contract LoanMaintenance is uint256 depositAmount ) internal returns (uint256 purchasedLoanToken) { /// Reverts in _loanSwap if amountNeeded can't be bought. - (uint256 destTokenAmountReceived, uint256 sourceTokenAmountUsed, ) = _loanSwap( - loanLocal.id, - loanParamsLocal.collateralToken, - loanParamsLocal.loanToken, - loanLocal.borrower, - loanLocal.collateral, /// minSourceTokenAmount - 0, /// maxSourceTokenAmount (0 means minSourceTokenAmount) - depositAmount, /// requiredDestTokenAmount (partial spend of loanLocal.collateral to fill this amount) - true, /// bypassFee - "" /// loanDataBytes - ); + (uint256 destTokenAmountReceived, uint256 sourceTokenAmountUsed, ) = + _loanSwap( + loanLocal.id, + loanParamsLocal.collateralToken, + loanParamsLocal.loanToken, + loanLocal.borrower, + loanLocal.collateral, /// minSourceTokenAmount + 0, /// maxSourceTokenAmount (0 means minSourceTokenAmount) + depositAmount, /// requiredDestTokenAmount (partial spend of loanLocal.collateral to fill this amount) + true, /// bypassFee + "" /// loanDataBytes + ); loanLocal.collateral = loanLocal.collateral.sub(sourceTokenAmountUsed); /// Ensure the loan is still healthy. - (uint256 currentMargin, ) = IPriceFeeds(priceFeeds).getCurrentMargin( - loanParamsLocal.loanToken, - loanParamsLocal.collateralToken, - loanLocal.principal, - loanLocal.collateral - ); + (uint256 currentMargin, ) = + IPriceFeeds(priceFeeds).getCurrentMargin( + loanParamsLocal.loanToken, + loanParamsLocal.collateralToken, + loanLocal.principal, + loanLocal.collateral + ); require(currentMargin > loanParamsLocal.maintenanceMargin, "unhealthy position"); return destTokenAmountReceived; diff --git a/scripts/contractInteraction/mainnet_contracts.json b/scripts/contractInteraction/mainnet_contracts.json index f839a272f..427abcfd1 100644 --- a/scripts/contractInteraction/mainnet_contracts.json +++ b/scripts/contractInteraction/mainnet_contracts.json @@ -127,4 +127,4 @@ "FastBTCBiDi": "0x0d5006330289336EBDf9D0ac9e0674F91B4851EA", "MyntMarketMaker": "0x722935ff8a99d801d802bb3ee528408c11c18656", "MyntController": "0xB576658700D32CCE28552349bCD52FaD8173ae32" -} \ No newline at end of file +} diff --git a/scripts/contractInteraction/testnet_contracts.json b/scripts/contractInteraction/testnet_contracts.json index 964baf6b1..1481034e5 100644 --- a/scripts/contractInteraction/testnet_contracts.json +++ b/scripts/contractInteraction/testnet_contracts.json @@ -112,4 +112,4 @@ "VestingRegistryProxy": "0x09e8659B6d204C6b1bED2BFF8E3F43F834A5Bbc4", "VestingRegistryLogic": "0xc9F6D4723Ed9AFc52FE07c083690e39e4505ED73", "SovrynSwapFormula": "0x7FF1C363b5600834bce7c514B01109eF1c103507" -} \ No newline at end of file +} From f695b410cea0c6322b1b83673190284d4af7e8b1 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Tue, 26 Apr 2022 17:11:14 +0530 Subject: [PATCH 53/86] Added addresses --- scripts/contractInteraction/testnet_contracts.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/contractInteraction/testnet_contracts.json b/scripts/contractInteraction/testnet_contracts.json index 1481034e5..17194037f 100644 --- a/scripts/contractInteraction/testnet_contracts.json +++ b/scripts/contractInteraction/testnet_contracts.json @@ -86,6 +86,8 @@ "VestingRegistry2": "0x068fbb3Bef062C3daBA7a4B12f53Cd614FBcBF1d", "VestingRegistry3": "0x52E4419b9D33C6e0ceb2e7c01D3aA1a04b21668C", "VestingLogic": "0xc1cECAC06c7a5d5480F158043A150acf06e206cD", + "FourYearVestingLogic": "0x051D4F2ad54114Ec3b003f308B09640e39D362B1", + "FourYearVestingFactory": "0x914563Aca50218a070431f866803628F19E8CCdD", "OriginInvestorsClaim": "0x9FBe4Bf89521088F790a4dD2F3e495B4f0dA7F42", "TokenSender": "0x4D1903BaAd894Fc6Ff70483d8518Db78F163F9ff", "RBTCWrapperProxy": "0x6b1a4735b1E25ccE9406B2d5D7417cE53d1cf90e", @@ -112,4 +114,4 @@ "VestingRegistryProxy": "0x09e8659B6d204C6b1bED2BFF8E3F43F834A5Bbc4", "VestingRegistryLogic": "0xc9F6D4723Ed9AFc52FE07c083690e39e4505ED73", "SovrynSwapFormula": "0x7FF1C363b5600834bce7c514B01109eF1c103507" -} +} \ No newline at end of file From 4ee730387b0ce852ac72ab68172327878cc6e005 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Tue, 26 Apr 2022 17:11:52 +0530 Subject: [PATCH 54/86] Ran prettier --- scripts/contractInteraction/testnet_contracts.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/contractInteraction/testnet_contracts.json b/scripts/contractInteraction/testnet_contracts.json index 17194037f..54772f635 100644 --- a/scripts/contractInteraction/testnet_contracts.json +++ b/scripts/contractInteraction/testnet_contracts.json @@ -114,4 +114,4 @@ "VestingRegistryProxy": "0x09e8659B6d204C6b1bED2BFF8E3F43F834A5Bbc4", "VestingRegistryLogic": "0xc9F6D4723Ed9AFc52FE07c083690e39e4505ED73", "SovrynSwapFormula": "0x7FF1C363b5600834bce7c514B01109eF1c103507" -} \ No newline at end of file +} From 2e5f4cd4a77dc4328da1ec2fe4aaf46306ee006a Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Tue, 26 Apr 2022 17:39:25 +0530 Subject: [PATCH 55/86] Updating tests --- tests/vesting/FourYearVesting.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index 6c339be0a..bcb3fc2d6 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -743,14 +743,14 @@ contract("FourYearVesting", (accounts) => { await expectRevert( staking.governanceWithdrawVesting(vesting.address, root, { from: a1 }), - "unauthorized" + "WS01" ); }); it("shouldn't be possible to use governanceWithdraw by user", async () => { await expectRevert( staking.governanceWithdraw(100, kickoffTS.toNumber() + 52 * WEEK, root), - "unauthorized" + "S07" ); }); }); @@ -847,7 +847,7 @@ contract("FourYearVesting", (accounts) => { vesting = await VestingLogic.at(vesting.address); await expectRevert( vesting.migrateToNewStakingContract(), - "there is no new staking contract set" + "S19" ); }); From dc147883012e4a217f131fb2570dfa8d4b73d341 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Tue, 26 Apr 2022 17:40:17 +0530 Subject: [PATCH 56/86] Ran prettier --- tests/vesting/FourYearVesting.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index bcb3fc2d6..eae18c722 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -845,10 +845,7 @@ contract("FourYearVesting", (accounts) => { feeSharingProxy.address ); vesting = await VestingLogic.at(vesting.address); - await expectRevert( - vesting.migrateToNewStakingContract(), - "S19" - ); + await expectRevert(vesting.migrateToNewStakingContract(), "S19"); }); it("should fail if the caller is neither owner nor token owner", async () => { From c4e68ee3d35e02b750f6cf8d5dd92b72f3112198 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Mon, 2 May 2022 17:15:21 +0530 Subject: [PATCH 57/86] Fixed cron script --- scripts/fouryearvesting/extendStakingCron.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/scripts/fouryearvesting/extendStakingCron.py b/scripts/fouryearvesting/extendStakingCron.py index 2bf8ef451..106f31347 100644 --- a/scripts/fouryearvesting/extendStakingCron.py +++ b/scripts/fouryearvesting/extendStakingCron.py @@ -4,6 +4,7 @@ import json import csv import time +import math def main(): global contracts, acct @@ -58,14 +59,17 @@ def readLockDate(timestamp): owner=acct) startDate = fourYearVestingLogic.startDate() print('startDate:', startDate) - timeLockDate = readLockDate(time.time()) + datenow = time.time() + timeLockDate = readLockDate(datenow) print('timeLockDate:', timeLockDate) DAY = 24 * 60 * 60 FOUR_WEEKS = 4 * 7 * DAY result = ((timeLockDate - startDate) % FOUR_WEEKS) # the cron should run every four weeks from start date newResult = ((timeLockDate - startDate) / FOUR_WEEKS) # the cron should run for first 13 intervals = 52 weeks + timediff = datenow - timeLockDate # To avoid execution on consecutive weeks print('result:', result) - print('newResult:', newResult) + print('newResult:', math.floor(newResult)) + print('timediff:', timediff) print('-----------------------------------------------------') - if ((result == 0) and (newResult <= 13)): + if ((result == 0) and (math.floor(newResult) >= 1) and (math.floor(newResult) <= 13) and (timediff < 86400) ): fourYearVestingLogic.extendStaking() \ No newline at end of file From e825b25bcfd6184d2a4d3d568ac39ba5760afc67 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Wed, 11 May 2022 19:25:08 +0530 Subject: [PATCH 58/86] Modified to lower case - CLIFF/DURATION --- .../Vesting/fouryear/FourYearVestingLogic.sol | 10 +++++----- .../Vesting/fouryear/FourYearVestingStorage.sol | 5 +++-- scripts/fouryearvesting/create_four_year_vestings.py | 4 ++-- tests/vesting/FourYearVesting.js | 8 ++++---- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol index 61e1fce29..48ab9bcee 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol @@ -102,7 +102,7 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro /// @dev Withdraw for each unlocked position. /// @dev Don't change FOUR_WEEKS to TWO_WEEKS, a lot of vestings already deployed with FOUR_WEEKS /// workaround found, but it doesn't work with TWO_WEEKS - for (uint256 i = startDate.add(CLIFF); i <= stakingEndDate; i += FOUR_WEEKS) { + for (uint256 i = startDate.add(cliff); i <= stakingEndDate; i += FOUR_WEEKS) { staking.delegate(_delegatee, i); } emit VotesDelegated(msg.sender, _delegatee); @@ -233,8 +233,8 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro // Runs for maxInterval only (consider maxInterval = 18 * 4 = 72 weeks) if (startDate == 0 && _restartStakeSchedule == 0) { startDate = staking.timestampToLockDate(block.timestamp); // Set only once - durationLeft = DURATION; // We do not touch duration and cliff as they are used throughout - cliffAdded = CLIFF; // Hence, durationLeft and cliffAdded is created + durationLeft = duration; // We do not touch duration and cliff as they are used throughout + cliffAdded = cliff; // Hence, durationLeft and cliffAdded is created } // Calling the _stakeTokens second/third time - we start from the end of previous interval // and the remaining amount(amount left after tokens are staked in the previous interval) @@ -249,7 +249,7 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro } // Runs only once when the _stakeTokens is called for the first time if (endDate == 0) { - endDate = staking.timestampToLockDate(block.timestamp.add(DURATION)); + endDate = staking.timestampToLockDate(block.timestamp.add(duration)); } uint256 addedMaxInterval = restartDate.add(maxInterval); // run for maxInterval if (addedMaxInterval < endDate) { @@ -276,7 +276,7 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro staking.stakesBySchedule( relativeAmount, cliffAdded, - DURATION.sub(durationLeft), + duration.sub(durationLeft), FOUR_WEEKS, address(this), tokenOwner diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol b/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol index 8daa632d7..f46731500 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol @@ -26,11 +26,12 @@ contract FourYearVestingStorage is Ownable { /// @notice Fee sharing Proxy. IFeeSharingProxy public feeSharingProxy; + // Used lower case for cliff and duration to maintain consistency with normal vesting /// @notice The cliff. After this time period the tokens begin to unlock. - uint256 public constant CLIFF = 4 weeks; + uint256 public constant cliff = 4 weeks; /// @notice The duration. After this period all tokens will have been unlocked. - uint256 public constant DURATION = 156 weeks; + uint256 public constant duration = 156 weeks; /// @notice The start date of the vesting. uint256 public startDate; diff --git a/scripts/fouryearvesting/create_four_year_vestings.py b/scripts/fouryearvesting/create_four_year_vestings.py index b31593693..cc822d038 100644 --- a/scripts/fouryearvesting/create_four_year_vestings.py +++ b/scripts/fouryearvesting/create_four_year_vestings.py @@ -40,8 +40,8 @@ def main(): MULTIPLIER = 10**16 # Expecting two decimals DAY = 24 * 60 * 60 FOUR_WEEKS = 4 * 7 * DAY - CLIFF = FOUR_WEEKS - DURATION = 39 * FOUR_WEEKS + cliff = FOUR_WEEKS + duration = 39 * FOUR_WEEKS balanceBefore = acct.balance() diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index eae18c722..5f1c7f44c 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -77,8 +77,8 @@ contract("FourYearVesting", (accounts) => { let _sov = await vestingInstance.SOV(); let _stackingAddress = await vestingInstance.staking(); let _tokenOwner = await vestingInstance.tokenOwner(); - let _cliff = await vestingInstance.CLIFF(); - let _duration = await vestingInstance.DURATION(); + let _cliff = await vestingInstance.cliff(); + let _duration = await vestingInstance.duration(); let _feeSharingProxy = await vestingInstance.feeSharingProxy(); assert.equal(_sov, token.address); @@ -105,8 +105,8 @@ contract("FourYearVesting", (accounts) => { let _sov = await vestingInstance.SOV(); let _stackingAddress = await vestingInstance.staking(); let _tokenOwner = await vestingInstance.tokenOwner(); - let _cliff = await vestingInstance.CLIFF(); - let _duration = await vestingInstance.DURATION(); + let _cliff = await vestingInstance.cliff(); + let _duration = await vestingInstance.duration(); let _feeSharingProxy = await vestingInstance.feeSharingProxy(); assert.equal(_sov, token.address); From 1eacec19d3f01d37222f135e0cec546ac9b7da5a Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Thu, 12 May 2022 01:28:40 +0530 Subject: [PATCH 59/86] Flexible duration for extending unlocked tokens --- .../Vesting/fouryear/FourYearVesting.sol | 6 +- .../fouryear/FourYearVestingFactory.sol | 24 ++-- .../Vesting/fouryear/FourYearVestingLogic.sol | 17 +-- .../fouryear/FourYearVestingStorage.sol | 3 + .../fouryear/IFourYearVestingFactory.sol | 3 +- .../create_four_year_vestings.py | 3 +- scripts/fouryearvesting/extendStakingCron.py | 8 +- .../fouryearvesting/fouryearvestinglist.csv | 10 +- tests/vesting/FourYearVesting.js | 112 ++++++++++++------ 9 files changed, 123 insertions(+), 63 deletions(-) diff --git a/contracts/governance/Vesting/fouryear/FourYearVesting.sol b/contracts/governance/Vesting/fouryear/FourYearVesting.sol index 8f6efce13..b7decf470 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVesting.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVesting.sol @@ -26,13 +26,15 @@ contract FourYearVesting is FourYearVestingStorage, UpgradableProxy { * @param _SOV The SOV token address. * @param _tokenOwner The owner of the tokens. * @param _feeSharingProxy Fee sharing proxy address. + * @param _extendDurationTill Duration till the unlocked tokens are extended. * */ constructor( address _logic, address _SOV, address _stakingAddress, address _tokenOwner, - address _feeSharingProxy + address _feeSharingProxy, + uint256 _extendDurationTill ) public { require(Address.isContract(_logic), "_logic not a contract"); require(_SOV != address(0), "SOV address invalid"); @@ -42,6 +44,7 @@ contract FourYearVesting is FourYearVestingStorage, UpgradableProxy { require(_tokenOwner != address(0), "token owner address invalid"); require(_feeSharingProxy != address(0), "feeSharingProxy address invalid"); require(Address.isContract(_feeSharingProxy), "_feeSharingProxy not a contract"); + require((_extendDurationTill % FOUR_WEEKS) == 0, "invalid duration"); _setImplementation(_logic); SOV = IERC20(_SOV); @@ -49,6 +52,7 @@ contract FourYearVesting is FourYearVestingStorage, UpgradableProxy { tokenOwner = _tokenOwner; feeSharingProxy = IFeeSharingProxy(_feeSharingProxy); maxInterval = 18 * FOUR_WEEKS; + extendDurationTill = _extendDurationTill; } /** diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol b/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol index d2e60a08d..7469ec49a 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol @@ -22,6 +22,7 @@ contract FourYearVestingFactory is IFourYearVestingFactory, Ownable { * @param _vestingOwnerMultisig The address of an owner of vesting contract. * @dev _vestingOwnerMultisig should ALWAYS be multisig. * @param _fourYearVestingLogic The implementation contract. + * @param _extendDurationTill Duration till the unlocked tokens are extended. * @return The four year vesting contract address. * */ function deployFourYearVesting( @@ -30,18 +31,19 @@ contract FourYearVestingFactory is IFourYearVestingFactory, Ownable { address _tokenOwner, address _feeSharing, address _vestingOwnerMultisig, - address _fourYearVestingLogic + address _fourYearVestingLogic, + uint256 _extendDurationTill ) external onlyOwner returns (address) { - address fourYearVesting = - address( - new FourYearVesting( - _fourYearVestingLogic, - _SOV, - _staking, - _tokenOwner, - _feeSharing - ) - ); + address fourYearVesting = address( + new FourYearVesting( + _fourYearVestingLogic, + _SOV, + _staking, + _tokenOwner, + _feeSharing, + _extendDurationTill + ) + ); Ownable(fourYearVesting).transferOwnership(_vestingOwnerMultisig); emit FourYearVestingCreated(_tokenOwner, fourYearVesting); return fourYearVesting; diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol index 48ab9bcee..df46aa1b4 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol @@ -183,20 +183,20 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro } /** - * @notice Extends first year stakes for four year vesting contracts. + * @notice Extends stakes(unlocked till timeDuration) for four year vesting contracts. * @dev Tokens are vested for 4 years. Since the max staking - * period is 3 years and the tokens are unlocked only after the first year is - * passed, hence, we extend the duration of staking for all unlocked tokens for the first - * year by 3 years. + * period is 3 years and the tokens are unlocked only after the first year(timeDuration) is + * passed, hence, we usually extend the duration of staking for all unlocked tokens for the first + * year by 3 years. In some cases, the timeDuration can differ. * */ function extendStaking() external { - uint256 oneYear = startDate.add(52 weeks); + uint256 timeDuration = startDate.add(extendDurationTill); uint256[] memory dates; uint96[] memory stakes; (dates, stakes) = staking.getStakes(address(this)); for (uint256 i = 0; i < dates.length; i++) { - if ((dates[i] < block.timestamp) && (dates[i] <= oneYear) && (stakes[i] > 0)) { + if ((dates[i] < block.timestamp) && (dates[i] <= timeDuration) && (stakes[i] > 0)) { staking.extendStakingDuration(dates[i], dates[i].add(156 weeks)); endDate = dates[i].add(156 weeks); } else { @@ -320,8 +320,9 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro /// @dev Don't change FOUR_WEEKS to TWO_WEEKS, a lot of vestings already deployed with FOUR_WEEKS /// workaround found, but it doesn't work with TWO_WEEKS /// @dev For four year vesting, withdrawal of stakes for the first year is not allowed. These - /// stakes are extended for three years. - for (uint256 i = startDate.add(52 weeks); i <= end; i += FOUR_WEEKS) { + /// stakes are extended for three years. In some cases the withdrawal may be allowed at a different + /// time and hence we use extendDurationTill. + for (uint256 i = startDate.add(extendDurationTill); i <= end; i += FOUR_WEEKS) { /// @dev Read amount to withdraw. stake = staking.getPriorUserStakeByDate(address(this), i, block.number.sub(1)); diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol b/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol index f46731500..cad5da865 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol @@ -63,6 +63,9 @@ contract FourYearVestingStorage is Ownable { /// @notice Address of new implementation. address public newImplementation; + /// @notice Duration(from start) till the time unlocked tokens are extended(for 3 years) + uint256 public extendDurationTill; + /// @dev Please add new state variables below this line. Mark them internal and /// add a getter function while upgrading the contracts. } diff --git a/contracts/governance/Vesting/fouryear/IFourYearVestingFactory.sol b/contracts/governance/Vesting/fouryear/IFourYearVestingFactory.sol index a645960c3..6ac3ee671 100644 --- a/contracts/governance/Vesting/fouryear/IFourYearVestingFactory.sol +++ b/contracts/governance/Vesting/fouryear/IFourYearVestingFactory.sol @@ -14,6 +14,7 @@ interface IFourYearVestingFactory { address _tokenOwner, address _feeSharing, address _vestingOwnerMultisig, - address _fourYearVestingLogic + address _fourYearVestingLogic, + uint256 _extendDurationTill ) external returns (address); } diff --git a/scripts/fouryearvesting/create_four_year_vestings.py b/scripts/fouryearvesting/create_four_year_vestings.py index cc822d038..9bcb4d949 100644 --- a/scripts/fouryearvesting/create_four_year_vestings.py +++ b/scripts/fouryearvesting/create_four_year_vestings.py @@ -57,7 +57,8 @@ def main(): tokenOwner = row[0].replace(" ", "") amount = row[1].replace(",", "").replace(".", "") amount = int(amount) * MULTIPLIER - tx = fourYearVestingFactory.deployFourYearVesting(SOVtoken.address, stakingAddress, tokenOwner, feeSharingAddress, multisig, fourYearVestingLogic) + extendDurationTill = row[2].replace(" ", "") + tx = fourYearVestingFactory.deployFourYearVesting(SOVtoken.address, stakingAddress, tokenOwner, feeSharingAddress, multisig, fourYearVestingLogic, extendDurationTill) event = tx.events["FourYearVestingCreated"] vestingAddress = event["vestingAddress"] print("=======================================") diff --git a/scripts/fouryearvesting/extendStakingCron.py b/scripts/fouryearvesting/extendStakingCron.py index 106f31347..10b8b5228 100644 --- a/scripts/fouryearvesting/extendStakingCron.py +++ b/scripts/fouryearvesting/extendStakingCron.py @@ -62,14 +62,18 @@ def readLockDate(timestamp): datenow = time.time() timeLockDate = readLockDate(datenow) print('timeLockDate:', timeLockDate) + extendDurationTill = fourYearVestingLogic.extendDurationTill() + print('extendDurationTill:', extendDurationTill) + maxIterations = extendDurationTill / FOUR_WEEKS + print('maxIterations:', maxIterations) DAY = 24 * 60 * 60 FOUR_WEEKS = 4 * 7 * DAY result = ((timeLockDate - startDate) % FOUR_WEEKS) # the cron should run every four weeks from start date - newResult = ((timeLockDate - startDate) / FOUR_WEEKS) # the cron should run for first 13 intervals = 52 weeks + newResult = ((timeLockDate - startDate) / FOUR_WEEKS) # the cron should run for maxIterations only timediff = datenow - timeLockDate # To avoid execution on consecutive weeks print('result:', result) print('newResult:', math.floor(newResult)) print('timediff:', timediff) print('-----------------------------------------------------') - if ((result == 0) and (math.floor(newResult) >= 1) and (math.floor(newResult) <= 13) and (timediff < 86400) ): + if ((result == 0) and (math.floor(newResult) >= 1) and (math.floor(newResult) <= maxIterations) and (timediff < 86400) ): fourYearVestingLogic.extendStaking() \ No newline at end of file diff --git a/scripts/fouryearvesting/fouryearvestinglist.csv b/scripts/fouryearvesting/fouryearvestinglist.csv index d90aff64a..7431b3247 100644 --- a/scripts/fouryearvesting/fouryearvestinglist.csv +++ b/scripts/fouryearvesting/fouryearvestinglist.csv @@ -1,5 +1,5 @@ -0x511893483DCc1A9A98f153ec8298b63BE010A99f,39.00 -0x8517ecCe55F6d11e4a60eDDbB7F4090dd2aC19E7,78.00 -0xC6138eC6c65e3280e720d4E6da0FD91A061351c4,117.00 -0x49d99a6821B7569f8D409728eD1D3EF5bF1befa9,156.00 -0xBf151ed43445a093E991211d46A34f60F02a9E09,195.00 +0x511893483DCc1A9A98f153ec8298b63BE010A99f,39.00,21772800 +0x8517ecCe55F6d11e4a60eDDbB7F4090dd2aC19E7,78.00,24192000 +0xC6138eC6c65e3280e720d4E6da0FD91A061351c4,117.00,26611200 +0x49d99a6821B7569f8D409728eD1D3EF5bF1befa9,156.00,29030400 +0xBf151ed43445a093E991211d46A34f60F02a9E09,195.00,31449600 diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index 5f1c7f44c..5696be2c4 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -69,7 +69,8 @@ contract("FourYearVesting", (accounts) => { a1, feeSharingProxy.address, root, - vestingLogic.address + vestingLogic.address, + 52 * WEEK ); vestingInstance = await VestingLogic.at(vestingInstance.logs[0].address); @@ -80,6 +81,7 @@ contract("FourYearVesting", (accounts) => { let _cliff = await vestingInstance.cliff(); let _duration = await vestingInstance.duration(); let _feeSharingProxy = await vestingInstance.feeSharingProxy(); + let _extendDurationTill = await vestingInstance.extendDurationTill(); assert.equal(_sov, token.address); assert.equal(_stackingAddress, staking.address); @@ -87,6 +89,7 @@ contract("FourYearVesting", (accounts) => { assert.equal(_cliff.toString(), cliff); assert.equal(_duration.toString(), duration); assert.equal(_feeSharingProxy, feeSharingProxy.address); + assert.equal(_extendDurationTill, 52 * WEEK); }); }); @@ -97,7 +100,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, root, - feeSharingProxy.address + feeSharingProxy.address, + 52 * WEEK ); vestingInstance = await VestingLogic.at(vestingInstance.address); @@ -108,6 +112,7 @@ contract("FourYearVesting", (accounts) => { let _cliff = await vestingInstance.cliff(); let _duration = await vestingInstance.duration(); let _feeSharingProxy = await vestingInstance.feeSharingProxy(); + let _extendDurationTill = await vestingInstance.extendDurationTill(); assert.equal(_sov, token.address); assert.equal(_stackingAddress, staking.address); @@ -115,6 +120,7 @@ contract("FourYearVesting", (accounts) => { assert.equal(_cliff.toString(), cliff); assert.equal(_duration.toString(), duration); assert.equal(_feeSharingProxy, feeSharingProxy.address); + assert.equal(_extendDurationTill, 52 * WEEK); }); it("fails if the 0 address is passed as SOV address", async () => { @@ -124,7 +130,8 @@ contract("FourYearVesting", (accounts) => { constants.ZERO_ADDRESS, staking.address, root, - feeSharingProxy.address + feeSharingProxy.address, + 52 * WEEK ), "SOV address invalid" ); @@ -137,7 +144,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, constants.ZERO_ADDRESS, - feeSharingProxy.address + feeSharingProxy.address, + 52 * WEEK ), "token owner address invalid" ); @@ -150,7 +158,8 @@ contract("FourYearVesting", (accounts) => { token.address, constants.ZERO_ADDRESS, root, - feeSharingProxy.address + feeSharingProxy.address, + 52 * WEEK ), "staking address invalid" ); @@ -163,7 +172,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, root, - constants.ZERO_ADDRESS + constants.ZERO_ADDRESS, + 52 * WEEK ), "feeSharingProxy address invalid" ); @@ -171,7 +181,8 @@ contract("FourYearVesting", (accounts) => { it("fails if logic is not a contract address", async () => { await expectRevert( - Vesting.new(a1, token.address, staking.address, a1, feeSharingProxy.address), + Vesting.new(a1, token.address, staking.address, a1, feeSharingProxy.address, + 52 * WEEK), "_logic not a contract" ); }); @@ -183,7 +194,8 @@ contract("FourYearVesting", (accounts) => { a1, staking.address, a1, - feeSharingProxy.address + feeSharingProxy.address, + 52 * WEEK ), "_SOV not a contract" ); @@ -191,17 +203,25 @@ contract("FourYearVesting", (accounts) => { it("fails if staking address is not a contract address", async () => { await expectRevert( - Vesting.new(vestingLogic.address, token.address, a1, a1, feeSharingProxy.address), + Vesting.new(vestingLogic.address, token.address, a1, a1, feeSharingProxy.address, + 52 * WEEK), "_stakingAddress not a contract" ); }); it("fails if fee sharing is not a contract address", async () => { await expectRevert( - Vesting.new(vestingLogic.address, token.address, staking.address, a1, a1), + Vesting.new(vestingLogic.address, token.address, staking.address, a1, a1, 52 * WEEK), "_feeSharingProxy not a contract" ); }); + + it("fails if extendDurationTill is not rounding to month", async () => { + await expectRevert( + Vesting.new(vestingLogic.address, token.address, staking.address, a1, feeSharingProxy.address, 6 * WEEK), + "invalid duration" + ); + }); }); describe("delegate", () => { @@ -213,7 +233,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, a2, - feeSharingProxy.address + feeSharingProxy.address, + 52 * WEEK ); vesting = await VestingLogic.at(vesting.address); @@ -272,7 +293,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, root, - feeSharingProxy.address + feeSharingProxy.address, + 52 * WEEK ); vesting = await VestingLogic.at(vesting.address); await token.approve(vesting.address, ONE_MILLON); @@ -370,7 +392,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, root, - feeSharingProxy.address + feeSharingProxy.address, + 52 * WEEK ); vesting = await VestingLogic.at(vesting.address); @@ -401,7 +424,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, root, - feeSharingProxy.address + feeSharingProxy.address, + 52 * WEEK ); vesting = await VestingLogic.at(vesting.address); await expectRevert(vesting.stakeTokensWithApproval(root, amount, 0), "unauthorized"); @@ -416,7 +440,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, root, - feeSharingProxy.address + feeSharingProxy.address, + 52 * WEEK ); vesting = await VestingLogic.at(vesting.address); @@ -439,7 +464,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, root, - feeSharingProxy.address + feeSharingProxy.address, + 52 * WEEK ); vesting = await VestingLogic.at(vesting.address); @@ -479,7 +505,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, root, - feeSharingProxy.address + feeSharingProxy.address, + 52 * WEEK ); vesting = await VestingLogic.at(vesting.address); @@ -552,7 +579,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, root, - feeSharingProxy.address + feeSharingProxy.address, + 52 * WEEK ); vesting = await VestingLogic.at(vesting.address); @@ -601,7 +629,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, root, - feeSharingProxy.address + feeSharingProxy.address, + 52 * WEEK ); vesting = await VestingLogic.at(vesting.address); @@ -644,7 +673,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, root, - feeSharingProxy.address + feeSharingProxy.address, + 52 * WEEK ); vesting = await VestingLogic.at(vesting.address); @@ -685,7 +715,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, a1, - feeSharingProxy.address + feeSharingProxy.address, + 52 * WEEK ); vesting = await VestingLogic.at(vesting.address); @@ -728,7 +759,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, root, - feeSharingProxy.address + feeSharingProxy.address, + 52 * WEEK ); vesting = await VestingLogic.at(vesting.address); @@ -763,7 +795,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, a1, - feeSharingProxy.address + feeSharingProxy.address, + 52 * WEEK ); vesting = await VestingLogic.at(vesting.address); await expectRevert( @@ -811,7 +844,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, a1, - feeSharingProxy.address + feeSharingProxy.address, + 52 * WEEK ); vesting = await VestingLogic.at(vesting.address); // 1. set new staking contract address on staking contract @@ -842,7 +876,8 @@ contract("FourYearVesting", (accounts) => { token.address, newStaking.address, a1, - feeSharingProxy.address + feeSharingProxy.address, + 52 * WEEK ); vesting = await VestingLogic.at(vesting.address); await expectRevert(vesting.migrateToNewStakingContract(), "S19"); @@ -858,7 +893,8 @@ contract("FourYearVesting", (accounts) => { token.address, newStaking.address, a1, - feeSharingProxy.address + feeSharingProxy.address, + 52 * WEEK ); vesting = await VestingLogic.at(vesting.address); @@ -880,7 +916,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, root, - feeSharingProxy.address + feeSharingProxy.address, + 52 * WEEK ); vesting = await VestingLogic.at(vesting.address); await token.approve(vesting.address, 1000); @@ -896,7 +933,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, root, - feeSharingProxy.address + feeSharingProxy.address, + 52 * WEEK ); vesting = await VestingLogic.at(vesting.address); await token.approve(vesting.address, ONE_MILLON); @@ -922,7 +960,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, root, - feeSharingProxy.address + feeSharingProxy.address, + 52 * WEEK ); vesting = await VestingLogic.at(vesting.address); await token.approve(vesting.address, ONE_MILLON); @@ -1004,7 +1043,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, a2, - feeSharingProxy.address + feeSharingProxy.address, + 52 * WEEK ); vesting = await VestingLogic.at(vesting.address); let maxIntervalOld = await vesting.maxInterval(); @@ -1019,7 +1059,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, a2, - feeSharingProxy.address + feeSharingProxy.address, + 52 * WEEK ); vesting = await VestingLogic.at(vesting.address); await expectRevert(vesting.setMaxInterval(7 * WEEK), "invalid interval"); @@ -1033,7 +1074,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, root, - feeSharingProxy.address + feeSharingProxy.address, + 52 * WEEK ); vesting = await VestingLogic.at(vesting.address); await token.approve(vesting.address, ONE_MILLON); @@ -1077,7 +1119,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, a1, - feeSharingProxy.address + feeSharingProxy.address, + 52 * WEEK ); vesting = await VestingLogic.at(vesting.address); await expectRevert(vesting.changeTokenOwner(a2, { from: a1 }), "unauthorized"); @@ -1129,7 +1172,8 @@ contract("FourYearVesting", (accounts) => { token.address, staking.address, a1, - feeSharingProxy.address + feeSharingProxy.address, + 52 * WEEK ); vesting = await VestingLogic.at(vestingObject.address); newVestingLogic = await NewVestingLogic.new(); From 5cfed8fac1952317d431fd9541d2b2d172cc79a0 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Thu, 12 May 2022 01:29:56 +0530 Subject: [PATCH 60/86] Ran Prettier --- .../fouryear/FourYearVestingFactory.sol | 21 ++++----- tests/vesting/FourYearVesting.js | 44 +++++++++++++++---- 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol b/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol index 7469ec49a..dc9b7840f 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol @@ -34,16 +34,17 @@ contract FourYearVestingFactory is IFourYearVestingFactory, Ownable { address _fourYearVestingLogic, uint256 _extendDurationTill ) external onlyOwner returns (address) { - address fourYearVesting = address( - new FourYearVesting( - _fourYearVestingLogic, - _SOV, - _staking, - _tokenOwner, - _feeSharing, - _extendDurationTill - ) - ); + address fourYearVesting = + address( + new FourYearVesting( + _fourYearVestingLogic, + _SOV, + _staking, + _tokenOwner, + _feeSharing, + _extendDurationTill + ) + ); Ownable(fourYearVesting).transferOwnership(_vestingOwnerMultisig); emit FourYearVestingCreated(_tokenOwner, fourYearVesting); return fourYearVesting; diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index 5696be2c4..835f20c88 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -131,7 +131,7 @@ contract("FourYearVesting", (accounts) => { staking.address, root, feeSharingProxy.address, - 52 * WEEK + 52 * WEEK ), "SOV address invalid" ); @@ -145,7 +145,7 @@ contract("FourYearVesting", (accounts) => { staking.address, constants.ZERO_ADDRESS, feeSharingProxy.address, - 52 * WEEK + 52 * WEEK ), "token owner address invalid" ); @@ -181,8 +181,14 @@ contract("FourYearVesting", (accounts) => { it("fails if logic is not a contract address", async () => { await expectRevert( - Vesting.new(a1, token.address, staking.address, a1, feeSharingProxy.address, - 52 * WEEK), + Vesting.new( + a1, + token.address, + staking.address, + a1, + feeSharingProxy.address, + 52 * WEEK + ), "_logic not a contract" ); }); @@ -195,7 +201,7 @@ contract("FourYearVesting", (accounts) => { staking.address, a1, feeSharingProxy.address, - 52 * WEEK + 52 * WEEK ), "_SOV not a contract" ); @@ -203,22 +209,42 @@ contract("FourYearVesting", (accounts) => { it("fails if staking address is not a contract address", async () => { await expectRevert( - Vesting.new(vestingLogic.address, token.address, a1, a1, feeSharingProxy.address, - 52 * WEEK), + Vesting.new( + vestingLogic.address, + token.address, + a1, + a1, + feeSharingProxy.address, + 52 * WEEK + ), "_stakingAddress not a contract" ); }); it("fails if fee sharing is not a contract address", async () => { await expectRevert( - Vesting.new(vestingLogic.address, token.address, staking.address, a1, a1, 52 * WEEK), + Vesting.new( + vestingLogic.address, + token.address, + staking.address, + a1, + a1, + 52 * WEEK + ), "_feeSharingProxy not a contract" ); }); it("fails if extendDurationTill is not rounding to month", async () => { await expectRevert( - Vesting.new(vestingLogic.address, token.address, staking.address, a1, feeSharingProxy.address, 6 * WEEK), + Vesting.new( + vestingLogic.address, + token.address, + staking.address, + a1, + feeSharingProxy.address, + 6 * WEEK + ), "invalid duration" ); }); From e61e50735f34d7ab5004115b6d649f3727a73dfe Mon Sep 17 00:00:00 2001 From: Santo Sinar Pandean Date: Thu, 12 May 2022 09:07:30 +0700 Subject: [PATCH 61/86] initialize transfer ownership to governance script --- .../contract_interaction.py | 29 ++++++++++++- scripts/contractInteraction/governance.py | 7 +++- .../contractInteraction/liquidity_mining.py | 6 ++- scripts/contractInteraction/loan_tokens.py | 42 ++++++++++++++++++- .../mainnet_contracts.json | 8 ++++ scripts/contractInteraction/prices.py | 38 +++++++++++++++++ scripts/contractInteraction/protocol.py | 8 +++- .../contractInteraction/staking_vesting.py | 21 ++++++++++ 8 files changed, 154 insertions(+), 5 deletions(-) diff --git a/scripts/contractInteraction/contract_interaction.py b/scripts/contractInteraction/contract_interaction.py index a5b3dfc2c..d7e1432b9 100644 --- a/scripts/contractInteraction/contract_interaction.py +++ b/scripts/contractInteraction/contract_interaction.py @@ -48,4 +48,31 @@ def main(): # confirmWithMS(i) #missed = getMissedBalance() - #transferSOVtoLM(missed) \ No newline at end of file + #transferSOVtoLM(missed) + + # # ---------- Transfer ownership to gov ---------- + # # core protocol + # transferProtocolOwnershipToGovernance() + + # # loan token + # transferBeaconOwnershipToGovernance() + # transferLoanTokenOwnershipToGovernance() + + # # oracles + # transferOracleOwnershipToGovernance() + + # # LM + # transferLiquidityMiningOwnershipToGovernance() + + # # Governance + # # lockedSOV + # transferLockedSOVOwnershipToGovernance() + + # # Staking + # transferStakingOwnershipToGovernance() + + # # StakingRewards + # transferStakingRewardsOwnershipToGovernance() + + # # VestingRegistry + # transferVestingRegistryOwnershipToGovernance() \ No newline at end of file diff --git a/scripts/contractInteraction/governance.py b/scripts/contractInteraction/governance.py index 1a886b56d..7445aa675 100644 --- a/scripts/contractInteraction/governance.py +++ b/scripts/contractInteraction/governance.py @@ -20,4 +20,9 @@ def queueProposal(id): def executeProposal(id): governor = Contract.from_abi("GovernorAlpha", address=conf.contracts['GovernorOwner'], abi=GovernorAlpha.abi, owner=conf.acct) tx = governor.execute(id) - tx.info() \ No newline at end of file + tx.info() + +def transferLockedSOVOwnershipToGovernance(): + lockedSOV = Contract.from_abi("LockedSOV", address=conf.contracts["LockedSOV"], abi=LockedSOV.abi, owner=conf.acct) + # TODO: Need to check whether we need to remove the other admin or not + lockedSOV.addAdmin(conf.contracts['GovernorAdmin']) diff --git a/scripts/contractInteraction/liquidity_mining.py b/scripts/contractInteraction/liquidity_mining.py index e6468c5a9..0e2db2a88 100644 --- a/scripts/contractInteraction/liquidity_mining.py +++ b/scripts/contractInteraction/liquidity_mining.py @@ -117,4 +117,8 @@ def getMissedBalance(): lm = Contract.from_abi("LiquidityMining", address = conf.contracts['LiquidityMiningProxy'], abi = LiquidityMining.abi, owner = conf.acct) res = lm.getMissedBalance() print(res/1e18) - return res \ No newline at end of file + return res + +def transferLiquidityMiningOwnershipToGovernance(): + lm = Contract.from_abi("LiquidityMining", address = conf.contracts['LiquidityMiningProxy'], abi = LiquidityMining.abi, owner = conf.acct) + lm.transferOwnership(conf.contracts['GovernorOwner']) \ No newline at end of file diff --git a/scripts/contractInteraction/loan_tokens.py b/scripts/contractInteraction/loan_tokens.py index 7546ff8f8..5ab61e594 100644 --- a/scripts/contractInteraction/loan_tokens.py +++ b/scripts/contractInteraction/loan_tokens.py @@ -855,4 +855,44 @@ def replaceLoanTokenSettingsLowerAdmin(): loanTokenLogicBeaconWrbtc = Contract.from_abi("LoanTokenLogicBeacon", address=conf.contracts['LoanTokenLogicBeaconWrbtc'], abi=LoanTokenLogicBeacon.abi, owner=conf.acct) print("Registering Loan Protocol Settings Module to LoanTOkenLogicBeaconWrbtc") data = loanTokenLogicBeaconWrbtc.registerLoanTokenModule.encode_input(loanTokenSettingsLowerAdmin.address) - sendWithMultisig(conf.contracts['multisig'], loanTokenLogicBeaconWrbtc.address, data, conf.acct) \ No newline at end of file + sendWithMultisig(conf.contracts['multisig'], loanTokenLogicBeaconWrbtc.address, data, conf.acct) + +def transferBeaconOwnershipToGovernance(): + # transfer beacon LM + loanTokenLogicBeaconLM = Contract.from_abi("loanTokenLogicBeaconLM", address=conf.contracts['LoanTokenLogicBeaconLM'], abi=LoanTokenLogicBeacon.abi, owner=conf.acct) + loanTokenLogicBeaconLM.transferOwnership(conf.contracts['GovernorOwner']) + + # transfer beacon wrbtc + loanTokenLogicBeaconWrbtc = Contract.from_abi("loanTokenLogicBeaconWrbtc", address=conf.contracts['LoanTokenLogicBeaconWrbtc'], abi=LoanTokenLogicBeacon.abi, owner=conf.acct) + loanTokenLogicBeaconWrbtc.transferOwnership(conf.contracts['GovernorOwner']) + +def transferLoanTokenOwnershipToGovernance(): + # iDOC + loanToken = Contract.from_abi("loanToken", address=conf.contracts['iDOC'], abi=LoanTokenLogicStandard.abi, owner=conf.acct) + # Need to change the admin first before the owner + loanToken.setAdmin(conf.contracts['GovernorAdmin']) + loanToken.transferOwnership(conf.contracts['GovernorOwner']) + + # iRBTC + loanToken = Contract.from_abi("loanToken", address=conf.contracts['iRBTC'], abi=LoanTokenLogicStandard.abi, owner=conf.acct) + # Need to change the admin first before the owner + loanToken.setAdmin(conf.contracts['GovernorAdmin']) + loanToken.transferOwnership(conf.contracts['GovernorOwner']) + + # iXUSD + loanToken = Contract.from_abi("loanToken", address=conf.contracts['iXUSD'], abi=LoanTokenLogicStandard.abi, owner=conf.acct) + # Need to change the admin first before the owner + loanToken.setAdmin(conf.contracts['GovernorAdmin']) + loanToken.transferOwnership(conf.contracts['GovernorOwner']) + + # iUSDT + loanToken = Contract.from_abi("loanToken", address=conf.contracts['iUSDT'], abi=LoanTokenLogicStandard.abi, owner=conf.acct) + # Need to change the admin first before the owner + loanToken.setAdmin(conf.contracts['GovernorAdmin']) + loanToken.transferOwnership(conf.contracts['GovernorOwner']) + + # iBPro + loanToken = Contract.from_abi("loanToken", address=conf.contracts['iBPro'], abi=LoanTokenLogicStandard.abi, owner=conf.acct) + # Need to change the admin first before the owner + loanToken.setAdmin(conf.contracts['GovernorAdmin']) + loanToken.transferOwnership(conf.contracts['GovernorOwner']) \ No newline at end of file diff --git a/scripts/contractInteraction/mainnet_contracts.json b/scripts/contractInteraction/mainnet_contracts.json index bebcd4540..14f3c133d 100644 --- a/scripts/contractInteraction/mainnet_contracts.json +++ b/scripts/contractInteraction/mainnet_contracts.json @@ -75,6 +75,14 @@ "USDTPriceFeed": "0xed80ccde8baeff2dbfc70d3028a27e501fa0d7d5", "PriceFeedsMOC": "0x391fe8a92a7FC626A25F30E8c19B92bf8BE37FD3", "SOVPriceFeedOnProtocol": "0xA266aA67e2a25B0CCa460DEAfcacC81D17341a0D", + "MOCPriceFeeds": "0xfF7ffCC3d0952C0133D4568C87ef4DeC72E4FddF", + "SOVPriceFeeds": "0xA266aA67e2a25B0CCa460DEAfcacC81D17341a0D", + "ETHsPriceFeeds": "0x9C9E06a23EE640A20DaAEd58E69012bB0742A098", + "BNBsPriceFeeds": "0xfb9898367e61B2e0CCE6C529dD5307996fAbd155", + "XUSDPriceFeeds": "0xEd80Ccde8bAeFf2dBFC70d3028a27e501Fa0D7D5", + "FISHPriceFeeds": "0x95576a065fD880e6C6621dBfAB54FdB9f827C783", + "RIFPriceFeeds": "0x15e6B67d5bCd57232104A891f466578b28f447D9", + "MYNTPriceFeeds": "0x1C11180b6730661090634cfD9F2510a1acA26fAf", "CSOV1": "0x0106F2fFBF6A4f5DEcE323d20E16E2037E732790", "CSOV2": "0x7f7Dcf9DF951C4A332740e9a125720DA242A34ff", "governorVault": "0xC7A1637b37190a456b017897207bceb2A29f19b9", diff --git a/scripts/contractInteraction/prices.py b/scripts/contractInteraction/prices.py index 983d01ac8..9e3354348 100644 --- a/scripts/contractInteraction/prices.py +++ b/scripts/contractInteraction/prices.py @@ -179,3 +179,41 @@ def setV1SOVPoolOracleAddress(v1PoolOracleAddress): oracle = Contract.from_abi("PriceFeedV1PoolOracle", address= conf.contracts['SOVPriceFeedOnProtocol'], abi = PriceFeedV1PoolOracle.abi, owner = conf.acct) data = oracle.setV1PoolOracleAddress.encode_input(v1PoolOracleAddress) sendWithMultisig(conf.contracts['multisig'], oracle.address, data, conf.acct) + + +def transferOracleOwnershipToGovernance(): + # PriceFeeds (Gateway) + feeds = Contract.from_abi("PriceFeeds", address= conf.contracts['PriceFeeds'], abi = PriceFeeds.abi, owner = conf.acct) + feeds.transferOwnership(conf.contracts['GovernorAdmin']) + + # MOC PriceFeeds + tokenFeeds = Contract.from_abi("PriceFeedV1PoolOracle", address=conf.contracts['MOCPriceFeeds'] , abi = PriceFeedV1PoolOracle.abi, owner = conf.acct) + tokenFeeds.transferOwnership(conf.contracts['GovernorAdmin']) + + # SOV PriceFeeds + tokenFeeds = Contract.from_abi("PriceFeedV1PoolOracle", address=conf.contracts['SOVPriceFeeds'] , abi = PriceFeedV1PoolOracle.abi, owner = conf.acct) + tokenFeeds.transferOwnership(conf.contracts['GovernorAdmin']) + + # ETHs PriceFeeds + tokenFeeds = Contract.from_abi("PriceFeedV1PoolOracle", address=conf.contracts['ETHsPriceFeeds'] , abi = PriceFeedV1PoolOracle.abi, owner = conf.acct) + tokenFeeds.transferOwnership(conf.contracts['GovernorAdmin']) + + # BNBs PriceFeeds + tokenFeeds = Contract.from_abi("PriceFeedV1PoolOracle", address=conf.contracts['BNBsPriceFeeds'] , abi = PriceFeedV1PoolOracle.abi, owner = conf.acct) + tokenFeeds.transferOwnership(conf.contracts['GovernorAdmin']) + + # XUSD PriceFeeds + tokenFeeds = Contract.from_abi("PriceFeedV1PoolOracle", address=conf.contracts['XUSDPriceFeeds'] , abi = PriceFeedV1PoolOracle.abi, owner = conf.acct) + tokenFeeds.transferOwnership(conf.contracts['GovernorAdmin']) + + # FISH PriceFeeds + tokenFeeds = Contract.from_abi("PriceFeedV1PoolOracle", address=conf.contracts['FISHPriceFeeds'] , abi = PriceFeedV1PoolOracle.abi, owner = conf.acct) + tokenFeeds.transferOwnership(conf.contracts['GovernorAdmin']) + + # RIF PriceFeeds + tokenFeeds = Contract.from_abi("PriceFeedV1PoolOracle", address=conf.contracts['RIFPriceFeeds'] , abi = PriceFeedV1PoolOracle.abi, owner = conf.acct) + tokenFeeds.transferOwnership(conf.contracts['GovernorAdmin']) + + # MYNT PriceFeeds + tokenFeeds = Contract.from_abi("PriceFeedV1PoolOracle", address=conf.contracts['MYNTPriceFeeds'] , abi = PriceFeedV1PoolOracle.abi, owner = conf.acct) + tokenFeeds.transferOwnership(conf.contracts['GovernorAdmin']) \ No newline at end of file diff --git a/scripts/contractInteraction/protocol.py b/scripts/contractInteraction/protocol.py index 2ee31e678..26cddb596 100644 --- a/scripts/contractInteraction/protocol.py +++ b/scripts/contractInteraction/protocol.py @@ -664,4 +664,10 @@ def depositCollateral(loanId,depositAmount, tokenAddress): "sovryn", address=conf.contracts['sovrynProtocol'], abi=interface.ISovrynBrownie.abi, owner=conf.acct) if(token.allowance(conf.acct, sovryn.address) < depositAmount): token.approve(sovryn.address, depositAmount) - sovryn.depositCollateral(loanId,depositAmount) \ No newline at end of file + sovryn.depositCollateral(loanId,depositAmount) + +# Transferring Ownership to GOV +def transferProtocolOwnershipToGovernance(): + sovryn = Contract.from_abi( + "sovryn", address=conf.contracts['sovrynProtocol'], abi=interface.ISovrynBrownie.abi, owner=conf.acct) + sovryn.transferOwnership(conf.contracts['GovernorOwner']) \ No newline at end of file diff --git a/scripts/contractInteraction/staking_vesting.py b/scripts/contractInteraction/staking_vesting.py index a9729bf91..38eb143b4 100644 --- a/scripts/contractInteraction/staking_vesting.py +++ b/scripts/contractInteraction/staking_vesting.py @@ -331,3 +331,24 @@ def governanceWithdrawVesting( vesting, receiver): data = stakingProxy.governanceWithdrawVesting.encode_input( vesting, receiver) print(data) sendWithMultisig(conf.contracts['multisig'], conf.contracts['Staking'], data, conf.acct) + +def transferStakingOwnershipToGovernance(): + staking = Contract.from_abi("Staking", address=conf.contracts['Staking'], abi=Staking.abi, owner=conf.acct) + staking.addAdmin(conf.contracts['GovernorAdmin']) + +def transferStakingRewardsOwnershipToGovernance(): + stakingRewards = Contract.from_abi("StakingRewards", address=conf.contracts['StakingRewardsProxy'], abi=StakingRewards.abi, owner=conf.acct) + stakingRewards.transferOwnership(conf.contracts['GovernorAdmin']) + +def transferVestingRegistryOwnershipToGovernance(): + # add governor admin as admin + vestingRegistry = Contract.from_abi("VestingRegistry", address=conf.contracts['VestingRegistryProxy'], abi=VestingRegistry.abi, owner=conf.acct) + vestingRegistry.addAdmin(conf.contracts['GovernorAdmin']) + + ''' + # add Exchequer admin as admin + vestingRegistry.addAdmin(conf.contracts['GovernorAdmin']) + ''' + + # Transfer ownership to GovernorOwner + vestingRegistry.transferOwnership(conf.contracts['GovernorOwner']) \ No newline at end of file From 55db3f6d8a4b8bc322bfd8cc3d5f3e1cc62fb488 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Thu, 12 May 2022 12:49:44 +0530 Subject: [PATCH 62/86] Fixed failing tests --- tests/vesting/VestingRegistryMigrations.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/vesting/VestingRegistryMigrations.js b/tests/vesting/VestingRegistryMigrations.js index 2184dd36e..f7ab14d52 100644 --- a/tests/vesting/VestingRegistryMigrations.js +++ b/tests/vesting/VestingRegistryMigrations.js @@ -77,7 +77,8 @@ contract("VestingRegistryMigrations", (accounts) => { SOV.address, staking.address, account4, - feeSharingProxy.address + feeSharingProxy.address, + 13 * FOUR_WEEKS ); fourYearVesting = await FourYearVestingLogic.at(fourYearVesting.address); }); From 19fc6bcce0e7b3e8871360af9baf22c20b4dfd59 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Thu, 12 May 2022 12:50:50 +0530 Subject: [PATCH 63/86] Ran prettier --- tests/vesting/VestingRegistryMigrations.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/vesting/VestingRegistryMigrations.js b/tests/vesting/VestingRegistryMigrations.js index f7ab14d52..6ef674b5d 100644 --- a/tests/vesting/VestingRegistryMigrations.js +++ b/tests/vesting/VestingRegistryMigrations.js @@ -78,7 +78,7 @@ contract("VestingRegistryMigrations", (accounts) => { staking.address, account4, feeSharingProxy.address, - 13 * FOUR_WEEKS + 13 * FOUR_WEEKS ); fourYearVesting = await FourYearVestingLogic.at(fourYearVesting.address); }); From 2a30b1dde447d688d7e1834315a63d32b5a35f5e Mon Sep 17 00:00:00 2001 From: Santo Sinar Pandean Date: Thu, 12 May 2022 22:45:17 +0700 Subject: [PATCH 64/86] Transfer ownership requested from multisig Add additional information for each tx --- scripts/contractInteraction/governance.py | 4 +- .../contractInteraction/liquidity_mining.py | 4 +- scripts/contractInteraction/loan_tokens.py | 54 +++++++++++++---- .../mainnet_contracts.json | 15 ++--- scripts/contractInteraction/prices.py | 60 +++++++++++++++---- scripts/contractInteraction/protocol.py | 4 +- .../contractInteraction/staking_vesting.py | 20 +++++-- .../testnet_contracts.json | 9 +++ 8 files changed, 132 insertions(+), 38 deletions(-) diff --git a/scripts/contractInteraction/governance.py b/scripts/contractInteraction/governance.py index 7445aa675..7fd81365e 100644 --- a/scripts/contractInteraction/governance.py +++ b/scripts/contractInteraction/governance.py @@ -23,6 +23,8 @@ def executeProposal(id): tx.info() def transferLockedSOVOwnershipToGovernance(): + print("Add LockedSOV admin for address: ", conf.contracts['GovernorAdmin']) lockedSOV = Contract.from_abi("LockedSOV", address=conf.contracts["LockedSOV"], abi=LockedSOV.abi, owner=conf.acct) # TODO: Need to check whether we need to remove the other admin or not - lockedSOV.addAdmin(conf.contracts['GovernorAdmin']) + data = lockedSOV.addAdmin.encode_input(conf.contracts['GovernorAdmin']) + sendWithMultisig(conf.contracts['multisig'], lockedSOV.address, data, conf.acct) diff --git a/scripts/contractInteraction/liquidity_mining.py b/scripts/contractInteraction/liquidity_mining.py index 0e2db2a88..ef0774661 100644 --- a/scripts/contractInteraction/liquidity_mining.py +++ b/scripts/contractInteraction/liquidity_mining.py @@ -120,5 +120,7 @@ def getMissedBalance(): return res def transferLiquidityMiningOwnershipToGovernance(): + print("Transferring LiquidityMining ownership to: ", conf.contracts['GovernorOwner']) lm = Contract.from_abi("LiquidityMining", address = conf.contracts['LiquidityMiningProxy'], abi = LiquidityMining.abi, owner = conf.acct) - lm.transferOwnership(conf.contracts['GovernorOwner']) \ No newline at end of file + data = lm.transferOwnership.encode_input(conf.contracts['GovernorOwner']) + sendWithMultisig(conf.contracts['multisig'], lm.address, data, conf.acct) \ No newline at end of file diff --git a/scripts/contractInteraction/loan_tokens.py b/scripts/contractInteraction/loan_tokens.py index 5ab61e594..ef229826c 100644 --- a/scripts/contractInteraction/loan_tokens.py +++ b/scripts/contractInteraction/loan_tokens.py @@ -859,40 +859,70 @@ def replaceLoanTokenSettingsLowerAdmin(): def transferBeaconOwnershipToGovernance(): # transfer beacon LM + print("Transferring beacon LM ownserhip to: ", conf.contracts['GovernorOwner']) loanTokenLogicBeaconLM = Contract.from_abi("loanTokenLogicBeaconLM", address=conf.contracts['LoanTokenLogicBeaconLM'], abi=LoanTokenLogicBeacon.abi, owner=conf.acct) - loanTokenLogicBeaconLM.transferOwnership(conf.contracts['GovernorOwner']) + data = loanTokenLogicBeaconLM.transferOwnership.encode_input(conf.contracts['GovernorOwner']) + sendWithMultisig(conf.contracts['multisig'], loanTokenLogicBeaconLM.address, data, conf.acct) # transfer beacon wrbtc + print("Transferring beacon WRBTC ownserhip to: ", conf.contracts['GovernorOwner']) loanTokenLogicBeaconWrbtc = Contract.from_abi("loanTokenLogicBeaconWrbtc", address=conf.contracts['LoanTokenLogicBeaconWrbtc'], abi=LoanTokenLogicBeacon.abi, owner=conf.acct) - loanTokenLogicBeaconWrbtc.transferOwnership(conf.contracts['GovernorOwner']) + data = loanTokenLogicBeaconWrbtc.transferOwnership.encode_input(conf.contracts['GovernorOwner']) + sendWithMultisig(conf.contracts['multisig'], loanTokenLogicBeaconWrbtc.address, data, conf.acct) def transferLoanTokenOwnershipToGovernance(): # iDOC + print("Transferring iDOC admin to: ", conf.contracts['GovernorAdmin']) loanToken = Contract.from_abi("loanToken", address=conf.contracts['iDOC'], abi=LoanTokenLogicStandard.abi, owner=conf.acct) # Need to change the admin first before the owner - loanToken.setAdmin(conf.contracts['GovernorAdmin']) - loanToken.transferOwnership(conf.contracts['GovernorOwner']) + data = loanToken.setAdmin.encode_input(conf.contracts['GovernorAdmin']) + sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) + + print("Transferring iDOC ownserhip to: ", conf.contracts['GovernorOwner']) + data = loanToken.transferOwnership.encode_input(conf.contracts['GovernorOwner']) + sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) # iRBTC + print("Transferring iRBTC admin to: ", conf.contracts['GovernorAdmin']) loanToken = Contract.from_abi("loanToken", address=conf.contracts['iRBTC'], abi=LoanTokenLogicStandard.abi, owner=conf.acct) # Need to change the admin first before the owner - loanToken.setAdmin(conf.contracts['GovernorAdmin']) - loanToken.transferOwnership(conf.contracts['GovernorOwner']) + data = loanToken.setAdmin.encode_input(conf.contracts['GovernorAdmin']) + sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) + + print("Transferring iRBTC ownserhip to: ", conf.contracts['GovernorOwner']) + data = loanToken.transferOwnership.encode_input(conf.contracts['GovernorOwner']) + sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) # iXUSD + print("Transferring iXUSD admin to: ", conf.contracts['GovernorAdmin']) loanToken = Contract.from_abi("loanToken", address=conf.contracts['iXUSD'], abi=LoanTokenLogicStandard.abi, owner=conf.acct) # Need to change the admin first before the owner - loanToken.setAdmin(conf.contracts['GovernorAdmin']) - loanToken.transferOwnership(conf.contracts['GovernorOwner']) + data = loanToken.setAdmin.encode_input(conf.contracts['GovernorAdmin']) + sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) + + print("Transferring iXUSD ownserhip to: ", conf.contracts['GovernorOwner']) + data = loanToken.transferOwnership.encode_input(conf.contracts['GovernorOwner']) + sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) # iUSDT + print("Transferring iUSDT admin to: ", conf.contracts['GovernorAdmin']) loanToken = Contract.from_abi("loanToken", address=conf.contracts['iUSDT'], abi=LoanTokenLogicStandard.abi, owner=conf.acct) # Need to change the admin first before the owner - loanToken.setAdmin(conf.contracts['GovernorAdmin']) - loanToken.transferOwnership(conf.contracts['GovernorOwner']) + data = loanToken.setAdmin.encode_input(conf.contracts['GovernorAdmin']) + sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) + + print("Transferring iUSDT ownserhip to: ", conf.contracts['GovernorOwner']) + data = loanToken.transferOwnership.encode_input(conf.contracts['GovernorOwner']) + sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) # iBPro + print("Transferring iBPro admin to: ", conf.contracts['GovernorAdmin']) loanToken = Contract.from_abi("loanToken", address=conf.contracts['iBPro'], abi=LoanTokenLogicStandard.abi, owner=conf.acct) # Need to change the admin first before the owner - loanToken.setAdmin(conf.contracts['GovernorAdmin']) - loanToken.transferOwnership(conf.contracts['GovernorOwner']) \ No newline at end of file + data = loanToken.setAdmin.encode_input(conf.contracts['GovernorAdmin']) + sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) + + print("Transferring iBPro ownserhip to: ", conf.contracts['GovernorOwner']) + data = loanToken.transferOwnership.encode_input(conf.contracts['GovernorOwner']) + sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) + diff --git a/scripts/contractInteraction/mainnet_contracts.json b/scripts/contractInteraction/mainnet_contracts.json index 14f3c133d..c20f59279 100644 --- a/scripts/contractInteraction/mainnet_contracts.json +++ b/scripts/contractInteraction/mainnet_contracts.json @@ -57,14 +57,14 @@ "(WR)BTC/RIF": "0xAE66117C8105a65D914fB47d37a127E879244319", "(WR)BTC/MYNT": "0x36263Ac99ecdCf1aB20513d580b7d8D32d3c439D", "LiquidityMiningConfigToken": "0x513B0f20027BDc6bc2fE9e28A9d4B40d20730Dca", - "FishPoolOracle": "0x95576a065fD880e6C6621dBfAB54FdB9f827C783", + "FishPoolOracle": "0x2D78E4dc352872Aa6ca12B40d18301Ff6DBE5ECB", "XUSDPoolOracle": "0xD08eDf687418dF0107bAbCc8Fcab9064F3A6fc05", - "ETHPoolOracle": "0x9C9E06a23EE640A20DaAEd58E69012bB0742A098", - "MOCPoolOracle": "0xfF7ffCC3d0952C0133D4568C87ef4DeC72E4FddF", - "BNBPoolOracle": "0x57B7B2feeA4ed576e899568F42dF272017E3d8CD", + "ETHPoolOracle": "0xD6CcacF91c3ca6705aD3047a55dC7760c5ADF1F4", + "MOCPoolOracle": "0x8Cda228A6870D7B3a9Fe1e42604b35F39Fbc5D59", + "BNBPoolOracle": "0x66464BED2420Ab2e7c2d06A9eEb67d06D7E10eAC", "SOVPoolOracle": "0x4290243b7F3aEF0F6922dAd4F9F8d321ee320fBd", - "RIFPoolOracle": "0x15e6B67d5bCd57232104A891f466578b28f447D9", - "MYNTPoolOracle": "0x1C11180b6730661090634cfD9F2510a1acA26fAf", + "RIFPoolOracle": "0x6C339235Bad0381C3bb6CaCecA68C941e81a4bDc", + "MYNTPoolOracle": "0x3f9fe40Cf3013d7B8E5517B166b909911d5AFcC2", "og": "0x81d25201D044f178883599Be1934FF53FDA98acD", "multisig": "0x924f5ad34698Fd20c90Fe5D5A8A0abd3b42dc711", "PriceFeeds": "0x437AC62769f386b2d238409B7f0a7596d36506e4", @@ -75,7 +75,8 @@ "USDTPriceFeed": "0xed80ccde8baeff2dbfc70d3028a27e501fa0d7d5", "PriceFeedsMOC": "0x391fe8a92a7FC626A25F30E8c19B92bf8BE37FD3", "SOVPriceFeedOnProtocol": "0xA266aA67e2a25B0CCa460DEAfcacC81D17341a0D", - "MOCPriceFeeds": "0xfF7ffCC3d0952C0133D4568C87ef4DeC72E4FddF", + "MOCPriceFeedsV1Pool": "0xfF7ffCC3d0952C0133D4568C87ef4DeC72E4FddF", + "BProPriceFeeds": "0x389e2447e1397A8e485D658a44D845a324A338Cf", "SOVPriceFeeds": "0xA266aA67e2a25B0CCa460DEAfcacC81D17341a0D", "ETHsPriceFeeds": "0x9C9E06a23EE640A20DaAEd58E69012bB0742A098", "BNBsPriceFeeds": "0xfb9898367e61B2e0CCE6C529dD5307996fAbd155", diff --git a/scripts/contractInteraction/prices.py b/scripts/contractInteraction/prices.py index 9e3354348..33b3c80b4 100644 --- a/scripts/contractInteraction/prices.py +++ b/scripts/contractInteraction/prices.py @@ -183,37 +183,75 @@ def setV1SOVPoolOracleAddress(v1PoolOracleAddress): def transferOracleOwnershipToGovernance(): # PriceFeeds (Gateway) + print("Transferring priceFeeds (gateway) ownership to: ", conf.contracts['GovernorAdmin']) feeds = Contract.from_abi("PriceFeeds", address= conf.contracts['PriceFeeds'], abi = PriceFeeds.abi, owner = conf.acct) - feeds.transferOwnership(conf.contracts['GovernorAdmin']) + data = feeds.transferOwnership.encode_input(conf.contracts['GovernorAdmin']) + sendWithMultisig(conf.contracts['multisig'], feeds.address, data, conf.acct) + + # BPRO PriceFeeds + print("Transferring BPro PriceFeeds ownership to: ", conf.contracts['GovernorAdmin']) + tokenFeeds = Contract.from_abi("BProPriceFeed", address=conf.contracts['BProPriceFeeds'] , abi = BProPriceFeed.abi, owner = conf.acct) + data = tokenFeeds.transferOwnership.encode_input(conf.contracts['GovernorAdmin']) + sendWithMultisig(conf.contracts['multisig'], tokenFeeds.address, data, conf.acct) + + # MOC PriceFeeds (external) + print("Transferring MOC (External) PriceFeeds ownership to: ", conf.contracts['GovernorAdmin']) + tokenFeeds = Contract.from_abi("PriceFeedsMoC", address = conf.contracts['PriceFeedsMOC'], abi = PriceFeedsMoC.abi, owner = conf.acct) + data = tokenFeeds.transferOwnership.encode_input(conf.contracts['GovernorAdmin']) + sendWithMultisig(conf.contracts['multisig'], tokenFeeds.address, data, conf.acct) + + # RSK PriceFeeds + print("Transferring RSK PriceFeeds ownership to: ", conf.contracts['GovernorAdmin']) + tokenFeeds = Contract.from_abi("PriceFeedRSKOracle", address=conf.contracts['PriceFeedRSKOracle'] , abi = PriceFeedRSKOracle.abi, owner = conf.acct) + data = tokenFeeds.transferOwnership.encode_input(conf.contracts['GovernorAdmin']) + sendWithMultisig(conf.contracts['multisig'], tokenFeeds.address, data, conf.acct) + + # --- V1Pool PriceFeeds --- - # MOC PriceFeeds - tokenFeeds = Contract.from_abi("PriceFeedV1PoolOracle", address=conf.contracts['MOCPriceFeeds'] , abi = PriceFeedV1PoolOracle.abi, owner = conf.acct) - tokenFeeds.transferOwnership(conf.contracts['GovernorAdmin']) + # MOC PriceFeeds V1Pool + print("Transferring MOC (V1Pool) PriceFeeds ownership to: ", conf.contracts['GovernorAdmin']) + tokenFeeds = Contract.from_abi("PriceFeedV1PoolOracle", address=conf.contracts['MOCPriceFeedsV1Pool'] , abi = PriceFeedV1PoolOracle.abi, owner = conf.acct) + data = tokenFeeds.transferOwnership.encode_input(conf.contracts['GovernorAdmin']) + sendWithMultisig(conf.contracts['multisig'], tokenFeeds.address, data, conf.acct) # SOV PriceFeeds + print("Transferring SOV PriceFeeds ownership to: ", conf.contracts['GovernorAdmin']) tokenFeeds = Contract.from_abi("PriceFeedV1PoolOracle", address=conf.contracts['SOVPriceFeeds'] , abi = PriceFeedV1PoolOracle.abi, owner = conf.acct) - tokenFeeds.transferOwnership(conf.contracts['GovernorAdmin']) + data = tokenFeeds.transferOwnership.encode_input(conf.contracts['GovernorAdmin']) + sendWithMultisig(conf.contracts['multisig'], tokenFeeds.address, data, conf.acct) # ETHs PriceFeeds + print("Transferring ETHs PriceFeeds ownership to: ", conf.contracts['GovernorAdmin']) tokenFeeds = Contract.from_abi("PriceFeedV1PoolOracle", address=conf.contracts['ETHsPriceFeeds'] , abi = PriceFeedV1PoolOracle.abi, owner = conf.acct) - tokenFeeds.transferOwnership(conf.contracts['GovernorAdmin']) + data = tokenFeeds.transferOwnership.encode_input(conf.contracts['GovernorAdmin']) + sendWithMultisig(conf.contracts['multisig'], tokenFeeds.address, data, conf.acct) # BNBs PriceFeeds + print("Transferring BNBs PriceFeeds ownership to: ", conf.contracts['GovernorAdmin']) tokenFeeds = Contract.from_abi("PriceFeedV1PoolOracle", address=conf.contracts['BNBsPriceFeeds'] , abi = PriceFeedV1PoolOracle.abi, owner = conf.acct) - tokenFeeds.transferOwnership(conf.contracts['GovernorAdmin']) + data = tokenFeeds.transferOwnership.encode_input(conf.contracts['GovernorAdmin']) + sendWithMultisig(conf.contracts['multisig'], tokenFeeds.address, data, conf.acct) # XUSD PriceFeeds + print("Transferring XUSD PriceFeeds ownership to: ", conf.contracts['GovernorAdmin']) tokenFeeds = Contract.from_abi("PriceFeedV1PoolOracle", address=conf.contracts['XUSDPriceFeeds'] , abi = PriceFeedV1PoolOracle.abi, owner = conf.acct) - tokenFeeds.transferOwnership(conf.contracts['GovernorAdmin']) + data = tokenFeeds.transferOwnership.encode_input(conf.contracts['GovernorAdmin']) + sendWithMultisig(conf.contracts['multisig'], tokenFeeds.address, data, conf.acct) # FISH PriceFeeds + print("Transferring FISH PriceFeeds ownership to: ", conf.contracts['GovernorAdmin']) tokenFeeds = Contract.from_abi("PriceFeedV1PoolOracle", address=conf.contracts['FISHPriceFeeds'] , abi = PriceFeedV1PoolOracle.abi, owner = conf.acct) - tokenFeeds.transferOwnership(conf.contracts['GovernorAdmin']) + data = tokenFeeds.transferOwnership.encode_input(conf.contracts['GovernorAdmin']) + sendWithMultisig(conf.contracts['multisig'], tokenFeeds.address, data, conf.acct) # RIF PriceFeeds + print("Transferring RIF PriceFeeds ownership to: ", conf.contracts['GovernorAdmin']) tokenFeeds = Contract.from_abi("PriceFeedV1PoolOracle", address=conf.contracts['RIFPriceFeeds'] , abi = PriceFeedV1PoolOracle.abi, owner = conf.acct) - tokenFeeds.transferOwnership(conf.contracts['GovernorAdmin']) + data = tokenFeeds.transferOwnership.encode_input(conf.contracts['GovernorAdmin']) + sendWithMultisig(conf.contracts['multisig'], tokenFeeds.address, data, conf.acct) # MYNT PriceFeeds + print("Transferring MYNT PriceFeeds ownership to: ", conf.contracts['GovernorAdmin']) tokenFeeds = Contract.from_abi("PriceFeedV1PoolOracle", address=conf.contracts['MYNTPriceFeeds'] , abi = PriceFeedV1PoolOracle.abi, owner = conf.acct) - tokenFeeds.transferOwnership(conf.contracts['GovernorAdmin']) \ No newline at end of file + data = tokenFeeds.transferOwnership.encode_input(conf.contracts['GovernorAdmin']) + sendWithMultisig(conf.contracts['multisig'], tokenFeeds.address, data, conf.acct) \ No newline at end of file diff --git a/scripts/contractInteraction/protocol.py b/scripts/contractInteraction/protocol.py index 26cddb596..4ca8ede66 100644 --- a/scripts/contractInteraction/protocol.py +++ b/scripts/contractInteraction/protocol.py @@ -668,6 +668,8 @@ def depositCollateral(loanId,depositAmount, tokenAddress): # Transferring Ownership to GOV def transferProtocolOwnershipToGovernance(): + print("Transferring sovryn protocol ownserhip to: ", conf.contracts['GovernorOwner']) sovryn = Contract.from_abi( "sovryn", address=conf.contracts['sovrynProtocol'], abi=interface.ISovrynBrownie.abi, owner=conf.acct) - sovryn.transferOwnership(conf.contracts['GovernorOwner']) \ No newline at end of file + data = sovryn.transferOwnership.encode_input(conf.contracts['GovernorOwner']) + sendWithMultisig(conf.contracts['multisig'], sovryn.address, data, conf.acct) \ No newline at end of file diff --git a/scripts/contractInteraction/staking_vesting.py b/scripts/contractInteraction/staking_vesting.py index 38eb143b4..d3a873189 100644 --- a/scripts/contractInteraction/staking_vesting.py +++ b/scripts/contractInteraction/staking_vesting.py @@ -333,22 +333,32 @@ def governanceWithdrawVesting( vesting, receiver): sendWithMultisig(conf.contracts['multisig'], conf.contracts['Staking'], data, conf.acct) def transferStakingOwnershipToGovernance(): + print("Add staking admin for address: ", conf.contracts['GovernorAdmin']) staking = Contract.from_abi("Staking", address=conf.contracts['Staking'], abi=Staking.abi, owner=conf.acct) - staking.addAdmin(conf.contracts['GovernorAdmin']) + data = staking.addAdmin.encode_input(conf.contracts['GovernorAdmin']) + sendWithMultisig(conf.contracts['multisig'], staking.address, data, conf.acct) def transferStakingRewardsOwnershipToGovernance(): + print("Transferring StakingRewards ownership to: ", conf.contracts['GovernorAdmin']) stakingRewards = Contract.from_abi("StakingRewards", address=conf.contracts['StakingRewardsProxy'], abi=StakingRewards.abi, owner=conf.acct) - stakingRewards.transferOwnership(conf.contracts['GovernorAdmin']) + data = stakingRewards.transferOwnership.encode_input(conf.contracts['GovernorAdmin']) + sendWithMultisig(conf.contracts['multisig'], stakingRewards.address, data, conf.acct) def transferVestingRegistryOwnershipToGovernance(): # add governor admin as admin + print("Add Vesting Registry admin for address: ", conf.contracts['GovernorAdmin']) vestingRegistry = Contract.from_abi("VestingRegistry", address=conf.contracts['VestingRegistryProxy'], abi=VestingRegistry.abi, owner=conf.acct) - vestingRegistry.addAdmin(conf.contracts['GovernorAdmin']) + data = vestingRegistry.addAdmin.encode_input(conf.contracts['GovernorAdmin']) + sendWithMultisig(conf.contracts['multisig'], vestingRegistry.address, data, conf.acct) ''' # add Exchequer admin as admin - vestingRegistry.addAdmin(conf.contracts['GovernorAdmin']) + print("Add Vesting Registry admin for multisig: ", conf.contracts['GovernorAdmin']) + data = vestingRegistry.addAdmin.encode_input(conf.contracts['multisig']) + sendWithMultisig(conf.contracts['multisig'], vestingRegistry.address, data, conf.acct) ''' # Transfer ownership to GovernorOwner - vestingRegistry.transferOwnership(conf.contracts['GovernorOwner']) \ No newline at end of file + print("Transferring Vesting Registry ownership to: ", conf.contracts['GovernorOwner']) + data = vestingRegistry.transferOwnership.encode_input(conf.contracts['GovernorOwner']) + sendWithMultisig(conf.contracts['multisig'], vestingRegistry.address, data, conf.acct) diff --git a/scripts/contractInteraction/testnet_contracts.json b/scripts/contractInteraction/testnet_contracts.json index 3280c502d..dda60b42a 100644 --- a/scripts/contractInteraction/testnet_contracts.json +++ b/scripts/contractInteraction/testnet_contracts.json @@ -68,6 +68,15 @@ "RSKOracle": "0xE00243Bc6912BF148302e8478996c98c22fE8739", "PriceFeedRSKOracle": "0xF2B7440C89431DF82EC6c8F3D079059847565dF0", "SOVPriceFeedOnProtocol": "0x0945E4d65Ad9AD7FB3d695c036CAdA63769079C7", + "MOCPriceFeedsV1Pool": "0xC672e85dCd5104A97f923D84be4a42DF07020064", + "BProPriceFeeds": "0xB7b9B10E04c36C3bB8Df34163331BBB0194d1DB4", + "SOVPriceFeeds": "0xe5289487d1Fa873e31da9d2f5821317Ce3c26462", + "ETHsPriceFeeds": "0xcb7A57AA05b01A4B028d0AfB04EeaaFD0D79e717", + "BNBsPriceFeeds": "0x92b4f54087aFB91BF420d6c7e6cF5C40f6Cc7C8B", + "XUSDPriceFeeds": "0x60e6040b9a5Bf47fE33558Fd70E6869bCA6B0BEC", + "FISHPriceFeeds": "0x0Bc059a8dC688fa12A959186305CDb07c4745925", + "RIFPriceFeeds": "0xcCC43720eEf00B9B7112849C78FE6000c8956abe", + "MYNTPriceFeeds": "0x61FA3Ea57787C02E82e6b9368ECe55ECD853d005", "CSOV1": "0x75bbf7f4d77777730eE35b94881B898113a93124", "CSOV2": "0x1dA260149ffee6fD4443590ee58F65b8dC2106B9", "governorVault": "0xE8276A1680CB970c2334B3201044Ddf7c492F52A", From 5332b4e84ae414b79424ca3f5a4b47912e45e717 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Fri, 13 May 2022 14:46:49 +0530 Subject: [PATCH 65/86] Modified variable name --- .../governance/Vesting/fouryear/FourYearVesting.sol | 8 ++++---- .../Vesting/fouryear/FourYearVestingFactory.sol | 6 +++--- .../Vesting/fouryear/FourYearVestingLogic.sol | 6 +++--- .../Vesting/fouryear/FourYearVestingStorage.sol | 2 +- .../Vesting/fouryear/IFourYearVestingFactory.sol | 2 +- scripts/fouryearvesting/create_four_year_vestings.py | 4 ++-- scripts/fouryearvesting/extendStakingCron.py | 6 +++--- tests/vesting/FourYearVesting.js | 10 +++++----- 8 files changed, 22 insertions(+), 22 deletions(-) diff --git a/contracts/governance/Vesting/fouryear/FourYearVesting.sol b/contracts/governance/Vesting/fouryear/FourYearVesting.sol index b7decf470..5dee260eb 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVesting.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVesting.sol @@ -26,7 +26,7 @@ contract FourYearVesting is FourYearVestingStorage, UpgradableProxy { * @param _SOV The SOV token address. * @param _tokenOwner The owner of the tokens. * @param _feeSharingProxy Fee sharing proxy address. - * @param _extendDurationTill Duration till the unlocked tokens are extended. + * @param _extendDurationFor Duration till the unlocked tokens are extended. * */ constructor( address _logic, @@ -34,7 +34,7 @@ contract FourYearVesting is FourYearVestingStorage, UpgradableProxy { address _stakingAddress, address _tokenOwner, address _feeSharingProxy, - uint256 _extendDurationTill + uint256 _extendDurationFor ) public { require(Address.isContract(_logic), "_logic not a contract"); require(_SOV != address(0), "SOV address invalid"); @@ -44,7 +44,7 @@ contract FourYearVesting is FourYearVestingStorage, UpgradableProxy { require(_tokenOwner != address(0), "token owner address invalid"); require(_feeSharingProxy != address(0), "feeSharingProxy address invalid"); require(Address.isContract(_feeSharingProxy), "_feeSharingProxy not a contract"); - require((_extendDurationTill % FOUR_WEEKS) == 0, "invalid duration"); + require((_extendDurationFor % FOUR_WEEKS) == 0, "invalid duration"); _setImplementation(_logic); SOV = IERC20(_SOV); @@ -52,7 +52,7 @@ contract FourYearVesting is FourYearVestingStorage, UpgradableProxy { tokenOwner = _tokenOwner; feeSharingProxy = IFeeSharingProxy(_feeSharingProxy); maxInterval = 18 * FOUR_WEEKS; - extendDurationTill = _extendDurationTill; + extendDurationFor = _extendDurationFor; } /** diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol b/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol index dc9b7840f..4bbef07fe 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol @@ -22,7 +22,7 @@ contract FourYearVestingFactory is IFourYearVestingFactory, Ownable { * @param _vestingOwnerMultisig The address of an owner of vesting contract. * @dev _vestingOwnerMultisig should ALWAYS be multisig. * @param _fourYearVestingLogic The implementation contract. - * @param _extendDurationTill Duration till the unlocked tokens are extended. + * @param _extendDurationFor Duration till the unlocked tokens are extended. * @return The four year vesting contract address. * */ function deployFourYearVesting( @@ -32,7 +32,7 @@ contract FourYearVestingFactory is IFourYearVestingFactory, Ownable { address _feeSharing, address _vestingOwnerMultisig, address _fourYearVestingLogic, - uint256 _extendDurationTill + uint256 _extendDurationFor ) external onlyOwner returns (address) { address fourYearVesting = address( @@ -42,7 +42,7 @@ contract FourYearVestingFactory is IFourYearVestingFactory, Ownable { _staking, _tokenOwner, _feeSharing, - _extendDurationTill + _extendDurationFor ) ); Ownable(fourYearVesting).transferOwnership(_vestingOwnerMultisig); diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol index df46aa1b4..d82039c8d 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol @@ -190,7 +190,7 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro * year by 3 years. In some cases, the timeDuration can differ. * */ function extendStaking() external { - uint256 timeDuration = startDate.add(extendDurationTill); + uint256 timeDuration = startDate.add(extendDurationFor); uint256[] memory dates; uint96[] memory stakes; (dates, stakes) = staking.getStakes(address(this)); @@ -321,8 +321,8 @@ contract FourYearVestingLogic is IFourYearVesting, FourYearVestingStorage, Appro /// workaround found, but it doesn't work with TWO_WEEKS /// @dev For four year vesting, withdrawal of stakes for the first year is not allowed. These /// stakes are extended for three years. In some cases the withdrawal may be allowed at a different - /// time and hence we use extendDurationTill. - for (uint256 i = startDate.add(extendDurationTill); i <= end; i += FOUR_WEEKS) { + /// time and hence we use extendDurationFor. + for (uint256 i = startDate.add(extendDurationFor); i <= end; i += FOUR_WEEKS) { /// @dev Read amount to withdraw. stake = staking.getPriorUserStakeByDate(address(this), i, block.number.sub(1)); diff --git a/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol b/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol index cad5da865..3339b0c5c 100644 --- a/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol +++ b/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol @@ -64,7 +64,7 @@ contract FourYearVestingStorage is Ownable { address public newImplementation; /// @notice Duration(from start) till the time unlocked tokens are extended(for 3 years) - uint256 public extendDurationTill; + uint256 public extendDurationFor; /// @dev Please add new state variables below this line. Mark them internal and /// add a getter function while upgrading the contracts. diff --git a/contracts/governance/Vesting/fouryear/IFourYearVestingFactory.sol b/contracts/governance/Vesting/fouryear/IFourYearVestingFactory.sol index 6ac3ee671..fd02295a4 100644 --- a/contracts/governance/Vesting/fouryear/IFourYearVestingFactory.sol +++ b/contracts/governance/Vesting/fouryear/IFourYearVestingFactory.sol @@ -15,6 +15,6 @@ interface IFourYearVestingFactory { address _feeSharing, address _vestingOwnerMultisig, address _fourYearVestingLogic, - uint256 _extendDurationTill + uint256 _extendDurationFor ) external returns (address); } diff --git a/scripts/fouryearvesting/create_four_year_vestings.py b/scripts/fouryearvesting/create_four_year_vestings.py index 9bcb4d949..42563b118 100644 --- a/scripts/fouryearvesting/create_four_year_vestings.py +++ b/scripts/fouryearvesting/create_four_year_vestings.py @@ -57,8 +57,8 @@ def main(): tokenOwner = row[0].replace(" ", "") amount = row[1].replace(",", "").replace(".", "") amount = int(amount) * MULTIPLIER - extendDurationTill = row[2].replace(" ", "") - tx = fourYearVestingFactory.deployFourYearVesting(SOVtoken.address, stakingAddress, tokenOwner, feeSharingAddress, multisig, fourYearVestingLogic, extendDurationTill) + extendDurationFor = row[2].replace(" ", "") + tx = fourYearVestingFactory.deployFourYearVesting(SOVtoken.address, stakingAddress, tokenOwner, feeSharingAddress, multisig, fourYearVestingLogic, extendDurationFor) event = tx.events["FourYearVestingCreated"] vestingAddress = event["vestingAddress"] print("=======================================") diff --git a/scripts/fouryearvesting/extendStakingCron.py b/scripts/fouryearvesting/extendStakingCron.py index 10b8b5228..ef0e84867 100644 --- a/scripts/fouryearvesting/extendStakingCron.py +++ b/scripts/fouryearvesting/extendStakingCron.py @@ -62,9 +62,9 @@ def readLockDate(timestamp): datenow = time.time() timeLockDate = readLockDate(datenow) print('timeLockDate:', timeLockDate) - extendDurationTill = fourYearVestingLogic.extendDurationTill() - print('extendDurationTill:', extendDurationTill) - maxIterations = extendDurationTill / FOUR_WEEKS + extendDurationFor = fourYearVestingLogic.extendDurationFor() + print('extendDurationFor:', extendDurationFor) + maxIterations = extendDurationFor / FOUR_WEEKS print('maxIterations:', maxIterations) DAY = 24 * 60 * 60 FOUR_WEEKS = 4 * 7 * DAY diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js index 835f20c88..a3c636cd7 100644 --- a/tests/vesting/FourYearVesting.js +++ b/tests/vesting/FourYearVesting.js @@ -81,7 +81,7 @@ contract("FourYearVesting", (accounts) => { let _cliff = await vestingInstance.cliff(); let _duration = await vestingInstance.duration(); let _feeSharingProxy = await vestingInstance.feeSharingProxy(); - let _extendDurationTill = await vestingInstance.extendDurationTill(); + let _extendDurationFor = await vestingInstance.extendDurationFor(); assert.equal(_sov, token.address); assert.equal(_stackingAddress, staking.address); @@ -89,7 +89,7 @@ contract("FourYearVesting", (accounts) => { assert.equal(_cliff.toString(), cliff); assert.equal(_duration.toString(), duration); assert.equal(_feeSharingProxy, feeSharingProxy.address); - assert.equal(_extendDurationTill, 52 * WEEK); + assert.equal(_extendDurationFor, 52 * WEEK); }); }); @@ -112,7 +112,7 @@ contract("FourYearVesting", (accounts) => { let _cliff = await vestingInstance.cliff(); let _duration = await vestingInstance.duration(); let _feeSharingProxy = await vestingInstance.feeSharingProxy(); - let _extendDurationTill = await vestingInstance.extendDurationTill(); + let _extendDurationFor = await vestingInstance.extendDurationFor(); assert.equal(_sov, token.address); assert.equal(_stackingAddress, staking.address); @@ -120,7 +120,7 @@ contract("FourYearVesting", (accounts) => { assert.equal(_cliff.toString(), cliff); assert.equal(_duration.toString(), duration); assert.equal(_feeSharingProxy, feeSharingProxy.address); - assert.equal(_extendDurationTill, 52 * WEEK); + assert.equal(_extendDurationFor, 52 * WEEK); }); it("fails if the 0 address is passed as SOV address", async () => { @@ -235,7 +235,7 @@ contract("FourYearVesting", (accounts) => { ); }); - it("fails if extendDurationTill is not rounding to month", async () => { + it("fails if extendDurationFor is not rounding to month", async () => { await expectRevert( Vesting.new( vestingLogic.address, From 03f4fed3b4cb5d82691aef70c3d06d7897318f79 Mon Sep 17 00:00:00 2001 From: Santo Sinar Pandean Date: Fri, 13 May 2022 22:44:58 +0700 Subject: [PATCH 66/86] Remove XUSD V1Pool from the script --- scripts/contractInteraction/prices.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/scripts/contractInteraction/prices.py b/scripts/contractInteraction/prices.py index 33b3c80b4..170df9dc8 100644 --- a/scripts/contractInteraction/prices.py +++ b/scripts/contractInteraction/prices.py @@ -232,12 +232,6 @@ def transferOracleOwnershipToGovernance(): data = tokenFeeds.transferOwnership.encode_input(conf.contracts['GovernorAdmin']) sendWithMultisig(conf.contracts['multisig'], tokenFeeds.address, data, conf.acct) - # XUSD PriceFeeds - print("Transferring XUSD PriceFeeds ownership to: ", conf.contracts['GovernorAdmin']) - tokenFeeds = Contract.from_abi("PriceFeedV1PoolOracle", address=conf.contracts['XUSDPriceFeeds'] , abi = PriceFeedV1PoolOracle.abi, owner = conf.acct) - data = tokenFeeds.transferOwnership.encode_input(conf.contracts['GovernorAdmin']) - sendWithMultisig(conf.contracts['multisig'], tokenFeeds.address, data, conf.acct) - # FISH PriceFeeds print("Transferring FISH PriceFeeds ownership to: ", conf.contracts['GovernorAdmin']) tokenFeeds = Contract.from_abi("PriceFeedV1PoolOracle", address=conf.contracts['FISHPriceFeeds'] , abi = PriceFeedV1PoolOracle.abi, owner = conf.acct) From a1c678f53c42a8314add1a1d4704a6c58f77c70e Mon Sep 17 00:00:00 2001 From: Tyrone Johnson <77393391+tjcloa@users.noreply.github.com> Date: Wed, 18 May 2022 01:55:07 +0400 Subject: [PATCH 67/86] Update testnet_contracts - XUSD converter --- scripts/contractInteraction/testnet_contracts.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/contractInteraction/testnet_contracts.json b/scripts/contractInteraction/testnet_contracts.json index 3280c502d..ba390b770 100644 --- a/scripts/contractInteraction/testnet_contracts.json +++ b/scripts/contractInteraction/testnet_contracts.json @@ -31,7 +31,7 @@ "ConverterBPRO": "", "ConverterUSDT": "0x133eBE9c8bA524C9B1B601E794dF527f390729bF", "ConverterSOV": "0xc2d05263318e2304fc7cdad40eea6a091b310080", - "ConverterXUSD": "0xe5e750ead0e564e489b0776273e4a10f3f3d4028", + "ConverterXUSD": "0xD877fd00ECF08eD78BF549fbc74bac3001aBBb07", "ConverterETHs": "0x9f570ffe6c421e2c7611aaea14770b807e9fb424", "ConverterMOC": "0x2cb88F02cCA4dddBE8C41a6920853838Ada09F8b", "ConverterBNBs": "0x20d5c55c92615d416d73b34c8afed99288e99be1", @@ -46,13 +46,13 @@ "(WR)BTC/BPRO2": "0xdaf6fd8370f5245d98e829c766e008cd39e8f060", "(WR)BTC/SOV": "0xdf298421cb18740a7059b0af532167faa45e7a98", "(WR)BTC/ETH": "0xBb5B900EDa0F1459F582aB2436EA825a927f5bA2", - "(WR)BTC/XUSD": "0x6601Ccd32342d644282e82Cb05A3Dd88964D18c1", + "(WR)BTC/XUSD": "0xb89D193c8a9Ae3fadF73B23519c215a0B7DD1B37", "(WR)BTC/FISH": "0xe41E262889f89b9a6331680606D9e9AabD01743e", "XUSD/BRZ": "0x7107E42f4b59310D217333B544465d428395Affe", "(WR)BTC/MYNT": "0xB12FA09a50c56e9a0C826b98e76DA7645017AB4D", "LiquidityMiningConfigToken": "0x0F1694aFEF2B25c1C069582F23Bca73608348F50", "SOVPoolOracle": "0x8A2a7F192DC39b70c2937C38559e704fcAB3F4CA", - "XUSDPoolOracle": "0xA30E5776c6Ae21E0CA28C6b4c39Fe7A9744d9a86", + "XUSDPoolOracle": "0xa888366a68179EeE27eDdfe1f68AAb829b03C9aA", "ETHPoolOracle": "0x9fDaA4E1AcFc243d29C5e2AE72fbC322a10C5530", "MOCPoolOracle": "0xA60d29C03452b858C4580725D5e9047982A9517a", "BNBPoolOracle": "0x73616dbc3A6fA63354d4dA0C3B74834D079BE46d", From 714da469a5de6daa2b5218f6ebf3cbfe3eff797c Mon Sep 17 00:00:00 2001 From: Tyrone Johnson <77393391+tjcloa@users.noreply.github.com> Date: Wed, 18 May 2022 02:12:57 +0400 Subject: [PATCH 68/86] Update testnet_contracts XUSD address --- scripts/contractInteraction/testnet_contracts.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/contractInteraction/testnet_contracts.json b/scripts/contractInteraction/testnet_contracts.json index ba390b770..4ed43b557 100644 --- a/scripts/contractInteraction/testnet_contracts.json +++ b/scripts/contractInteraction/testnet_contracts.json @@ -18,7 +18,7 @@ "WRBTC": "0x69FE5cEC81D5eF92600c1A0dB1F11986AB3758Ab", "MOC": "", "ETHs": "0x0Fd0d8D78Ce9299Ee0e5676a8d51F938C234162c", - "XUSD": "0x74858FE37d391f81F89472e1D8BC8Ef9CF67B3b1", + "XUSD": "0xa9262CC3fB54Ea55B1B0af00EfCa9416B8d59570", "BNBs": "", "FISH": "0xaa7038D80521351F243168FefE0352194e3f83C3", "USDT": "0x4d5a316d23ebe168d8f887b4447bf8dbfa4901cc", From deeb1e872889771cc75a2e372affb7a97fe7e714 Mon Sep 17 00:00:00 2001 From: Tyrone Johnson <77393391+tjcloa@users.noreply.github.com> Date: Wed, 18 May 2022 02:53:13 +0400 Subject: [PATCH 69/86] Update testnet_contracts XUSD/BRZ addresses XUSD/BRZ converter deployed with correct XUSD address on the testnet --- scripts/contractInteraction/testnet_contracts.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/contractInteraction/testnet_contracts.json b/scripts/contractInteraction/testnet_contracts.json index 4ed43b557..418a5bd39 100644 --- a/scripts/contractInteraction/testnet_contracts.json +++ b/scripts/contractInteraction/testnet_contracts.json @@ -36,7 +36,7 @@ "ConverterMOC": "0x2cb88F02cCA4dddBE8C41a6920853838Ada09F8b", "ConverterBNBs": "0x20d5c55c92615d416d73b34c8afed99288e99be1", "ConverterFISH": "0x4265d4f55219a4BDe9f1DE1348dA1f0b504849b4", - "ConverterXUSD-BRZ": "0x6Ca500A8F39C452CE7533AA320c9b7752F04AA64", + "ConverterXUSD-BRZ": "0x3378E4dc28c862E71E9c097C12305513B3cCc6B9", "ConverterMYNT": "0x84953dAF0E7a9fFb8B4fDf7F948185e1cF85852e", "(WR)BTC/USDT1": "0xffbbf93ecd27c8b500bd35d554802f7f349a1e9b", "(WR)BTC/USDT2": "0x7274305bb36d66f70cb8824621ec26d52abe9069", @@ -48,7 +48,7 @@ "(WR)BTC/ETH": "0xBb5B900EDa0F1459F582aB2436EA825a927f5bA2", "(WR)BTC/XUSD": "0xb89D193c8a9Ae3fadF73B23519c215a0B7DD1B37", "(WR)BTC/FISH": "0xe41E262889f89b9a6331680606D9e9AabD01743e", - "XUSD/BRZ": "0x7107E42f4b59310D217333B544465d428395Affe", + "XUSD/BRZ": "0x226E68FE73c4F3DA6697c08ABB44e4fa85d66116", "(WR)BTC/MYNT": "0xB12FA09a50c56e9a0C826b98e76DA7645017AB4D", "LiquidityMiningConfigToken": "0x0F1694aFEF2B25c1C069582F23Bca73608348F50", "SOVPoolOracle": "0x8A2a7F192DC39b70c2937C38559e704fcAB3F4CA", From 9397a80e68f0e3bb491a04651905202dce567441 Mon Sep 17 00:00:00 2001 From: James Howlett Date: Wed, 18 May 2022 17:38:57 +0300 Subject: [PATCH 70/86] script for SIP-0048 --- scripts/sip/sip_interaction.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts/sip/sip_interaction.py b/scripts/sip/sip_interaction.py index 2a724e1fb..5c229a2c2 100644 --- a/scripts/sip/sip_interaction.py +++ b/scripts/sip/sip_interaction.py @@ -20,7 +20,7 @@ def main(): # Call the function you want here - # createProposalSIP0044() + # createProposalSIP0048() balanceAfter = acct.balance() @@ -379,3 +379,14 @@ def createProposalSIP0044(): print(datas) print(description) # createProposal(contracts['GovernorOwner'], targets, values, signatures, datas, description) + +def createProposalSIP0048(): + # Action + target = [contracts['SOV']] + value = [0] + signature = ["symbol()"] + data = ["0x"] + description = "SIP-0048: Sovryn Strategic Investment Proposal : https://github.com/DistributedCollective/SIPS/blob/5a9b213/SIP-0048.md, sha256: 0d159814e12132caf36391ab3faa24e90174bbeeaf84449909a8b716e964267f" + + # Create Proposal + createProposal(contracts['GovernorAdmin'], target, value, signature, data, description) From ad795187cffd6720a3c053e61a1e55418dd544ac Mon Sep 17 00:00:00 2001 From: Santo Sinar Pandean Date: Thu, 19 May 2022 20:03:40 +0700 Subject: [PATCH 71/86] Use TimeLock instead of the governor contract itself Keep multisig as owner of vestingRegistry --- scripts/contractInteraction/governance.py | 4 +- .../contractInteraction/liquidity_mining.py | 4 +- scripts/contractInteraction/loan_tokens.py | 48 +++++++++---------- scripts/contractInteraction/prices.py | 44 ++++++++--------- scripts/contractInteraction/protocol.py | 4 +- .../contractInteraction/staking_vesting.py | 19 +++----- .../testnet_contracts.json | 2 + 7 files changed, 61 insertions(+), 64 deletions(-) diff --git a/scripts/contractInteraction/governance.py b/scripts/contractInteraction/governance.py index 7fd81365e..606a3dbc8 100644 --- a/scripts/contractInteraction/governance.py +++ b/scripts/contractInteraction/governance.py @@ -23,8 +23,8 @@ def executeProposal(id): tx.info() def transferLockedSOVOwnershipToGovernance(): - print("Add LockedSOV admin for address: ", conf.contracts['GovernorAdmin']) + print("Add LockedSOV admin for address: ", conf.contracts['TimelockAdmin']) lockedSOV = Contract.from_abi("LockedSOV", address=conf.contracts["LockedSOV"], abi=LockedSOV.abi, owner=conf.acct) # TODO: Need to check whether we need to remove the other admin or not - data = lockedSOV.addAdmin.encode_input(conf.contracts['GovernorAdmin']) + data = lockedSOV.addAdmin.encode_input(conf.contracts['TimelockAdmin']) sendWithMultisig(conf.contracts['multisig'], lockedSOV.address, data, conf.acct) diff --git a/scripts/contractInteraction/liquidity_mining.py b/scripts/contractInteraction/liquidity_mining.py index ef0774661..10d7fca6f 100644 --- a/scripts/contractInteraction/liquidity_mining.py +++ b/scripts/contractInteraction/liquidity_mining.py @@ -120,7 +120,7 @@ def getMissedBalance(): return res def transferLiquidityMiningOwnershipToGovernance(): - print("Transferring LiquidityMining ownership to: ", conf.contracts['GovernorOwner']) + print("Transferring LiquidityMining ownership to: ", conf.contracts['TimelockOwner']) lm = Contract.from_abi("LiquidityMining", address = conf.contracts['LiquidityMiningProxy'], abi = LiquidityMining.abi, owner = conf.acct) - data = lm.transferOwnership.encode_input(conf.contracts['GovernorOwner']) + data = lm.transferOwnership.encode_input(conf.contracts['TimelockOwner']) sendWithMultisig(conf.contracts['multisig'], lm.address, data, conf.acct) \ No newline at end of file diff --git a/scripts/contractInteraction/loan_tokens.py b/scripts/contractInteraction/loan_tokens.py index ef229826c..288456171 100644 --- a/scripts/contractInteraction/loan_tokens.py +++ b/scripts/contractInteraction/loan_tokens.py @@ -859,70 +859,70 @@ def replaceLoanTokenSettingsLowerAdmin(): def transferBeaconOwnershipToGovernance(): # transfer beacon LM - print("Transferring beacon LM ownserhip to: ", conf.contracts['GovernorOwner']) + print("Transferring beacon LM ownserhip to: ", conf.contracts['TimelockOwner']) loanTokenLogicBeaconLM = Contract.from_abi("loanTokenLogicBeaconLM", address=conf.contracts['LoanTokenLogicBeaconLM'], abi=LoanTokenLogicBeacon.abi, owner=conf.acct) - data = loanTokenLogicBeaconLM.transferOwnership.encode_input(conf.contracts['GovernorOwner']) + data = loanTokenLogicBeaconLM.transferOwnership.encode_input(conf.contracts['TimelockOwner']) sendWithMultisig(conf.contracts['multisig'], loanTokenLogicBeaconLM.address, data, conf.acct) # transfer beacon wrbtc - print("Transferring beacon WRBTC ownserhip to: ", conf.contracts['GovernorOwner']) + print("Transferring beacon WRBTC ownserhip to: ", conf.contracts['TimelockOwner']) loanTokenLogicBeaconWrbtc = Contract.from_abi("loanTokenLogicBeaconWrbtc", address=conf.contracts['LoanTokenLogicBeaconWrbtc'], abi=LoanTokenLogicBeacon.abi, owner=conf.acct) - data = loanTokenLogicBeaconWrbtc.transferOwnership.encode_input(conf.contracts['GovernorOwner']) + data = loanTokenLogicBeaconWrbtc.transferOwnership.encode_input(conf.contracts['TimelockOwner']) sendWithMultisig(conf.contracts['multisig'], loanTokenLogicBeaconWrbtc.address, data, conf.acct) def transferLoanTokenOwnershipToGovernance(): # iDOC - print("Transferring iDOC admin to: ", conf.contracts['GovernorAdmin']) + print("Transferring iDOC admin to: ", conf.contracts['TimelockAdmin']) loanToken = Contract.from_abi("loanToken", address=conf.contracts['iDOC'], abi=LoanTokenLogicStandard.abi, owner=conf.acct) # Need to change the admin first before the owner - data = loanToken.setAdmin.encode_input(conf.contracts['GovernorAdmin']) + data = loanToken.setAdmin.encode_input(conf.contracts['TimelockAdmin']) sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) - print("Transferring iDOC ownserhip to: ", conf.contracts['GovernorOwner']) - data = loanToken.transferOwnership.encode_input(conf.contracts['GovernorOwner']) + print("Transferring iDOC ownserhip to: ", conf.contracts['TimelockOwner']) + data = loanToken.transferOwnership.encode_input(conf.contracts['TimelockOwner']) sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) # iRBTC - print("Transferring iRBTC admin to: ", conf.contracts['GovernorAdmin']) + print("Transferring iRBTC admin to: ", conf.contracts['TimelockAdmin']) loanToken = Contract.from_abi("loanToken", address=conf.contracts['iRBTC'], abi=LoanTokenLogicStandard.abi, owner=conf.acct) # Need to change the admin first before the owner - data = loanToken.setAdmin.encode_input(conf.contracts['GovernorAdmin']) + data = loanToken.setAdmin.encode_input(conf.contracts['TimelockAdmin']) sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) - print("Transferring iRBTC ownserhip to: ", conf.contracts['GovernorOwner']) - data = loanToken.transferOwnership.encode_input(conf.contracts['GovernorOwner']) + print("Transferring iRBTC ownserhip to: ", conf.contracts['TimelockOwner']) + data = loanToken.transferOwnership.encode_input(conf.contracts['TimelockOwner']) sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) # iXUSD - print("Transferring iXUSD admin to: ", conf.contracts['GovernorAdmin']) + print("Transferring iXUSD admin to: ", conf.contracts['TimelockAdmin']) loanToken = Contract.from_abi("loanToken", address=conf.contracts['iXUSD'], abi=LoanTokenLogicStandard.abi, owner=conf.acct) # Need to change the admin first before the owner - data = loanToken.setAdmin.encode_input(conf.contracts['GovernorAdmin']) + data = loanToken.setAdmin.encode_input(conf.contracts['TimelockAdmin']) sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) - print("Transferring iXUSD ownserhip to: ", conf.contracts['GovernorOwner']) - data = loanToken.transferOwnership.encode_input(conf.contracts['GovernorOwner']) + print("Transferring iXUSD ownserhip to: ", conf.contracts['TimelockOwner']) + data = loanToken.transferOwnership.encode_input(conf.contracts['TimelockOwner']) sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) # iUSDT - print("Transferring iUSDT admin to: ", conf.contracts['GovernorAdmin']) + print("Transferring iUSDT admin to: ", conf.contracts['TimelockAdmin']) loanToken = Contract.from_abi("loanToken", address=conf.contracts['iUSDT'], abi=LoanTokenLogicStandard.abi, owner=conf.acct) # Need to change the admin first before the owner - data = loanToken.setAdmin.encode_input(conf.contracts['GovernorAdmin']) + data = loanToken.setAdmin.encode_input(conf.contracts['TimelockAdmin']) sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) - print("Transferring iUSDT ownserhip to: ", conf.contracts['GovernorOwner']) - data = loanToken.transferOwnership.encode_input(conf.contracts['GovernorOwner']) + print("Transferring iUSDT ownserhip to: ", conf.contracts['TimelockOwner']) + data = loanToken.transferOwnership.encode_input(conf.contracts['TimelockOwner']) sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) # iBPro - print("Transferring iBPro admin to: ", conf.contracts['GovernorAdmin']) + print("Transferring iBPro admin to: ", conf.contracts['TimelockAdmin']) loanToken = Contract.from_abi("loanToken", address=conf.contracts['iBPro'], abi=LoanTokenLogicStandard.abi, owner=conf.acct) # Need to change the admin first before the owner - data = loanToken.setAdmin.encode_input(conf.contracts['GovernorAdmin']) + data = loanToken.setAdmin.encode_input(conf.contracts['TimelockAdmin']) sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) - print("Transferring iBPro ownserhip to: ", conf.contracts['GovernorOwner']) - data = loanToken.transferOwnership.encode_input(conf.contracts['GovernorOwner']) + print("Transferring iBPro ownserhip to: ", conf.contracts['TimelockOwner']) + data = loanToken.transferOwnership.encode_input(conf.contracts['TimelockOwner']) sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) diff --git a/scripts/contractInteraction/prices.py b/scripts/contractInteraction/prices.py index 170df9dc8..13ca5c425 100644 --- a/scripts/contractInteraction/prices.py +++ b/scripts/contractInteraction/prices.py @@ -183,69 +183,69 @@ def setV1SOVPoolOracleAddress(v1PoolOracleAddress): def transferOracleOwnershipToGovernance(): # PriceFeeds (Gateway) - print("Transferring priceFeeds (gateway) ownership to: ", conf.contracts['GovernorAdmin']) + print("Transferring priceFeeds (gateway) ownership to: ", conf.contracts['TimelockAdmin']) feeds = Contract.from_abi("PriceFeeds", address= conf.contracts['PriceFeeds'], abi = PriceFeeds.abi, owner = conf.acct) - data = feeds.transferOwnership.encode_input(conf.contracts['GovernorAdmin']) + data = feeds.transferOwnership.encode_input(conf.contracts['TimelockAdmin']) sendWithMultisig(conf.contracts['multisig'], feeds.address, data, conf.acct) # BPRO PriceFeeds - print("Transferring BPro PriceFeeds ownership to: ", conf.contracts['GovernorAdmin']) + print("Transferring BPro PriceFeeds ownership to: ", conf.contracts['TimelockAdmin']) tokenFeeds = Contract.from_abi("BProPriceFeed", address=conf.contracts['BProPriceFeeds'] , abi = BProPriceFeed.abi, owner = conf.acct) - data = tokenFeeds.transferOwnership.encode_input(conf.contracts['GovernorAdmin']) + data = tokenFeeds.transferOwnership.encode_input(conf.contracts['TimelockAdmin']) sendWithMultisig(conf.contracts['multisig'], tokenFeeds.address, data, conf.acct) # MOC PriceFeeds (external) - print("Transferring MOC (External) PriceFeeds ownership to: ", conf.contracts['GovernorAdmin']) + print("Transferring MOC (External) PriceFeeds ownership to: ", conf.contracts['TimelockAdmin']) tokenFeeds = Contract.from_abi("PriceFeedsMoC", address = conf.contracts['PriceFeedsMOC'], abi = PriceFeedsMoC.abi, owner = conf.acct) - data = tokenFeeds.transferOwnership.encode_input(conf.contracts['GovernorAdmin']) + data = tokenFeeds.transferOwnership.encode_input(conf.contracts['TimelockAdmin']) sendWithMultisig(conf.contracts['multisig'], tokenFeeds.address, data, conf.acct) # RSK PriceFeeds - print("Transferring RSK PriceFeeds ownership to: ", conf.contracts['GovernorAdmin']) + print("Transferring RSK PriceFeeds ownership to: ", conf.contracts['TimelockAdmin']) tokenFeeds = Contract.from_abi("PriceFeedRSKOracle", address=conf.contracts['PriceFeedRSKOracle'] , abi = PriceFeedRSKOracle.abi, owner = conf.acct) - data = tokenFeeds.transferOwnership.encode_input(conf.contracts['GovernorAdmin']) + data = tokenFeeds.transferOwnership.encode_input(conf.contracts['TimelockAdmin']) sendWithMultisig(conf.contracts['multisig'], tokenFeeds.address, data, conf.acct) # --- V1Pool PriceFeeds --- # MOC PriceFeeds V1Pool - print("Transferring MOC (V1Pool) PriceFeeds ownership to: ", conf.contracts['GovernorAdmin']) + print("Transferring MOC (V1Pool) PriceFeeds ownership to: ", conf.contracts['TimelockAdmin']) tokenFeeds = Contract.from_abi("PriceFeedV1PoolOracle", address=conf.contracts['MOCPriceFeedsV1Pool'] , abi = PriceFeedV1PoolOracle.abi, owner = conf.acct) - data = tokenFeeds.transferOwnership.encode_input(conf.contracts['GovernorAdmin']) + data = tokenFeeds.transferOwnership.encode_input(conf.contracts['TimelockAdmin']) sendWithMultisig(conf.contracts['multisig'], tokenFeeds.address, data, conf.acct) # SOV PriceFeeds - print("Transferring SOV PriceFeeds ownership to: ", conf.contracts['GovernorAdmin']) + print("Transferring SOV PriceFeeds ownership to: ", conf.contracts['TimelockAdmin']) tokenFeeds = Contract.from_abi("PriceFeedV1PoolOracle", address=conf.contracts['SOVPriceFeeds'] , abi = PriceFeedV1PoolOracle.abi, owner = conf.acct) - data = tokenFeeds.transferOwnership.encode_input(conf.contracts['GovernorAdmin']) + data = tokenFeeds.transferOwnership.encode_input(conf.contracts['TimelockAdmin']) sendWithMultisig(conf.contracts['multisig'], tokenFeeds.address, data, conf.acct) # ETHs PriceFeeds - print("Transferring ETHs PriceFeeds ownership to: ", conf.contracts['GovernorAdmin']) + print("Transferring ETHs PriceFeeds ownership to: ", conf.contracts['TimelockAdmin']) tokenFeeds = Contract.from_abi("PriceFeedV1PoolOracle", address=conf.contracts['ETHsPriceFeeds'] , abi = PriceFeedV1PoolOracle.abi, owner = conf.acct) - data = tokenFeeds.transferOwnership.encode_input(conf.contracts['GovernorAdmin']) + data = tokenFeeds.transferOwnership.encode_input(conf.contracts['TimelockAdmin']) sendWithMultisig(conf.contracts['multisig'], tokenFeeds.address, data, conf.acct) # BNBs PriceFeeds - print("Transferring BNBs PriceFeeds ownership to: ", conf.contracts['GovernorAdmin']) + print("Transferring BNBs PriceFeeds ownership to: ", conf.contracts['TimelockAdmin']) tokenFeeds = Contract.from_abi("PriceFeedV1PoolOracle", address=conf.contracts['BNBsPriceFeeds'] , abi = PriceFeedV1PoolOracle.abi, owner = conf.acct) - data = tokenFeeds.transferOwnership.encode_input(conf.contracts['GovernorAdmin']) + data = tokenFeeds.transferOwnership.encode_input(conf.contracts['TimelockAdmin']) sendWithMultisig(conf.contracts['multisig'], tokenFeeds.address, data, conf.acct) # FISH PriceFeeds - print("Transferring FISH PriceFeeds ownership to: ", conf.contracts['GovernorAdmin']) + print("Transferring FISH PriceFeeds ownership to: ", conf.contracts['TimelockAdmin']) tokenFeeds = Contract.from_abi("PriceFeedV1PoolOracle", address=conf.contracts['FISHPriceFeeds'] , abi = PriceFeedV1PoolOracle.abi, owner = conf.acct) - data = tokenFeeds.transferOwnership.encode_input(conf.contracts['GovernorAdmin']) + data = tokenFeeds.transferOwnership.encode_input(conf.contracts['TimelockAdmin']) sendWithMultisig(conf.contracts['multisig'], tokenFeeds.address, data, conf.acct) # RIF PriceFeeds - print("Transferring RIF PriceFeeds ownership to: ", conf.contracts['GovernorAdmin']) + print("Transferring RIF PriceFeeds ownership to: ", conf.contracts['TimelockAdmin']) tokenFeeds = Contract.from_abi("PriceFeedV1PoolOracle", address=conf.contracts['RIFPriceFeeds'] , abi = PriceFeedV1PoolOracle.abi, owner = conf.acct) - data = tokenFeeds.transferOwnership.encode_input(conf.contracts['GovernorAdmin']) + data = tokenFeeds.transferOwnership.encode_input(conf.contracts['TimelockAdmin']) sendWithMultisig(conf.contracts['multisig'], tokenFeeds.address, data, conf.acct) # MYNT PriceFeeds - print("Transferring MYNT PriceFeeds ownership to: ", conf.contracts['GovernorAdmin']) + print("Transferring MYNT PriceFeeds ownership to: ", conf.contracts['TimelockAdmin']) tokenFeeds = Contract.from_abi("PriceFeedV1PoolOracle", address=conf.contracts['MYNTPriceFeeds'] , abi = PriceFeedV1PoolOracle.abi, owner = conf.acct) - data = tokenFeeds.transferOwnership.encode_input(conf.contracts['GovernorAdmin']) + data = tokenFeeds.transferOwnership.encode_input(conf.contracts['TimelockAdmin']) sendWithMultisig(conf.contracts['multisig'], tokenFeeds.address, data, conf.acct) \ No newline at end of file diff --git a/scripts/contractInteraction/protocol.py b/scripts/contractInteraction/protocol.py index 4ca8ede66..dd268458a 100644 --- a/scripts/contractInteraction/protocol.py +++ b/scripts/contractInteraction/protocol.py @@ -668,8 +668,8 @@ def depositCollateral(loanId,depositAmount, tokenAddress): # Transferring Ownership to GOV def transferProtocolOwnershipToGovernance(): - print("Transferring sovryn protocol ownserhip to: ", conf.contracts['GovernorOwner']) + print("Transferring sovryn protocol ownserhip to: ", conf.contracts['TimelockOwner']) sovryn = Contract.from_abi( "sovryn", address=conf.contracts['sovrynProtocol'], abi=interface.ISovrynBrownie.abi, owner=conf.acct) - data = sovryn.transferOwnership.encode_input(conf.contracts['GovernorOwner']) + data = sovryn.transferOwnership.encode_input(conf.contracts['TimelockOwner']) sendWithMultisig(conf.contracts['multisig'], sovryn.address, data, conf.acct) \ No newline at end of file diff --git a/scripts/contractInteraction/staking_vesting.py b/scripts/contractInteraction/staking_vesting.py index d3a873189..db574e387 100644 --- a/scripts/contractInteraction/staking_vesting.py +++ b/scripts/contractInteraction/staking_vesting.py @@ -333,32 +333,27 @@ def governanceWithdrawVesting( vesting, receiver): sendWithMultisig(conf.contracts['multisig'], conf.contracts['Staking'], data, conf.acct) def transferStakingOwnershipToGovernance(): - print("Add staking admin for address: ", conf.contracts['GovernorAdmin']) + print("Add staking admin for address: ", conf.contracts['TimelockAdmin']) staking = Contract.from_abi("Staking", address=conf.contracts['Staking'], abi=Staking.abi, owner=conf.acct) - data = staking.addAdmin.encode_input(conf.contracts['GovernorAdmin']) + data = staking.addAdmin.encode_input(conf.contracts['TimelockAdmin']) sendWithMultisig(conf.contracts['multisig'], staking.address, data, conf.acct) def transferStakingRewardsOwnershipToGovernance(): - print("Transferring StakingRewards ownership to: ", conf.contracts['GovernorAdmin']) + print("Transferring StakingRewards ownership to: ", conf.contracts['TimelockAdmin']) stakingRewards = Contract.from_abi("StakingRewards", address=conf.contracts['StakingRewardsProxy'], abi=StakingRewards.abi, owner=conf.acct) - data = stakingRewards.transferOwnership.encode_input(conf.contracts['GovernorAdmin']) + data = stakingRewards.transferOwnership.encode_input(conf.contracts['TimelockAdmin']) sendWithMultisig(conf.contracts['multisig'], stakingRewards.address, data, conf.acct) def transferVestingRegistryOwnershipToGovernance(): # add governor admin as admin - print("Add Vesting Registry admin for address: ", conf.contracts['GovernorAdmin']) + print("Add Vesting Registry admin for address: ", conf.contracts['TimelockAdmin']) vestingRegistry = Contract.from_abi("VestingRegistry", address=conf.contracts['VestingRegistryProxy'], abi=VestingRegistry.abi, owner=conf.acct) - data = vestingRegistry.addAdmin.encode_input(conf.contracts['GovernorAdmin']) + data = vestingRegistry.addAdmin.encode_input(conf.contracts['TimelockAdmin']) sendWithMultisig(conf.contracts['multisig'], vestingRegistry.address, data, conf.acct) ''' # add Exchequer admin as admin - print("Add Vesting Registry admin for multisig: ", conf.contracts['GovernorAdmin']) + print("Add Vesting Registry admin for multisig: ", conf.contracts['multisig']) data = vestingRegistry.addAdmin.encode_input(conf.contracts['multisig']) sendWithMultisig(conf.contracts['multisig'], vestingRegistry.address, data, conf.acct) ''' - - # Transfer ownership to GovernorOwner - print("Transferring Vesting Registry ownership to: ", conf.contracts['GovernorOwner']) - data = vestingRegistry.transferOwnership.encode_input(conf.contracts['GovernorOwner']) - sendWithMultisig(conf.contracts['multisig'], vestingRegistry.address, data, conf.acct) diff --git a/scripts/contractInteraction/testnet_contracts.json b/scripts/contractInteraction/testnet_contracts.json index dda60b42a..92cb32039 100644 --- a/scripts/contractInteraction/testnet_contracts.json +++ b/scripts/contractInteraction/testnet_contracts.json @@ -91,7 +91,9 @@ "OldFeeSharingProxy": "0x740E6f892C0132D659Abcd2B6146D237A4B6b653", "FeeSharingProxy": "0xedD92fb7C556E4A4faf8c4f5A90f471aDCD018f4", "GovernorOwner": "0x058FD3F6a40b92b311B49E5e3E064300600021D7", + "TimelockOwner": "0xF09631d220f9Da04F707F4bfA24376b1cac630B1", "GovernorAdmin": "0x1528f0341a1Ea546780caD690F54b4FBE1834ED4", + "TimelockAdmin": "0xD97E5Ba368f86766b574657580aec49d4C3Be615", "VestingRegistry": "0x80ec7ADd6CC1003BBEa89527ce93722e1DaD5c2a", "VestingRegistry2": "0x068fbb3Bef062C3daBA7a4B12f53Cd614FBcBF1d", "VestingRegistry3": "0x52E4419b9D33C6e0ceb2e7c01D3aA1a04b21668C", From faca26eb6ea7e54333a3db48e212580dc242b84d Mon Sep 17 00:00:00 2001 From: Santo Sinar Pandean Date: Fri, 20 May 2022 12:07:54 +0700 Subject: [PATCH 72/86] CloseWithDeposit: Revert if tiny position will remain --- contracts/modules/LoanClosingsWith.sol | 2 +- tests/protocol/CloseDepositTestToken.test.js | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/contracts/modules/LoanClosingsWith.sol b/contracts/modules/LoanClosingsWith.sol index d1deb97a4..6dec37f1a 100644 --- a/contracts/modules/LoanClosingsWith.sol +++ b/contracts/modules/LoanClosingsWith.sol @@ -154,7 +154,7 @@ contract LoanClosingsWith is LoanClosingsShared { if (remainingAmount > 0) { remainingAmount = _getAmountInRbtc(loanParamsLocal.loanToken, remainingAmount); if (remainingAmount <= TINY_AMOUNT) { - loanCloseAmount = loanLocal.principal; + revert("Tiny position will remain"); } } diff --git a/tests/protocol/CloseDepositTestToken.test.js b/tests/protocol/CloseDepositTestToken.test.js index 847c81d3a..8be5b9829 100644 --- a/tests/protocol/CloseDepositTestToken.test.js +++ b/tests/protocol/CloseDepositTestToken.test.js @@ -121,11 +121,6 @@ contract("ProtocolCloseDeposit", (accounts) => { await lockedSOV.getLockedBalance(borrower) ); - const tx = await sovryn.closeWithDeposit(loan_id, receiver, deposit_amount, { - from: borrower, - }); - const receipt = tx.receipt; - let loan_close_amount = deposit_amount.gt(principal) ? principal : deposit_amount; // Check that tiny position won't be created @@ -139,10 +134,21 @@ contract("ProtocolCloseDeposit", (accounts) => { remainingAmountInWRBTC = remainingAmount.mul(rate).div(precision); if (remainingAmountInWRBTC.cmp(TINY_AMOUNT) <= 0) { - loan_close_amount = principal; + await expectRevert( + sovryn.closeWithDeposit(loan_id, receiver, deposit_amount, { + from: borrower, + }), + "Tiny position will remain" + ); + return; } } + const tx = await sovryn.closeWithDeposit(loan_id, receiver, deposit_amount, { + from: borrower, + }); + const receipt = tx.receipt; + const withdraw_amount = loan_close_amount.eq(principal) ? collateral : collateral.mul(loan_close_amount).div(principal); From ddcb44de30e600439b3a6ee0ce5c4b38efdf7b94 Mon Sep 17 00:00:00 2001 From: ororopickpocket Date: Fri, 20 May 2022 11:01:45 +0000 Subject: [PATCH 73/86] added to the scripts --- .../contract_interaction.py | 26 ++++++++++++------- scripts/contractInteraction/fastbtc.py | 6 +++++ scripts/contractInteraction/loan_tokens.py | 2 +- scripts/contractInteraction/misc.py | 4 +++ scripts/contractInteraction/ownership.py | 2 +- scripts/contractInteraction/prices.py | 4 +++ scripts/contractInteraction/protocol.py | 9 +++++++ .../contractInteraction/staking_vesting.py | 6 +++++ scripts/generateAddress.py | 22 ++++++++++++++++ 9 files changed, 70 insertions(+), 11 deletions(-) create mode 100644 scripts/generateAddress.py diff --git a/scripts/contractInteraction/contract_interaction.py b/scripts/contractInteraction/contract_interaction.py index 80767b1bb..714974b25 100644 --- a/scripts/contractInteraction/contract_interaction.py +++ b/scripts/contractInteraction/contract_interaction.py @@ -34,18 +34,26 @@ def main(): #used often: - #withdrawRBTCFromWatcher(30e18, conf.contracts['FastBTC']) + #withdrawRBTCFromWatcher(40e18, conf.contracts['FastBTC']) #bal = getBalance(conf.contracts['SOV'], conf.contracts['Watcher']) - #withdrawTokensFromWatcher(conf.contracts['SOV'], bal, conf.contracts['multisig']) + #withdrawTokensFromWatcher(conf.contracts['DoC'], 170000e18, conf.contracts['multisig']) - #sendTokensFromMultisig(conf.contracts['SOV'], '0xd1c42e0ace7a80efc191835dac102043bcfbbbe6', 4500e18) - #sendFromMultisig('0xD9ECB390a6a32ae651D5C614974c5570c50A5D89', 25e18) + #sendTokensFromMultisig(conf.contracts['SOV'], '0xEd09C97b91e8fC6d34FDe6503f64A01a6b8684c6', 77065e18) + #sendFromMultisig('0xD9ECB390a6a32ae651D5C614974c5570c50A5D89', 30e18) #sendMYNTFromMultisigToFeeSharingProxy(36632.144056847e18) - - #for i in range (885, 887): - # checkTx(i) - # confirmWithMS(i) - + + ''' + for i in range (922, 928): + #confirmWithMS(i) + checkTx(i) + ''' #missed = getMissedBalance() #transferSOVtoLM(missed) + + #transferRBTCFromFastBTCOffRampToOnRamp(15e18) + + #redeemFromAggregatorWithMS(conf.contracts['XUSDAggregatorProxy'], conf.contracts['USDT'], 100000e18) + #sendTokensFromMultisig(conf.contracts['USDT'], conf.contracts['Watcher'], 100000e18) + + \ No newline at end of file diff --git a/scripts/contractInteraction/fastbtc.py b/scripts/contractInteraction/fastbtc.py index 557d0f43d..291edc892 100644 --- a/scripts/contractInteraction/fastbtc.py +++ b/scripts/contractInteraction/fastbtc.py @@ -12,6 +12,12 @@ def withdrawRBTCFromFastBTCBiDi(amount, recipient): print(data) sendWithMultisig(conf.contracts['multisig'], fastBTC.address, data, conf.acct) +def transferRBTCFromFastBTCOffRampToOnRamp(amount): + fastBTC = loadBiDiFastBTC() + data = fastBTC.withdrawRbtc.encode_input(amount, conf.contracts['FastBTC']) + print(data) + sendWithMultisig(conf.contracts['multisig'], fastBTC.address, data, conf.acct) + def setMaxTransferSatoshi(newMaxSatoshi): fastBTC = loadBiDiFastBTC() data = fastBTC.setMaxTransferSatoshi.encode_input(newMaxSatoshi) diff --git a/scripts/contractInteraction/loan_tokens.py b/scripts/contractInteraction/loan_tokens.py index 7546ff8f8..63bcf8107 100644 --- a/scripts/contractInteraction/loan_tokens.py +++ b/scripts/contractInteraction/loan_tokens.py @@ -95,7 +95,7 @@ def testTradeOpeningAndClosing(protocolAddress, loanTokenAddress, underlyingToke conf.acct, # trader, 0, # slippage b'', # loanDataBytes (only required with ether) - {'value': sendValue, 'allow_revert': True} + {'value': sendValue}#, 'allow_revert': True ) tx.info() loanId = tx.events['Trade']['loanId'] diff --git a/scripts/contractInteraction/misc.py b/scripts/contractInteraction/misc.py index 017b8423d..4a6da4796 100644 --- a/scripts/contractInteraction/misc.py +++ b/scripts/contractInteraction/misc.py @@ -34,6 +34,10 @@ def mintAggregatedTokenWithMS(aggregatorAddress, tokenAddress, amount): abiFile = open('./scripts/contractInteraction/ABIs/aggregator.json') abi = json.load(abiFile) aggregator = Contract.from_abi("Aggregator", address=aggregatorAddress, abi=abi, owner=conf.acct) + token = Contract.from_abi("Token", address= tokenAddress, abi = TestToken.abi, owner=conf.acct) + if(token.allowance(conf.acct, aggregatorAddress) < amount): + data = token.approve(aggregatorAddress, amount) + sendWithMultisig(conf.contracts['multisig'], token.address, data, conf.acct) data = aggregator.mint.encode_input(tokenAddress, amount) sendWithMultisig(conf.contracts['multisig'], aggregator.address, data, conf.acct) diff --git a/scripts/contractInteraction/ownership.py b/scripts/contractInteraction/ownership.py index b5d1f11b4..3927ef3df 100644 --- a/scripts/contractInteraction/ownership.py +++ b/scripts/contractInteraction/ownership.py @@ -31,7 +31,7 @@ def checkOwnerIsAddress(contractAddress, expectedOwner): def readOwnersOfAllContracts(): - for contractName in contracts: + for contractName in conf.contracts: #print(contractName) contract = Contract.from_abi("Ownable", address=conf.contracts[contractName], abi=LoanToken.abi, owner=conf.acct) if(contractName != 'multisig' and contractName != 'WRBTC' and contractName != 'og' and contractName != 'USDT' and contractName != 'medianizer' and contractName != 'USDTtoUSDTOracleAMM' and contractName != 'GovernorOwner' and contractName != 'GovernorAdmin' and contractName != 'SovrynSwapFormula' and contractName != 'MOCState' and contractName != 'USDTPriceFeed' and contractName != 'FeeSharingProxy' and contractName != 'TimelockOwner' and contractName != 'TimelockAdmin' and contractName != 'AdoptionFund' and contractName != 'DevelopmentFund'): diff --git a/scripts/contractInteraction/prices.py b/scripts/contractInteraction/prices.py index 983d01ac8..9de0c0b5b 100644 --- a/scripts/contractInteraction/prices.py +++ b/scripts/contractInteraction/prices.py @@ -77,6 +77,10 @@ def readMocOracleAddress(): priceFeedsMoC = Contract.from_abi("PriceFeedsMoC", address = conf.contracts['PriceFeedsMOC'], abi = PriceFeedsMoC.abi, owner = conf.acct) print(priceFeedsMoC.mocOracleAddress()) +def readRSKOracleAddress(): + priceFeedsMoC = Contract.from_abi("PriceFeedsMoC", address = conf.contracts['PriceFeedsMOC'], abi = PriceFeedsMoC.abi, owner = conf.acct) + print(priceFeedsMoC.rskOracleAddress()) + def updateOracleAddressAt(priceFeedAddress, newAddress): print("set oracle address to", newAddress) priceFeedsMoC = Contract.from_abi("PriceFeedsMoC", address = priceFeedAddress, abi = PriceFeedsMoC.abi, owner = conf.acct) diff --git a/scripts/contractInteraction/protocol.py b/scripts/contractInteraction/protocol.py index c37bd90e5..1885affcf 100644 --- a/scripts/contractInteraction/protocol.py +++ b/scripts/contractInteraction/protocol.py @@ -24,7 +24,16 @@ def readLendingFee(): def readLoan(loanId): sovryn = Contract.from_abi( "sovryn", address=conf.contracts['sovrynProtocol'], abi=interface.ISovrynBrownie.abi, owner=conf.acct) + loan = sovryn.getLoan(loanId).dict() + print('--------------------------------') + print('loan ID:', loan['loanId']) + print('principal:', loan['principal'] /1e18) + print('collateral:', loan['collateral']/1e18) + print('currentMargin', loan['currentMargin']/1e18) + print('complete object:') print(sovryn.getLoan(loanId).dict()) + print('--------------------------------') + def liquidate(protocolAddress, loanId): diff --git a/scripts/contractInteraction/staking_vesting.py b/scripts/contractInteraction/staking_vesting.py index 1cc3d59be..320007f51 100644 --- a/scripts/contractInteraction/staking_vesting.py +++ b/scripts/contractInteraction/staking_vesting.py @@ -332,3 +332,9 @@ def governanceWithdrawVesting( vesting, receiver): data = stakingProxy.governanceWithdrawVesting.encode_input( vesting, receiver) print(data) sendWithMultisig(conf.contracts['multisig'], conf.contracts['Staking'], data, conf.acct) + +def getStakedBalance(account): + stakingProxy = Contract.from_abi("Staking", address=conf.contracts['Staking'], abi=Staking.abi, owner=conf.acct) + bal = stakingProxy.balanceOf(account) + print(bal) + return bal \ No newline at end of file diff --git a/scripts/generateAddress.py b/scripts/generateAddress.py new file mode 100644 index 000000000..3d778328b --- /dev/null +++ b/scripts/generateAddress.py @@ -0,0 +1,22 @@ +import secrets +import web3 + +def generate_addr(prefix): + found = False + prefix = prefix.upper() + print("Looking for address with prefix = ", prefix, "...") + prefix = "0X"+prefix.upper() + while not found: + private_key = "0x" + secrets.token_hex(32) + acct = web3.Account.from_key(private_key) + found = acct.address.upper().startswith(prefix) + + print ("PK DO NOT SHARE:", private_key) + print("Address:", acct.address) + +#if __name__=="__main__": +def main(): + #generate_addr() + prefix="03030" + generate_addr(prefix) + print("pause") \ No newline at end of file From f6ee39088702b9bf2207cdf953b6593bfd234c88 Mon Sep 17 00:00:00 2001 From: James Howlett Date: Fri, 20 May 2022 14:48:58 +0300 Subject: [PATCH 74/86] changed update-lm.py --- .../deployment/liquidity-mining/update-lm.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/scripts/deployment/liquidity-mining/update-lm.py b/scripts/deployment/liquidity-mining/update-lm.py index 058af62fa..d0b82e6ad 100644 --- a/scripts/deployment/liquidity-mining/update-lm.py +++ b/scripts/deployment/liquidity-mining/update-lm.py @@ -104,26 +104,33 @@ def updateLMConfig(): # SOV/rBTC - 30k SOV ALLOCATION_POINT_BTC_SOV = 30000 # (WR)BTC/SOV # ETH/rBTC - 15k SOV - ALLOCATION_POINT_BTC_ETH = 15000 # (WR)BTC/ETH + ALLOCATION_POINT_BTC_ETH = 5000 # (WR)BTC/ETH # xUSD/rBTC - 15k SOV - ALLOCATION_POINT_BTC_XUSD = 15000 # (WR)BTC/XUSD + ALLOCATION_POINT_BTC_XUSD = 25000 # (WR)BTC/XUSD # BNB/rBTC - 15k SOV - ALLOCATION_POINT_BTC_BNB = 15000 # (WR)BTC/BNB + ALLOCATION_POINT_BTC_BNB = 1 # (WR)BTC/BNB ALLOCATION_POINT_I_XUSD = 15000 # iXUSD - ALLOCATION_POINT_BTC_MYNT = 15000 # (WR)BTC/MYNT + ALLOCATION_POINT_BTC_MYNT = 5000 # (WR)BTC/MYNT ALLOCATION_POINT_DEFAULT = 1 # (WR)BTC/USDT1 | (WR)BTC/USDT2 | (WR)BTC/DOC1 | (WR)BTC/DOC2 | (WR)BTC/BPRO1 | (WR)BTC/BPRO2 | (WR)BTC/MOC ALLOCATION_POINT_CONFIG_TOKEN = MAX_ALLOCATION_POINT - ALLOCATION_POINT_BTC_SOV - ALLOCATION_POINT_BTC_ETH - ALLOCATION_POINT_BTC_XUSD \ - ALLOCATION_POINT_BTC_BNB - ALLOCATION_POINT_I_XUSD -ALLOCATION_POINT_BTC_MYNT - ALLOCATION_POINT_DEFAULT * 9 - print("ALLOCATION_POINT_BTC_SOV: ", ALLOCATION_POINT_BTC_SOV) print("ALLOCATION_POINT_CONFIG_TOKEN: ", ALLOCATION_POINT_CONFIG_TOKEN) - print(lm.getPoolInfo(contracts['(WR)BTC/MYNT'])) print(lm.getPoolInfo(contracts['LiquidityMiningConfigToken'])) + data = lm.update.encode_input( + [contracts['(WR)BTC/ETH'], contracts['(WR)BTC/XUSD'], contracts['(WR)BTC/BNB'], contracts['(WR)BTC/MYNT'], contracts['LiquidityMiningConfigToken']], + [ALLOCATION_POINT_BTC_ETH, ALLOCATION_POINT_BTC_XUSD, ALLOCATION_POINT_BTC_BNB, ALLOCATION_POINT_BTC_MYNT, ALLOCATION_POINT_CONFIG_TOKEN], + True + ) + tx = multisig.submitTransaction(lm.address,0,data) + txId = tx.events["Submission"]["transactionId"] + print("txid",txId) + # data = lm.update.encode_input(contracts['(WR)BTC/MYNT'],ALLOCATION_POINT_BTC_MYNT,True) # tx = multisig.submitTransaction(lm.address,0,data) # txId = tx.events["Submission"]["transactionId"] From 6d48e0baf13b5e5a9297b7974486e8c7c8159581 Mon Sep 17 00:00:00 2001 From: ororopickpocket Date: Fri, 20 May 2022 12:57:58 +0000 Subject: [PATCH 75/86] updated addresses, copied helper scripts to contract interaction --- .../contract_interaction.py | 56 ++++++++++++++++++- .../testnet_contracts.json | 6 +- .../addfouryearvestingstoregistry.csv | 10 ++-- .../fouryearvesting/fouryearvestinglist.csv | 6 +- 4 files changed, 65 insertions(+), 13 deletions(-) diff --git a/scripts/contractInteraction/contract_interaction.py b/scripts/contractInteraction/contract_interaction.py index a5b3dfc2c..bfe007697 100644 --- a/scripts/contractInteraction/contract_interaction.py +++ b/scripts/contractInteraction/contract_interaction.py @@ -48,4 +48,58 @@ def main(): # confirmWithMS(i) #missed = getMissedBalance() - #transferSOVtoLM(missed) \ No newline at end of file + #transferSOVtoLM(missed) + + vestingAddress = "0x9768eF9F59b030E98f12B2B4C859E5eCbC016633" + fourYearVesting = Contract.from_abi("FourYearVestingLogic", address=vestingAddress, abi=FourYearVestingLogic.abi, owner=conf.acct) + + ''' + stakingAddress = conf.contracts['Staking'] + staking = Contract.from_abi("Staking", address=stakingAddress, abi=Staking.abi, owner=conf.acct) + stakes = staking.getStakes(vestingAddress) + print("Staking Details") + print("=======================================") + print(stakes) + + print(fourYearVesting.lastStakingSchedule()) + print(fourYearVesting.remainingStakeAmount()) + print(fourYearVesting.owner()) + print(fourYearVesting.tokenOwner()) + print(fourYearVesting.startDate()) + + fourYearVestingFactory = Contract.from_abi("FourYearVestingFactory", address=conf.contracts['FourYearVestingFactory'], abi=FourYearVestingFactory.abi, owner=conf.acct) + print(fourYearVestingFactory.owner()) + data = fourYearVestingFactory.transferOwnership.encode_input("0x511893483DCc1A9A98f153ec8298b63BE010A99f") + #sendWithMultisig(conf.contracts['multisig'], fourYearVestingFactory.address, data, conf.acct) + + + SOVtoken = Contract.from_abi("SOV", address=conf.contracts['SOV'], abi=SOV.abi, owner=conf.acct) + remainingAmount = 78*10**18 + #SOVtoken.approve(vestingAddress, remainingAmount) + lastSchedule = 0 + while remainingAmount > 0: + fourYearVesting.stakeTokens(remainingAmount, lastSchedule) + lastSchedule = fourYearVesting.lastStakingSchedule() + remainingAmount = fourYearVesting.remainingStakeAmount() + time.sleep(10) + + stakes = staking.getStakes(vestingAddress) + print("Staking Details") + print("=======================================") + print(stakes) + + + lastSchedule = fourYearVesting.lastStakingSchedule() + print(lastSchedule) + remainingAmount = fourYearVesting.remainingStakeAmount() + print(remainingAmount) + if remainingAmount > 0: + fourYearVesting.stakeTokens(remainingAmount, lastSchedule) + ''' + + #upgradeVesting() + + #isVestingAdmin(conf.acct) + + #readAllVestingContractsForAddress(conf.acct) + diff --git a/scripts/contractInteraction/testnet_contracts.json b/scripts/contractInteraction/testnet_contracts.json index 7d5053333..6d8827e59 100644 --- a/scripts/contractInteraction/testnet_contracts.json +++ b/scripts/contractInteraction/testnet_contracts.json @@ -87,8 +87,8 @@ "VestingRegistry2": "0x068fbb3Bef062C3daBA7a4B12f53Cd614FBcBF1d", "VestingRegistry3": "0x52E4419b9D33C6e0ceb2e7c01D3aA1a04b21668C", "VestingLogic": "0xc1cECAC06c7a5d5480F158043A150acf06e206cD", - "FourYearVestingLogic": "0x051D4F2ad54114Ec3b003f308B09640e39D362B1", - "FourYearVestingFactory": "0x914563Aca50218a070431f866803628F19E8CCdD", + "FourYearVestingLogic": "0x75B8faC8907f196Bd791dB57D8492a779cdE09b5", + "FourYearVestingFactory": "0x2AC0b13c174f03B4bE8C174Daf9d86C05b21496e", "OriginInvestorsClaim": "0x9FBe4Bf89521088F790a4dD2F3e495B4f0dA7F42", "TokenSender": "0x4D1903BaAd894Fc6Ff70483d8518Db78F163F9ff", "RBTCWrapperProxy": "0x6b1a4735b1E25ccE9406B2d5D7417cE53d1cf90e", @@ -113,6 +113,6 @@ "StakingRewardsProxy": "0x18eF0ff12f1b4D30104B4680D485D026C26D164D", "StakingRewards": "0x9762e0aA49248A58e14a5C09B4edE5c185b0d178", "VestingRegistryProxy": "0x09e8659B6d204C6b1bED2BFF8E3F43F834A5Bbc4", - "VestingRegistryLogic": "0xc9F6D4723Ed9AFc52FE07c083690e39e4505ED73", + "VestingRegistryLogic": "0x2Ccb5AE60D725dEB5c6FA38aDeCa4684250FE2f9", "SovrynSwapFormula": "0x7FF1C363b5600834bce7c514B01109eF1c103507" } diff --git a/scripts/fouryearvesting/addfouryearvestingstoregistry.csv b/scripts/fouryearvesting/addfouryearvestingstoregistry.csv index bf038f40b..b0e4ff1ca 100644 --- a/scripts/fouryearvesting/addfouryearvestingstoregistry.csv +++ b/scripts/fouryearvesting/addfouryearvestingstoregistry.csv @@ -1,6 +1,6 @@ tokenOwner,vestingAddress -0x511893483DCc1A9A98f153ec8298b63BE010A99f,0x7C2e9Ac45F6AD5F474CFE03d2985f39f365D46df -0x8517ecCe55F6d11e4a60eDDbB7F4090dd2aC19E7,0x7ed1A31064476FB4F7d8794fdE5398D568DC75B0 -0xC6138eC6c65e3280e720d4E6da0FD91A061351c4,0xDf7454C512758AF997FE4B7ce3D3039D5dcaF243 -0x49d99a6821B7569f8D409728eD1D3EF5bF1befa9,0x7bFd4e6Be7B5373072eDecEb8375CBF41225D61B -0xBf151ed43445a093E991211d46A34f60F02a9E09,0xE26d671Af44e8118def31f1e8833a66d247741ab \ No newline at end of file +0x511893483DCc1A9A98f153ec8298b63BE010A99f,0x6A86A77851d9D5E4D4E1b9Ad26aC3D010A5A9cF9 +0x8517ecCe55F6d11e4a60eDDbB7F4090dd2aC19E7,0x9768eF9F59b030E98f12B2B4C859E5eCbC016633 +0xC6138eC6c65e3280e720d4E6da0FD91A061351c4,0x1DbE53CB4a8895b32641ceAFB7d5e894F1D6c82E +0x9E0816a71B53ca67201a5088df960fE90910DE55,0xB0A9a94f41A0113AF99Ce6adcf5376A924BA9544 +0xD0Af98AEb84Ca58C0EfB2B5dBd80297bE02259Dc,0xB875aC55c8513A74629225CBF03b924F4b33D68D \ No newline at end of file diff --git a/scripts/fouryearvesting/fouryearvestinglist.csv b/scripts/fouryearvesting/fouryearvestinglist.csv index 7431b3247..8e323e378 100644 --- a/scripts/fouryearvesting/fouryearvestinglist.csv +++ b/scripts/fouryearvesting/fouryearvestinglist.csv @@ -1,5 +1,3 @@ -0x511893483DCc1A9A98f153ec8298b63BE010A99f,39.00,21772800 -0x8517ecCe55F6d11e4a60eDDbB7F4090dd2aC19E7,78.00,24192000 0xC6138eC6c65e3280e720d4E6da0FD91A061351c4,117.00,26611200 -0x49d99a6821B7569f8D409728eD1D3EF5bF1befa9,156.00,29030400 -0xBf151ed43445a093E991211d46A34f60F02a9E09,195.00,31449600 +0x9E0816a71B53ca67201a5088df960fE90910DE55,156.00,29030400 +0xD0Af98AEb84Ca58C0EfB2B5dBd80297bE02259Dc,195.00,31449600 From e3f00ceb4b816a3d457918be5fe7aa11281c26bf Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Fri, 20 May 2022 19:01:12 +0530 Subject: [PATCH 76/86] Stop adding 4 year vestings multiple times --- .../Vesting/VestingRegistryLogic.sol | 85 +++++++++---------- tests/vesting/VestingRegistryMigrations.js | 7 ++ 2 files changed, 47 insertions(+), 45 deletions(-) diff --git a/contracts/governance/Vesting/VestingRegistryLogic.sol b/contracts/governance/Vesting/VestingRegistryLogic.sol index dca87e39c..6d4d1c542 100644 --- a/contracts/governance/Vesting/VestingRegistryLogic.sol +++ b/contracts/governance/Vesting/VestingRegistryLogic.sol @@ -116,20 +116,20 @@ contract VestingRegistryLogic is VestingRegistryStorage { uint256 cliff = 4 weeks; uint256 duration = 156 weeks; for (uint256 i = 0; i < _tokenOwners.length; i++) { + require(!isVesting[_vestingAddresses[i]], "vesting exists"); require(_tokenOwners[i] != address(0), "token owner cannot be 0 address"); require(_vestingAddresses[i] != address(0), "vesting cannot be 0 address"); - uint256 uid = - uint256( - keccak256( - abi.encodePacked( - _tokenOwners[i], - uint256(VestingType.Vesting), - cliff, - duration, - vestingCreationType - ) + uint256 uid = uint256( + keccak256( + abi.encodePacked( + _tokenOwners[i], + uint256(VestingType.Vesting), + cliff, + duration, + vestingCreationType ) - ); + ) + ); vestings[uid] = Vesting( uint256(VestingType.Vesting), vestingCreationType, @@ -173,14 +173,13 @@ contract VestingRegistryLogic is VestingRegistryStorage { uint256 _duration, uint256 _vestingCreationType ) public onlyAuthorized { - address vesting = - _getOrCreateVesting( - _tokenOwner, - _cliff, - _duration, - uint256(VestingType.Vesting), - _vestingCreationType - ); + address vesting = _getOrCreateVesting( + _tokenOwner, + _cliff, + _duration, + uint256(VestingType.Vesting), + _vestingCreationType + ); emit VestingCreated( _tokenOwner, vesting, @@ -206,14 +205,13 @@ contract VestingRegistryLogic is VestingRegistryStorage { uint256 _duration, uint256 _vestingCreationType ) external onlyAuthorized { - address vesting = - _getOrCreateVesting( - _tokenOwner, - _cliff, - _duration, - uint256(VestingType.TeamVesting), - _vestingCreationType - ); + address vesting = _getOrCreateVesting( + _tokenOwner, + _cliff, + _duration, + uint256(VestingType.TeamVesting), + _vestingCreationType + ); emit TeamVestingCreated( _tokenOwner, vesting, @@ -260,12 +258,11 @@ contract VestingRegistryLogic is VestingRegistryStorage { uint256 _vestingCreationType ) public view returns (address) { uint256 type_ = uint256(VestingType.Vesting); - uint256 uid = - uint256( - keccak256( - abi.encodePacked(_tokenOwner, type_, _cliff, _duration, _vestingCreationType) - ) - ); + uint256 uid = uint256( + keccak256( + abi.encodePacked(_tokenOwner, type_, _cliff, _duration, _vestingCreationType) + ) + ); return vestings[uid].vestingAddress; } @@ -279,12 +276,11 @@ contract VestingRegistryLogic is VestingRegistryStorage { uint256 _vestingCreationType ) public view returns (address) { uint256 type_ = uint256(VestingType.TeamVesting); - uint256 uid = - uint256( - keccak256( - abi.encodePacked(_tokenOwner, type_, _cliff, _duration, _vestingCreationType) - ) - ); + uint256 uid = uint256( + keccak256( + abi.encodePacked(_tokenOwner, type_, _cliff, _duration, _vestingCreationType) + ) + ); return vestings[uid].vestingAddress; } @@ -304,12 +300,11 @@ contract VestingRegistryLogic is VestingRegistryStorage { uint256 _vestingCreationType ) internal returns (address) { address vesting; - uint256 uid = - uint256( - keccak256( - abi.encodePacked(_tokenOwner, _type, _cliff, _duration, _vestingCreationType) - ) - ); + uint256 uid = uint256( + keccak256( + abi.encodePacked(_tokenOwner, _type, _cliff, _duration, _vestingCreationType) + ) + ); if (vestings[uid].vestingAddress == address(0)) { if (_type == 1) { vesting = vestingFactory.deployVesting( diff --git a/tests/vesting/VestingRegistryMigrations.js b/tests/vesting/VestingRegistryMigrations.js index 6ef674b5d..886920e3f 100644 --- a/tests/vesting/VestingRegistryMigrations.js +++ b/tests/vesting/VestingRegistryMigrations.js @@ -318,6 +318,13 @@ contract("VestingRegistryMigrations", (accounts) => { assert.equal(vestingAddresses[0].vestingAddress, newVestingAddress); }); + it("fails adding four year vesting if already added", async () => { + // Add deployed four year vesting to registry + await vesting.addFourYearVestings([account4], [fourYearVesting.address]); + await expectRevert(vesting.addFourYearVestings([account4], [fourYearVesting.address]), "vesting exists"); + + }); + it("fails adding four year vesting if array mismatch", async () => { await expectRevert(vesting.addFourYearVestings([account4], []), "arrays mismatch"); }); From 6e5a6ff85598e44252a230480b48255a4dbc523d Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Fri, 20 May 2022 19:01:56 +0530 Subject: [PATCH 77/86] Ran Prettier --- .../Vesting/VestingRegistryLogic.sol | 84 ++++++++++--------- tests/vesting/VestingRegistryMigrations.js | 6 +- 2 files changed, 49 insertions(+), 41 deletions(-) diff --git a/contracts/governance/Vesting/VestingRegistryLogic.sol b/contracts/governance/Vesting/VestingRegistryLogic.sol index 6d4d1c542..11114d79c 100644 --- a/contracts/governance/Vesting/VestingRegistryLogic.sol +++ b/contracts/governance/Vesting/VestingRegistryLogic.sol @@ -119,17 +119,18 @@ contract VestingRegistryLogic is VestingRegistryStorage { require(!isVesting[_vestingAddresses[i]], "vesting exists"); require(_tokenOwners[i] != address(0), "token owner cannot be 0 address"); require(_vestingAddresses[i] != address(0), "vesting cannot be 0 address"); - uint256 uid = uint256( - keccak256( - abi.encodePacked( - _tokenOwners[i], - uint256(VestingType.Vesting), - cliff, - duration, - vestingCreationType + uint256 uid = + uint256( + keccak256( + abi.encodePacked( + _tokenOwners[i], + uint256(VestingType.Vesting), + cliff, + duration, + vestingCreationType + ) ) - ) - ); + ); vestings[uid] = Vesting( uint256(VestingType.Vesting), vestingCreationType, @@ -173,13 +174,14 @@ contract VestingRegistryLogic is VestingRegistryStorage { uint256 _duration, uint256 _vestingCreationType ) public onlyAuthorized { - address vesting = _getOrCreateVesting( - _tokenOwner, - _cliff, - _duration, - uint256(VestingType.Vesting), - _vestingCreationType - ); + address vesting = + _getOrCreateVesting( + _tokenOwner, + _cliff, + _duration, + uint256(VestingType.Vesting), + _vestingCreationType + ); emit VestingCreated( _tokenOwner, vesting, @@ -205,13 +207,14 @@ contract VestingRegistryLogic is VestingRegistryStorage { uint256 _duration, uint256 _vestingCreationType ) external onlyAuthorized { - address vesting = _getOrCreateVesting( - _tokenOwner, - _cliff, - _duration, - uint256(VestingType.TeamVesting), - _vestingCreationType - ); + address vesting = + _getOrCreateVesting( + _tokenOwner, + _cliff, + _duration, + uint256(VestingType.TeamVesting), + _vestingCreationType + ); emit TeamVestingCreated( _tokenOwner, vesting, @@ -258,11 +261,12 @@ contract VestingRegistryLogic is VestingRegistryStorage { uint256 _vestingCreationType ) public view returns (address) { uint256 type_ = uint256(VestingType.Vesting); - uint256 uid = uint256( - keccak256( - abi.encodePacked(_tokenOwner, type_, _cliff, _duration, _vestingCreationType) - ) - ); + uint256 uid = + uint256( + keccak256( + abi.encodePacked(_tokenOwner, type_, _cliff, _duration, _vestingCreationType) + ) + ); return vestings[uid].vestingAddress; } @@ -276,11 +280,12 @@ contract VestingRegistryLogic is VestingRegistryStorage { uint256 _vestingCreationType ) public view returns (address) { uint256 type_ = uint256(VestingType.TeamVesting); - uint256 uid = uint256( - keccak256( - abi.encodePacked(_tokenOwner, type_, _cliff, _duration, _vestingCreationType) - ) - ); + uint256 uid = + uint256( + keccak256( + abi.encodePacked(_tokenOwner, type_, _cliff, _duration, _vestingCreationType) + ) + ); return vestings[uid].vestingAddress; } @@ -300,11 +305,12 @@ contract VestingRegistryLogic is VestingRegistryStorage { uint256 _vestingCreationType ) internal returns (address) { address vesting; - uint256 uid = uint256( - keccak256( - abi.encodePacked(_tokenOwner, _type, _cliff, _duration, _vestingCreationType) - ) - ); + uint256 uid = + uint256( + keccak256( + abi.encodePacked(_tokenOwner, _type, _cliff, _duration, _vestingCreationType) + ) + ); if (vestings[uid].vestingAddress == address(0)) { if (_type == 1) { vesting = vestingFactory.deployVesting( diff --git a/tests/vesting/VestingRegistryMigrations.js b/tests/vesting/VestingRegistryMigrations.js index 886920e3f..21d381974 100644 --- a/tests/vesting/VestingRegistryMigrations.js +++ b/tests/vesting/VestingRegistryMigrations.js @@ -321,8 +321,10 @@ contract("VestingRegistryMigrations", (accounts) => { it("fails adding four year vesting if already added", async () => { // Add deployed four year vesting to registry await vesting.addFourYearVestings([account4], [fourYearVesting.address]); - await expectRevert(vesting.addFourYearVestings([account4], [fourYearVesting.address]), "vesting exists"); - + await expectRevert( + vesting.addFourYearVestings([account4], [fourYearVesting.address]), + "vesting exists" + ); }); it("fails adding four year vesting if array mismatch", async () => { From 13a5f74e8daeca482e0184fb644acef44b1120d5 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Fri, 20 May 2022 19:25:55 +0530 Subject: [PATCH 78/86] Upgraded Vesting Logic --- scripts/contractInteraction/testnet_contracts.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/contractInteraction/testnet_contracts.json b/scripts/contractInteraction/testnet_contracts.json index aa0672c6e..a02c911aa 100644 --- a/scripts/contractInteraction/testnet_contracts.json +++ b/scripts/contractInteraction/testnet_contracts.json @@ -113,6 +113,6 @@ "StakingRewardsProxy": "0x18eF0ff12f1b4D30104B4680D485D026C26D164D", "StakingRewards": "0x9762e0aA49248A58e14a5C09B4edE5c185b0d178", "VestingRegistryProxy": "0x09e8659B6d204C6b1bED2BFF8E3F43F834A5Bbc4", - "VestingRegistryLogic": "0x2Ccb5AE60D725dEB5c6FA38aDeCa4684250FE2f9", + "VestingRegistryLogic": "0x38B729f1c42095EEd5A7c22A56d186987F75CED5", "SovrynSwapFormula": "0x7FF1C363b5600834bce7c514B01109eF1c103507" -} +} \ No newline at end of file From 2f066b09431525576b7b043d0c010fdf68667ae2 Mon Sep 17 00:00:00 2001 From: Stan Marsh Date: Fri, 20 May 2022 19:26:32 +0530 Subject: [PATCH 79/86] Ran Prettier --- scripts/contractInteraction/testnet_contracts.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/contractInteraction/testnet_contracts.json b/scripts/contractInteraction/testnet_contracts.json index a02c911aa..7cc3849dd 100644 --- a/scripts/contractInteraction/testnet_contracts.json +++ b/scripts/contractInteraction/testnet_contracts.json @@ -115,4 +115,4 @@ "VestingRegistryProxy": "0x09e8659B6d204C6b1bED2BFF8E3F43F834A5Bbc4", "VestingRegistryLogic": "0x38B729f1c42095EEd5A7c22A56d186987F75CED5", "SovrynSwapFormula": "0x7FF1C363b5600834bce7c514B01109eF1c103507" -} \ No newline at end of file +} From 7e8615ef21b2551db42361056db1b88593cbeca6 Mon Sep 17 00:00:00 2001 From: ororopickpocket Date: Thu, 26 May 2022 12:32:32 +0000 Subject: [PATCH 80/86] helper scripts for vesting deployment added to contract interaction --- scripts/contractInteraction/contract_interaction.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/contractInteraction/contract_interaction.py b/scripts/contractInteraction/contract_interaction.py index bfe007697..79ab0d087 100644 --- a/scripts/contractInteraction/contract_interaction.py +++ b/scripts/contractInteraction/contract_interaction.py @@ -46,12 +46,13 @@ def main(): #for i in range (885, 887): # checkTx(i) # confirmWithMS(i) - + checkTx(946) + confirmWithMS(946) #missed = getMissedBalance() #transferSOVtoLM(missed) - vestingAddress = "0x9768eF9F59b030E98f12B2B4C859E5eCbC016633" - fourYearVesting = Contract.from_abi("FourYearVestingLogic", address=vestingAddress, abi=FourYearVestingLogic.abi, owner=conf.acct) + #vestingAddress = "0x9768eF9F59b030E98f12B2B4C859E5eCbC016633" + #fourYearVesting = Contract.from_abi("FourYearVestingLogic", address=vestingAddress, abi=FourYearVestingLogic.abi, owner=conf.acct) ''' stakingAddress = conf.contracts['Staking'] From 103731b1a982a62b29155762b803759838bb60f5 Mon Sep 17 00:00:00 2001 From: Santo Sinar Pandean Date: Thu, 26 May 2022 22:38:10 +0700 Subject: [PATCH 81/86] Separate loan token transfer between admin & owner role --- .../contract_interaction.py | 1 + scripts/contractInteraction/loan_tokens.py | 50 +++++++++++-------- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/scripts/contractInteraction/contract_interaction.py b/scripts/contractInteraction/contract_interaction.py index d7e1432b9..d027e3322 100644 --- a/scripts/contractInteraction/contract_interaction.py +++ b/scripts/contractInteraction/contract_interaction.py @@ -56,6 +56,7 @@ def main(): # # loan token # transferBeaconOwnershipToGovernance() + # transferLoanTokenAdminRoleToGovernance() # transferLoanTokenOwnershipToGovernance() # # oracles diff --git a/scripts/contractInteraction/loan_tokens.py b/scripts/contractInteraction/loan_tokens.py index 288456171..93400e192 100644 --- a/scripts/contractInteraction/loan_tokens.py +++ b/scripts/contractInteraction/loan_tokens.py @@ -870,58 +870,64 @@ def transferBeaconOwnershipToGovernance(): data = loanTokenLogicBeaconWrbtc.transferOwnership.encode_input(conf.contracts['TimelockOwner']) sendWithMultisig(conf.contracts['multisig'], loanTokenLogicBeaconWrbtc.address, data, conf.acct) -def transferLoanTokenOwnershipToGovernance(): +def transferLoanTokenAdminRoleToGovernance(): # iDOC print("Transferring iDOC admin to: ", conf.contracts['TimelockAdmin']) loanToken = Contract.from_abi("loanToken", address=conf.contracts['iDOC'], abi=LoanTokenLogicStandard.abi, owner=conf.acct) - # Need to change the admin first before the owner data = loanToken.setAdmin.encode_input(conf.contracts['TimelockAdmin']) sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) - print("Transferring iDOC ownserhip to: ", conf.contracts['TimelockOwner']) - data = loanToken.transferOwnership.encode_input(conf.contracts['TimelockOwner']) - sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) - # iRBTC print("Transferring iRBTC admin to: ", conf.contracts['TimelockAdmin']) loanToken = Contract.from_abi("loanToken", address=conf.contracts['iRBTC'], abi=LoanTokenLogicStandard.abi, owner=conf.acct) - # Need to change the admin first before the owner data = loanToken.setAdmin.encode_input(conf.contracts['TimelockAdmin']) sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) - print("Transferring iRBTC ownserhip to: ", conf.contracts['TimelockOwner']) - data = loanToken.transferOwnership.encode_input(conf.contracts['TimelockOwner']) - sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) - # iXUSD print("Transferring iXUSD admin to: ", conf.contracts['TimelockAdmin']) loanToken = Contract.from_abi("loanToken", address=conf.contracts['iXUSD'], abi=LoanTokenLogicStandard.abi, owner=conf.acct) - # Need to change the admin first before the owner data = loanToken.setAdmin.encode_input(conf.contracts['TimelockAdmin']) sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) - print("Transferring iXUSD ownserhip to: ", conf.contracts['TimelockOwner']) - data = loanToken.transferOwnership.encode_input(conf.contracts['TimelockOwner']) - sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) - # iUSDT print("Transferring iUSDT admin to: ", conf.contracts['TimelockAdmin']) loanToken = Contract.from_abi("loanToken", address=conf.contracts['iUSDT'], abi=LoanTokenLogicStandard.abi, owner=conf.acct) - # Need to change the admin first before the owner data = loanToken.setAdmin.encode_input(conf.contracts['TimelockAdmin']) sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) - print("Transferring iUSDT ownserhip to: ", conf.contracts['TimelockOwner']) - data = loanToken.transferOwnership.encode_input(conf.contracts['TimelockOwner']) - sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) - # iBPro print("Transferring iBPro admin to: ", conf.contracts['TimelockAdmin']) loanToken = Contract.from_abi("loanToken", address=conf.contracts['iBPro'], abi=LoanTokenLogicStandard.abi, owner=conf.acct) - # Need to change the admin first before the owner data = loanToken.setAdmin.encode_input(conf.contracts['TimelockAdmin']) sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) +def transferLoanTokenOwnershipToGovernance(): + # iDOC + loanToken = Contract.from_abi("loanToken", address=conf.contracts['iDOC'], abi=LoanTokenLogicStandard.abi, owner=conf.acct) + print("Transferring iDOC ownserhip to: ", conf.contracts['TimelockOwner']) + data = loanToken.transferOwnership.encode_input(conf.contracts['TimelockOwner']) + sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) + + # iRBTC + loanToken = Contract.from_abi("loanToken", address=conf.contracts['iRBTC'], abi=LoanTokenLogicStandard.abi, owner=conf.acct) + print("Transferring iRBTC ownserhip to: ", conf.contracts['TimelockOwner']) + data = loanToken.transferOwnership.encode_input(conf.contracts['TimelockOwner']) + sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) + + # iXUSD + loanToken = Contract.from_abi("loanToken", address=conf.contracts['iXUSD'], abi=LoanTokenLogicStandard.abi, owner=conf.acct) + print("Transferring iXUSD ownserhip to: ", conf.contracts['TimelockOwner']) + data = loanToken.transferOwnership.encode_input(conf.contracts['TimelockOwner']) + sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) + + # iUSDT + loanToken = Contract.from_abi("loanToken", address=conf.contracts['iUSDT'], abi=LoanTokenLogicStandard.abi, owner=conf.acct) + print("Transferring iUSDT ownserhip to: ", conf.contracts['TimelockOwner']) + data = loanToken.transferOwnership.encode_input(conf.contracts['TimelockOwner']) + sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) + + # iBPro + loanToken = Contract.from_abi("loanToken", address=conf.contracts['iBPro'], abi=LoanTokenLogicStandard.abi, owner=conf.acct) print("Transferring iBPro ownserhip to: ", conf.contracts['TimelockOwner']) data = loanToken.transferOwnership.encode_input(conf.contracts['TimelockOwner']) sendWithMultisig(conf.contracts['multisig'], loanToken.address, data, conf.acct) From 257dc08bff99c5f0a10da119e6b6ce1aff0f5b40 Mon Sep 17 00:00:00 2001 From: Santo Sinar Pandean Date: Fri, 27 May 2022 21:58:02 +0700 Subject: [PATCH 82/86] Update feeSharingProxy address --- scripts/contractInteraction/testnet_contracts.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/contractInteraction/testnet_contracts.json b/scripts/contractInteraction/testnet_contracts.json index 418a5bd39..11eb2bea4 100644 --- a/scripts/contractInteraction/testnet_contracts.json +++ b/scripts/contractInteraction/testnet_contracts.json @@ -80,7 +80,8 @@ "StakingLogic6": "0x78372F3a1Bdd9F341c819f903038e8aA1FDC3FC6", "StakingLogic7": "0xB75005D5393b4cf54fb428570F7b8919AD2ed4F8", "OldFeeSharingProxy": "0x740E6f892C0132D659Abcd2B6146D237A4B6b653", - "FeeSharingProxy": "0xedD92fb7C556E4A4faf8c4f5A90f471aDCD018f4", + "FeeSharingProxy": "0x85B19DD6E3c6cCC54D40c1bAEC15058962B8245b", + "FeeSharingProxy1DayStaking": "0xedD92fb7C556E4A4faf8c4f5A90f471aDCD018f4", "GovernorOwner": "0x058FD3F6a40b92b311B49E5e3E064300600021D7", "GovernorAdmin": "0x1528f0341a1Ea546780caD690F54b4FBE1834ED4", "VestingRegistry": "0x80ec7ADd6CC1003BBEa89527ce93722e1DaD5c2a", From 36e42ce633569e6c28f3650ceabd9481b2f2d901 Mon Sep 17 00:00:00 2001 From: Santo Sinar Pandean Date: Tue, 31 May 2022 22:33:27 +0700 Subject: [PATCH 83/86] Update comment & revert message to be more relevant --- contracts/modules/LoanClosingsWith.sol | 10 +++++----- tests/protocol/CloseDepositTestToken.test.js | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/modules/LoanClosingsWith.sol b/contracts/modules/LoanClosingsWith.sol index 6dec37f1a..8f9d9c439 100644 --- a/contracts/modules/LoanClosingsWith.sol +++ b/contracts/modules/LoanClosingsWith.sol @@ -149,13 +149,13 @@ contract LoanClosingsWith is LoanClosingsShared { ? loanLocal.principal : depositAmount; - //close whole loan if tiny position will remain + //revert if tiny position remains uint256 remainingAmount = loanLocal.principal - loanCloseAmount; if (remainingAmount > 0) { - remainingAmount = _getAmountInRbtc(loanParamsLocal.loanToken, remainingAmount); - if (remainingAmount <= TINY_AMOUNT) { - revert("Tiny position will remain"); - } + require( + _getAmountInRbtc(loanParamsLocal.loanToken, remainingAmount) > TINY_AMOUNT, + "Tiny amount when closing with deposit" + ); } uint256 loanCloseAmountLessInterest = diff --git a/tests/protocol/CloseDepositTestToken.test.js b/tests/protocol/CloseDepositTestToken.test.js index 8be5b9829..43d80767f 100644 --- a/tests/protocol/CloseDepositTestToken.test.js +++ b/tests/protocol/CloseDepositTestToken.test.js @@ -138,7 +138,7 @@ contract("ProtocolCloseDeposit", (accounts) => { sovryn.closeWithDeposit(loan_id, receiver, deposit_amount, { from: borrower, }), - "Tiny position will remain" + "Tiny amount when closing with deposit" ); return; } From 8053be6d97fc6b99b8e6e8a7a2e6678c946a3b4e Mon Sep 17 00:00:00 2001 From: Santo Sinar Pandean Date: Tue, 7 Jun 2022 16:10:12 +0700 Subject: [PATCH 84/86] feature: add new view function for checking tiny position close with deposit --- contracts/interfaces/ISovryn.sol | 5 ++++ contracts/modules/LoanClosingsShared.sol | 2 +- contracts/modules/LoanClosingsWith.sol | 30 ++++++++++++++++++++ interfaces/ISovrynBrownie.sol | 5 ++++ tests/protocol/CloseDepositTestToken.test.js | 17 +++++++++++ 5 files changed, 58 insertions(+), 1 deletion(-) diff --git a/contracts/interfaces/ISovryn.sol b/contracts/interfaces/ISovryn.sol index e51a9ada6..9ce478e23 100644 --- a/contracts/interfaces/ISovryn.sol +++ b/contracts/interfaces/ISovryn.sol @@ -484,4 +484,9 @@ contract ISovryn is function getDedicatedSOVRebate() external view returns (uint256); function setRolloverFlexFeePercent(uint256 newRolloverFlexFeePercent) external; + + function checkCloseWithDepositIsTinyPosition(bytes32 loanId, uint256 depositAmount) + external + view + returns (bool isTinyPosition, uint256 tinyPositionAmount); } diff --git a/contracts/modules/LoanClosingsShared.sol b/contracts/modules/LoanClosingsShared.sol index 54be2acc5..ca24b83ad 100644 --- a/contracts/modules/LoanClosingsShared.sol +++ b/contracts/modules/LoanClosingsShared.sol @@ -698,7 +698,7 @@ contract LoanClosingsShared is * @param amount the amount to be transferred * @return amount in RBTC * */ - function _getAmountInRbtc(address asset, uint256 amount) internal returns (uint256) { + function _getAmountInRbtc(address asset, uint256 amount) internal view returns (uint256) { (uint256 rbtcRate, uint256 rbtcPrecision) = IPriceFeeds(priceFeeds).queryRate(asset, address(wrbtcToken)); return amount.mul(rbtcRate).div(rbtcPrecision); diff --git a/contracts/modules/LoanClosingsWith.sol b/contracts/modules/LoanClosingsWith.sol index 8f9d9c439..7420a2aec 100644 --- a/contracts/modules/LoanClosingsWith.sol +++ b/contracts/modules/LoanClosingsWith.sol @@ -28,6 +28,7 @@ contract LoanClosingsWith is LoanClosingsShared { address prevModuleContractAddress = logicTargets[this.closeWithDeposit.selector]; _setTarget(this.closeWithDeposit.selector, target); _setTarget(this.closeWithSwap.selector, target); + _setTarget(this.checkCloseWithDepositIsTinyPosition.selector, target); emit ProtocolModuleContractReplaced(prevModuleContractAddress, target, "LoanClosingsWith"); } @@ -191,4 +192,33 @@ contract LoanClosingsWith is LoanClosingsShared { CloseTypes.Deposit ); } + + /** + * @notice Function to check whether the given loanId & deposit amount when closing with deposit will cause the tiny position + * + * @param loanId The id of the loan. + * @param depositAmount Defines how much the deposit amount to close the position. + * + * @return isTinyPosition true is indicating tiny position, false otherwise. + * @return tinyPositionAmount will return 0 for non tiny position, and will return the amount of tiny position if true + */ + function checkCloseWithDepositIsTinyPosition(bytes32 loanId, uint256 depositAmount) + external + view + returns (bool isTinyPosition, uint256 tinyPositionAmount) + { + (Loan memory loanLocal, LoanParams memory loanParamsLocal) = _checkLoan(loanId); + + if (depositAmount < loanLocal.principal) { + uint256 remainingAmount = loanLocal.principal - depositAmount; + uint256 remainingRBTCAmount = + _getAmountInRbtc(loanParamsLocal.loanToken, remainingAmount); + if (remainingRBTCAmount < TINY_AMOUNT) { + isTinyPosition = true; + tinyPositionAmount = remainingRBTCAmount; + } + } + + return (isTinyPosition, tinyPositionAmount); + } } diff --git a/interfaces/ISovrynBrownie.sol b/interfaces/ISovrynBrownie.sol index eedbb0b76..783677e9e 100644 --- a/interfaces/ISovrynBrownie.sol +++ b/interfaces/ISovrynBrownie.sol @@ -481,4 +481,9 @@ contract ISovrynBrownie is function getDedicatedSOVRebate() external view returns (uint256); function setRolloverFlexFeePercent(uint256 newRolloverFlexFeePercent) external; + + function checkCloseWithDepositIsTinyPosition(bytes32 loanId, uint256 depositAmount) + external + view + returns (bool isTinyPosition, uint256 tinyPositionAmount); } diff --git a/tests/protocol/CloseDepositTestToken.test.js b/tests/protocol/CloseDepositTestToken.test.js index 43d80767f..a5a0d8cd3 100644 --- a/tests/protocol/CloseDepositTestToken.test.js +++ b/tests/protocol/CloseDepositTestToken.test.js @@ -122,6 +122,7 @@ contract("ProtocolCloseDeposit", (accounts) => { ); let loan_close_amount = deposit_amount.gt(principal) ? principal : deposit_amount; + let checkTinyPosition; // Check that tiny position won't be created // Comparison must be in wrbtc format because TINY_AMOUNT is assumed as WRBTC @@ -134,6 +135,15 @@ contract("ProtocolCloseDeposit", (accounts) => { remainingAmountInWRBTC = remainingAmount.mul(rate).div(precision); if (remainingAmountInWRBTC.cmp(TINY_AMOUNT) <= 0) { + checkTinyPosition = await sovryn.checkCloseWithDepositIsTinyPosition( + loan_id, + deposit_amount + ); + expect(checkTinyPosition.isTinyPosition).to.equal(true); + expect(checkTinyPosition.tinyPositionAmount.toString()).to.equal( + remainingAmountInWRBTC.toString() + ); + await expectRevert( sovryn.closeWithDeposit(loan_id, receiver, deposit_amount, { from: borrower, @@ -144,6 +154,13 @@ contract("ProtocolCloseDeposit", (accounts) => { } } + checkTinyPosition = await sovryn.checkCloseWithDepositIsTinyPosition( + loan_id, + deposit_amount + ); + expect(checkTinyPosition.isTinyPosition).to.equal(false); + expect(checkTinyPosition.tinyPositionAmount.toString()).to.equal("0"); + const tx = await sovryn.closeWithDeposit(loan_id, receiver, deposit_amount, { from: borrower, }); From f7e0075a49c6bedf6bd73f3b28e3e73d09e6daae Mon Sep 17 00:00:00 2001 From: ororopickpocket Date: Thu, 9 Jun 2022 08:54:47 +0000 Subject: [PATCH 85/86] prepared removal of v2 pool liquidity --- scripts/contractInteraction/amm.py | 17 ++++++++++++- .../contract_interaction.py | 24 ++++++++++++------- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/scripts/contractInteraction/amm.py b/scripts/contractInteraction/amm.py index 012b876c6..f90e53c2d 100644 --- a/scripts/contractInteraction/amm.py +++ b/scripts/contractInteraction/amm.py @@ -320,4 +320,19 @@ def printConverterRegistryData(): print("") print("converters qty:", len(converters)) for i in range (0, len(converters)): - printV1ConverterData(converters[i]) \ No newline at end of file + printV1ConverterData(converters[i]) + +def removeLiquidityV2toMultisig(converter, poolToken, amount, minReturn): + abiFile = open('./scripts/contractInteraction/ABIs/LiquidityPoolV2Converter.json') + abi = json.load(abiFile) + converter = Contract.from_abi("LiquidityPoolV2Converter", address=converter, abi=abi, owner=conf.acct) + print("is active? ", converter.isActive()) + print("price oracle", converter.priceOracle()) + data = converter.addLiquidity.encode_input(poolToken, amount, minReturn) + print(data) + +def getReturnForV2PoolToken(converter, poolToken, amount): + abiFile = open('./scripts/contractInteraction/ABIs/LiquidityPoolV2Converter.json') + abi = json.load(abiFile) + converter = Contract.from_abi("LiquidityPoolV2Converter", address=converter, abi=abi, owner=conf.acct) + print(converter.removeLiquidityReturnAndFee(poolToken, amount)) \ No newline at end of file diff --git a/scripts/contractInteraction/contract_interaction.py b/scripts/contractInteraction/contract_interaction.py index e7b3aeaa0..ed8bea749 100644 --- a/scripts/contractInteraction/contract_interaction.py +++ b/scripts/contractInteraction/contract_interaction.py @@ -34,26 +34,34 @@ def main(): #used often: - #withdrawRBTCFromWatcher(40e18, conf.contracts['FastBTC']) + #withdrawRBTCFromWatcher(30e18, conf.contracts['multisig']) + #withdrawRBTCFromFastBTCBiDi(19e18, conf.contracts['multisig']) #bal = getBalance(conf.contracts['SOV'], conf.contracts['Watcher']) #withdrawTokensFromWatcher(conf.contracts['DoC'], 170000e18, conf.contracts['multisig']) - #sendTokensFromMultisig(conf.contracts['SOV'], '0xEd09C97b91e8fC6d34FDe6503f64A01a6b8684c6', 77065e18) + #sendTokensFromMultisig(conf.contracts['XUSD'], conf.contracts['Watcher'], 200000e18) #sendFromMultisig('0xD9ECB390a6a32ae651D5C614974c5570c50A5D89', 30e18) #sendMYNTFromMultisigToFeeSharingProxy(36632.144056847e18) ''' - for i in range (922, 928): - #confirmWithMS(i) + for i in range (961, 963): + confirmWithMS(i) checkTx(i) ''' + #confirmWithMS(956) + #confirmWithMS(958) #missed = getMissedBalance() #transferSOVtoLM(missed) - #transferRBTCFromFastBTCOffRampToOnRamp(15e18) + #transferRBTCFromFastBTCOffRampToOnRamp(9.6e18) - #redeemFromAggregatorWithMS(conf.contracts['XUSDAggregatorProxy'], conf.contracts['USDT'], 100000e18) - #sendTokensFromMultisig(conf.contracts['USDT'], conf.contracts['Watcher'], 100000e18) + #redeemFromAggregatorWithMS(conf.contracts['XUSDAggregatorProxy'], conf.contracts['DoC'], 30000e18) + #sendTokensFromMultisig(conf.contracts['DoC'], conf.contracts['Watcher'], 30000e18) - + #readMocOracleAddress() + + #bal = getBalance(conf.contracts['(WR)BTC/USDT2'], conf.contracts['multisig']) + #removeLiquidityV2toMultisig(conf.contracts['ConverterUSDT'], conf.contracts['(WR)BTC/USDT2'], bal, 1) + + #getReturnForV2PoolToken(conf.contracts['ConverterUSDT'], conf.contracts['(WR)BTC/USDT2'], bal) From 742b99a83dfbcaabf64dbbae2be985ea7a645327 Mon Sep 17 00:00:00 2001 From: ororopickpocket Date: Thu, 9 Jun 2022 15:08:00 +0000 Subject: [PATCH 86/86] started the process, updated addresses --- scripts/contractInteraction/contract_interaction.py | 11 ++++++----- scripts/contractInteraction/mainnet_contracts.json | 4 +++- .../fouryearvesting/addfouryearvestingstoregistry.csv | 7 ++----- scripts/fouryearvesting/create_four_year_vestings.py | 2 +- scripts/fouryearvesting/fouryearvestinglist.csv | 8 +++++--- 5 files changed, 17 insertions(+), 15 deletions(-) diff --git a/scripts/contractInteraction/contract_interaction.py b/scripts/contractInteraction/contract_interaction.py index d610f549f..d26708af8 100644 --- a/scripts/contractInteraction/contract_interaction.py +++ b/scripts/contractInteraction/contract_interaction.py @@ -37,8 +37,8 @@ def main(): #withdrawRBTCFromWatcher(40e18, conf.contracts['FastBTC']) #bal = getBalance(conf.contracts['SOV'], conf.contracts['Watcher']) #withdrawTokensFromWatcher(conf.contracts['DoC'], 170000e18, conf.contracts['multisig']) - - #sendTokensFromMultisig(conf.contracts['SOV'], '0xEd09C97b91e8fC6d34FDe6503f64A01a6b8684c6', 77065e18) + #print(conf.acct) + #sendTokensFromMultisig(conf.contracts['SOV'], conf.acct, 2336448e18) #sendFromMultisig('0xD9ECB390a6a32ae651D5C614974c5570c50A5D89', 30e18) #sendMYNTFromMultisigToFeeSharingProxy(36632.144056847e18) @@ -58,11 +58,11 @@ def main(): #redeemFromAggregatorWithMS(conf.contracts['XUSDAggregatorProxy'], conf.contracts['USDT'], 100000e18) #sendTokensFromMultisig(conf.contracts['USDT'], conf.contracts['Watcher'], 100000e18) - + ''' vestingAddress = "0x9768eF9F59b030E98f12B2B4C859E5eCbC016633" fourYearVesting = Contract.from_abi("FourYearVestingLogic", address=vestingAddress, abi=FourYearVestingLogic.abi, owner=conf.acct) - ''' + stakingAddress = conf.contracts['Staking'] staking = Contract.from_abi("Staking", address=stakingAddress, abi=Staking.abi, owner=conf.acct) stakes = staking.getStakes(vestingAddress) @@ -108,8 +108,9 @@ def main(): #upgradeVesting() + #addVestingAdmin(conf.acct) #isVestingAdmin(conf.acct) - #readAllVestingContractsForAddress(conf.acct) + readAllVestingContractsForAddress('0x945F5A36998abfC3450d9108f8C1D34Aa18fa48E') diff --git a/scripts/contractInteraction/mainnet_contracts.json b/scripts/contractInteraction/mainnet_contracts.json index bebcd4540..985118716 100644 --- a/scripts/contractInteraction/mainnet_contracts.json +++ b/scripts/contractInteraction/mainnet_contracts.json @@ -91,7 +91,7 @@ "FeeSharingProxy": "0x115cAF168c51eD15ec535727F64684D33B7b08D1", "FeeSharingLogic": "0x8289AF920cA3d63245740a20116e13aAe0F978e3", "VestingRegistryProxy": "0xe24ABdB7DcaB57F3cbe4cBDDd850D52F143eE920", - "VestingRegistryLogic": "0x536416A9fbAc10A3EA0D7f7396d0eE8FaE8146D0", + "VestingRegistryLogic": "0xc47E977d30fa553412897d0B1f97247F635c09b2", "VestingCreator": "0xa003D9F781a498D90f489328612E74Af1027417f", "TimelockOwner": "0x967c84b731679E36A344002b8E3CE50620A7F69f", "GovernorOwner": "0x6496DF39D000478a7A7352C01E0E713835051CcD", @@ -100,6 +100,8 @@ "GovernorAdmin": "0xfF25f66b7D7F385503D70574AE0170b6B1622dAd", "GovernorVaultAdmin": "0x51C754330c6cD04B810014E769Dab0343E31409E", "VestingLogic": "0x24fbA2281202C3aaE95A3440C08C0050448508A6", + "FourYearVestingLogic": "0xfA0888E2Cd5b045496A63E230910B9EC16EFA073", + "FourYearVestingFactory": "0xD5564a16f356dD45e445beC725F54496700b5C5A", "VestingRegistry": "0x80B036ae59B3e38B573837c01BB1DB95515b7E6B", "AdoptionFund": "0x0f31cfd6aAb4d378668Ad74DeFa89d3f4DB26633", "DevelopmentFund": "0x617866cC4a089c3653ddC31a618b078291839AeB", diff --git a/scripts/fouryearvesting/addfouryearvestingstoregistry.csv b/scripts/fouryearvesting/addfouryearvestingstoregistry.csv index b0e4ff1ca..ffee384af 100644 --- a/scripts/fouryearvesting/addfouryearvestingstoregistry.csv +++ b/scripts/fouryearvesting/addfouryearvestingstoregistry.csv @@ -1,6 +1,3 @@ tokenOwner,vestingAddress -0x511893483DCc1A9A98f153ec8298b63BE010A99f,0x6A86A77851d9D5E4D4E1b9Ad26aC3D010A5A9cF9 -0x8517ecCe55F6d11e4a60eDDbB7F4090dd2aC19E7,0x9768eF9F59b030E98f12B2B4C859E5eCbC016633 -0xC6138eC6c65e3280e720d4E6da0FD91A061351c4,0x1DbE53CB4a8895b32641ceAFB7d5e894F1D6c82E -0x9E0816a71B53ca67201a5088df960fE90910DE55,0xB0A9a94f41A0113AF99Ce6adcf5376A924BA9544 -0xD0Af98AEb84Ca58C0EfB2B5dBd80297bE02259Dc,0xB875aC55c8513A74629225CBF03b924F4b33D68D \ No newline at end of file +0x616cc7A216dBB411f0632e6b65CE2B1A9D9a05F3,0x8bA06b52B0b9d86381329E73499d9138F4d34569 +0x9E0816a71B53ca67201a5088df960fE90910DE55,0x71d6240d87ec6c8dac09852dEad7e7192E138C9C \ No newline at end of file diff --git a/scripts/fouryearvesting/create_four_year_vestings.py b/scripts/fouryearvesting/create_four_year_vestings.py index 42563b118..193e42b7c 100644 --- a/scripts/fouryearvesting/create_four_year_vestings.py +++ b/scripts/fouryearvesting/create_four_year_vestings.py @@ -85,7 +85,7 @@ def main(): print(stakes) # == Transfer ownership to multisig ============================================================================================= - fourYearVestingFactory.transferOwnership(multisig) + #fourYearVestingFactory.transferOwnership(multisig) print("SOV Balance After:") print(SOVtoken.balanceOf(acct) / 10**18) diff --git a/scripts/fouryearvesting/fouryearvestinglist.csv b/scripts/fouryearvesting/fouryearvestinglist.csv index 8e323e378..9a8c42adc 100644 --- a/scripts/fouryearvesting/fouryearvestinglist.csv +++ b/scripts/fouryearvesting/fouryearvestinglist.csv @@ -1,3 +1,5 @@ -0xC6138eC6c65e3280e720d4E6da0FD91A061351c4,117.00,26611200 -0x9E0816a71B53ca67201a5088df960fE90910DE55,156.00,29030400 -0xD0Af98AEb84Ca58C0EfB2B5dBd80297bE02259Dc,195.00,31449600 +0x616cc7A216dBB411f0632e6b65CE2B1A9D9a05F3,0.20,31449600 +0x9E0816a71B53ca67201a5088df960fE90910DE55,0.02,31449600 + + +