diff --git a/.github/workflows/extendstakingcron.yml b/.github/workflows/extendstakingcron.yml new file mode 100644 index 000000000..6d40ed8dc --- /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/contracts/governance/Vesting/VestingRegistryLogic.sol b/contracts/governance/Vesting/VestingRegistryLogic.sol index 57e1f5e7a..11114d79c 100644 --- a/contracts/governance/Vesting/VestingRegistryLogic.sol +++ b/contracts/governance/Vesting/VestingRegistryLogic.sol @@ -102,6 +102,45 @@ 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 { + 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(!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 + ) + ) + ); + 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/contracts/governance/Vesting/fouryear/FourYearVesting.sol b/contracts/governance/Vesting/fouryear/FourYearVesting.sol new file mode 100644 index 000000000..5dee260eb --- /dev/null +++ b/contracts/governance/Vesting/fouryear/FourYearVesting.sol @@ -0,0 +1,72 @@ +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/UpgradableProxy.sol"; +import "../../../openzeppelin/Address.sol"; + +/** + * @title Four Year Vesting Contract. + * + * @notice A four year vesting contract. + * + * @dev Vesting contract is upgradable, + * Make sure the vesting owner is multisig otherwise it will be + * 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. + * @param _extendDurationFor Duration till the unlocked tokens are extended. + * */ + constructor( + address _logic, + address _SOV, + address _stakingAddress, + address _tokenOwner, + address _feeSharingProxy, + uint256 _extendDurationFor + ) 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"); + require((_extendDurationFor % FOUR_WEEKS) == 0, "invalid duration"); + + _setImplementation(_logic); + SOV = IERC20(_SOV); + staking = Staking(_stakingAddress); + tokenOwner = _tokenOwner; + feeSharingProxy = IFeeSharingProxy(_feeSharingProxy); + maxInterval = 18 * FOUR_WEEKS; + extendDurationFor = _extendDurationFor; + } + + /** + * @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 new file mode 100644 index 000000000..4bbef07fe --- /dev/null +++ b/contracts/governance/Vesting/fouryear/FourYearVestingFactory.sol @@ -0,0 +1,52 @@ +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 { + /// @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. + * @param _extendDurationFor Duration till the unlocked tokens are extended. + * @return The four year vesting contract address. + * */ + function deployFourYearVesting( + address _SOV, + address _staking, + address _tokenOwner, + address _feeSharing, + address _vestingOwnerMultisig, + address _fourYearVestingLogic, + uint256 _extendDurationFor + ) external onlyOwner returns (address) { + address fourYearVesting = + address( + new FourYearVesting( + _fourYearVestingLogic, + _SOV, + _staking, + _tokenOwner, + _feeSharing, + _extendDurationFor + ) + ); + 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 new file mode 100644 index 000000000..d82039c8d --- /dev/null +++ b/contracts/governance/Vesting/fouryear/FourYearVestingLogic.sol @@ -0,0 +1,361 @@ +pragma solidity ^0.5.17; +pragma experimental ABIEncoderV2; + +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); + 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 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(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 timeDuration = startDate.add(extendDurationFor); + 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] <= timeDuration) && (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. In some cases the withdrawal may be allowed at a different + /// 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)); + + /// @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 new file mode 100644 index 000000000..3339b0c5c --- /dev/null +++ b/contracts/governance/Vesting/fouryear/FourYearVestingStorage.sol @@ -0,0 +1,71 @@ +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; + + // 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; + + /// @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 end date of the vesting. + uint256 public endDate; + + /// @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 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; + + /// @notice Address of new token owner. + address public newTokenOwner; + + /// @notice Address of new implementation. + address public newImplementation; + + /// @notice Duration(from start) till the time unlocked tokens are extended(for 3 years) + 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/IFourYearVesting.sol b/contracts/governance/Vesting/fouryear/IFourYearVesting.sol new file mode 100644 index 000000000..2d34fe61e --- /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 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..fd02295a4 --- /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, + address _feeSharing, + address _vestingOwnerMultisig, + address _fourYearVestingLogic, + uint256 _extendDurationFor + ) external returns (address); +} 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/mockup/MockFourYearVestingLogic.sol b/contracts/mockup/MockFourYearVestingLogic.sol new file mode 100644 index 000000000..dfc442459 --- /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/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 d1deb97a4..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"); } @@ -149,13 +150,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) { - loanCloseAmount = loanLocal.principal; - } + require( + _getAmountInRbtc(loanParamsLocal.loanToken, remainingAmount) > TINY_AMOUNT, + "Tiny amount when closing with deposit" + ); } uint256 loanCloseAmountLessInterest = @@ -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/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 a5b3dfc2c..bd12baf95 100644 --- a/scripts/contractInteraction/contract_interaction.py +++ b/scripts/contractInteraction/contract_interaction.py @@ -34,18 +34,64 @@ def main(): #used often: - #withdrawRBTCFromWatcher(30e18, conf.contracts['FastBTC']) + #withdrawRBTCFromWatcher(30e18, conf.contracts['multisig']) + #withdrawRBTCFromFastBTCBiDi(19e18, conf.contracts['multisig']) #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['XUSD'], conf.contracts['Watcher'], 200000e18) + #sendFromMultisig('0xD9ECB390a6a32ae651D5C614974c5570c50A5D89', 30e18) #sendMYNTFromMultisigToFeeSharingProxy(36632.144056847e18) + + ''' + for i in range (961, 963): + confirmWithMS(i) + checkTx(i) + ''' + #confirmWithMS(956) + #confirmWithMS(958) + #missed = getMissedBalance() + #transferSOVtoLM(missed) - #for i in range (885, 887): - # checkTx(i) - # confirmWithMS(i) + #transferRBTCFromFastBTCOffRampToOnRamp(9.6e18) - #missed = getMissedBalance() - #transferSOVtoLM(missed) \ No newline at end of file + #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) + + # # ---------- Transfer ownership to gov ---------- + # # core protocol + # transferProtocolOwnershipToGovernance() + + # # loan token + # transferBeaconOwnershipToGovernance() + # transferLoanTokenAdminRoleToGovernance() + # transferLoanTokenOwnershipToGovernance() + + # # oracles + # transferOracleOwnershipToGovernance() + + # # LM + # transferLiquidityMiningOwnershipToGovernance() + + # # Governance + # # lockedSOV + # transferLockedSOVOwnershipToGovernance() + + # # Staking + # transferStakingOwnershipToGovernance() + + # # StakingRewards + # transferStakingRewardsOwnershipToGovernance() + + # # VestingRegistry + # transferVestingRegistryOwnershipToGovernance() + + 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/governance.py b/scripts/contractInteraction/governance.py index 1a886b56d..606a3dbc8 100644 --- a/scripts/contractInteraction/governance.py +++ b/scripts/contractInteraction/governance.py @@ -20,4 +20,11 @@ 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(): + 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['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 e6468c5a9..10d7fca6f 100644 --- a/scripts/contractInteraction/liquidity_mining.py +++ b/scripts/contractInteraction/liquidity_mining.py @@ -117,4 +117,10 @@ 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(): + 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['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 7546ff8f8..64bedff32 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'] @@ -855,4 +855,80 @@ 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 + 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['TimelockOwner']) + sendWithMultisig(conf.contracts['multisig'], loanTokenLogicBeaconLM.address, data, conf.acct) + + # transfer beacon wrbtc + 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['TimelockOwner']) + sendWithMultisig(conf.contracts['multisig'], loanTokenLogicBeaconWrbtc.address, data, conf.acct) + +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) + data = loanToken.setAdmin.encode_input(conf.contracts['TimelockAdmin']) + 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) + data = loanToken.setAdmin.encode_input(conf.contracts['TimelockAdmin']) + 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) + data = loanToken.setAdmin.encode_input(conf.contracts['TimelockAdmin']) + 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) + data = loanToken.setAdmin.encode_input(conf.contracts['TimelockAdmin']) + 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) + 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) + diff --git a/scripts/contractInteraction/mainnet_contracts.json b/scripts/contractInteraction/mainnet_contracts.json index bebcd4540..ce027642b 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,6 +75,15 @@ "USDTPriceFeed": "0xed80ccde8baeff2dbfc70d3028a27e501fa0d7d5", "PriceFeedsMOC": "0x391fe8a92a7FC626A25F30E8c19B92bf8BE37FD3", "SOVPriceFeedOnProtocol": "0xA266aA67e2a25B0CCa460DEAfcacC81D17341a0D", + "MOCPriceFeedsV1Pool": "0xfF7ffCC3d0952C0133D4568C87ef4DeC72E4FddF", + "BProPriceFeeds": "0x389e2447e1397A8e485D658a44D845a324A338Cf", + "SOVPriceFeeds": "0xA266aA67e2a25B0CCa460DEAfcacC81D17341a0D", + "ETHsPriceFeeds": "0x9C9E06a23EE640A20DaAEd58E69012bB0742A098", + "BNBsPriceFeeds": "0xfb9898367e61B2e0CCE6C529dD5307996fAbd155", + "XUSDPriceFeeds": "0xEd80Ccde8bAeFf2dBFC70d3028a27e501Fa0D7D5", + "FISHPriceFeeds": "0x95576a065fD880e6C6621dBfAB54FdB9f827C783", + "RIFPriceFeeds": "0x15e6B67d5bCd57232104A891f466578b28f447D9", + "MYNTPriceFeeds": "0x1C11180b6730661090634cfD9F2510a1acA26fAf", "CSOV1": "0x0106F2fFBF6A4f5DEcE323d20E16E2037E732790", "CSOV2": "0x7f7Dcf9DF951C4A332740e9a125720DA242A34ff", "governorVault": "0xC7A1637b37190a456b017897207bceb2A29f19b9", @@ -91,7 +100,7 @@ "FeeSharingProxy": "0x115cAF168c51eD15ec535727F64684D33B7b08D1", "FeeSharingLogic": "0x8289AF920cA3d63245740a20116e13aAe0F978e3", "VestingRegistryProxy": "0xe24ABdB7DcaB57F3cbe4cBDDd850D52F143eE920", - "VestingRegistryLogic": "0x536416A9fbAc10A3EA0D7f7396d0eE8FaE8146D0", + "VestingRegistryLogic": "0xc47E977d30fa553412897d0B1f97247F635c09b2", "VestingCreator": "0xa003D9F781a498D90f489328612E74Af1027417f", "TimelockOwner": "0x967c84b731679E36A344002b8E3CE50620A7F69f", "GovernorOwner": "0x6496DF39D000478a7A7352C01E0E713835051CcD", @@ -100,6 +109,8 @@ "GovernorAdmin": "0xfF25f66b7D7F385503D70574AE0170b6B1622dAd", "GovernorVaultAdmin": "0x51C754330c6cD04B810014E769Dab0343E31409E", "VestingLogic": "0x24fbA2281202C3aaE95A3440C08C0050448508A6", + "FourYearVestingLogic": "0xfA0888E2Cd5b045496A63E230910B9EC16EFA073", + "FourYearVestingFactory": "0xD5564a16f356dD45e445beC725F54496700b5C5A", "VestingRegistry": "0x80B036ae59B3e38B573837c01BB1DB95515b7E6B", "AdoptionFund": "0x0f31cfd6aAb4d378668Ad74DeFa89d3f4DB26633", "DevelopmentFund": "0x617866cC4a089c3653ddC31a618b078291839AeB", 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..742032f0d 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) @@ -179,3 +183,73 @@ 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) + 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['TimelockAdmin']) + sendWithMultisig(conf.contracts['multisig'], feeds.address, data, conf.acct) + + # BPRO PriceFeeds + 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['TimelockAdmin']) + sendWithMultisig(conf.contracts['multisig'], tokenFeeds.address, data, conf.acct) + + # MOC PriceFeeds (external) + 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['TimelockAdmin']) + sendWithMultisig(conf.contracts['multisig'], tokenFeeds.address, data, conf.acct) + + # RSK PriceFeeds + 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['TimelockAdmin']) + sendWithMultisig(conf.contracts['multisig'], tokenFeeds.address, data, conf.acct) + + # --- V1Pool PriceFeeds --- + + # MOC PriceFeeds V1Pool + 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['TimelockAdmin']) + sendWithMultisig(conf.contracts['multisig'], tokenFeeds.address, data, conf.acct) + + # SOV PriceFeeds + 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['TimelockAdmin']) + sendWithMultisig(conf.contracts['multisig'], tokenFeeds.address, data, conf.acct) + + # ETHs PriceFeeds + 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['TimelockAdmin']) + sendWithMultisig(conf.contracts['multisig'], tokenFeeds.address, data, conf.acct) + + # BNBs PriceFeeds + 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['TimelockAdmin']) + sendWithMultisig(conf.contracts['multisig'], tokenFeeds.address, data, conf.acct) + + # FISH PriceFeeds + 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['TimelockAdmin']) + sendWithMultisig(conf.contracts['multisig'], tokenFeeds.address, data, conf.acct) + + # RIF PriceFeeds + 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['TimelockAdmin']) + sendWithMultisig(conf.contracts['multisig'], tokenFeeds.address, data, conf.acct) + + # MYNT PriceFeeds + 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['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 2ee31e678..134a3331b 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): @@ -664,4 +673,12 @@ 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(): + 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['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 a9729bf91..20790fd63 100644 --- a/scripts/contractInteraction/staking_vesting.py +++ b/scripts/contractInteraction/staking_vesting.py @@ -48,6 +48,22 @@ 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 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()) @@ -192,11 +208,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 @@ -331,3 +347,35 @@ 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(): + 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['TimelockAdmin']) + sendWithMultisig(conf.contracts['multisig'], staking.address, data, conf.acct) + +def transferStakingRewardsOwnershipToGovernance(): + 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['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['TimelockAdmin']) + vestingRegistry = Contract.from_abi("VestingRegistry", address=conf.contracts['VestingRegistryProxy'], abi=VestingRegistry.abi, owner=conf.acct) + 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['multisig']) + data = vestingRegistry.addAdmin.encode_input(conf.contracts['multisig']) + sendWithMultisig(conf.contracts['multisig'], vestingRegistry.address, 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 diff --git a/scripts/contractInteraction/testnet_contracts.json b/scripts/contractInteraction/testnet_contracts.json index 3280c502d..b822416ff 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", @@ -31,12 +31,12 @@ "ConverterBPRO": "", "ConverterUSDT": "0x133eBE9c8bA524C9B1B601E794dF527f390729bF", "ConverterSOV": "0xc2d05263318e2304fc7cdad40eea6a091b310080", - "ConverterXUSD": "0xe5e750ead0e564e489b0776273e4a10f3f3d4028", + "ConverterXUSD": "0xD877fd00ECF08eD78BF549fbc74bac3001aBBb07", "ConverterETHs": "0x9f570ffe6c421e2c7611aaea14770b807e9fb424", "ConverterMOC": "0x2cb88F02cCA4dddBE8C41a6920853838Ada09F8b", "ConverterBNBs": "0x20d5c55c92615d416d73b34c8afed99288e99be1", "ConverterFISH": "0x4265d4f55219a4BDe9f1DE1348dA1f0b504849b4", - "ConverterXUSD-BRZ": "0x6Ca500A8F39C452CE7533AA320c9b7752F04AA64", + "ConverterXUSD-BRZ": "0x3378E4dc28c862E71E9c097C12305513B3cCc6B9", "ConverterMYNT": "0x84953dAF0E7a9fFb8B4fDf7F948185e1cF85852e", "(WR)BTC/USDT1": "0xffbbf93ecd27c8b500bd35d554802f7f349a1e9b", "(WR)BTC/USDT2": "0x7274305bb36d66f70cb8824621ec26d52abe9069", @@ -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", + "XUSD/BRZ": "0x226E68FE73c4F3DA6697c08ABB44e4fa85d66116", "(WR)BTC/MYNT": "0xB12FA09a50c56e9a0C826b98e76DA7645017AB4D", "LiquidityMiningConfigToken": "0x0F1694aFEF2B25c1C069582F23Bca73608348F50", "SOVPoolOracle": "0x8A2a7F192DC39b70c2937C38559e704fcAB3F4CA", - "XUSDPoolOracle": "0xA30E5776c6Ae21E0CA28C6b4c39Fe7A9744d9a86", + "XUSDPoolOracle": "0xa888366a68179EeE27eDdfe1f68AAb829b03C9aA", "ETHPoolOracle": "0x9fDaA4E1AcFc243d29C5e2AE72fbC322a10C5530", "MOCPoolOracle": "0xA60d29C03452b858C4580725D5e9047982A9517a", "BNBPoolOracle": "0x73616dbc3A6fA63354d4dA0C3B74834D079BE46d", @@ -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", @@ -80,13 +89,18 @@ "StakingLogic6": "0x78372F3a1Bdd9F341c819f903038e8aA1FDC3FC6", "StakingLogic7": "0xB75005D5393b4cf54fb428570F7b8919AD2ed4F8", "OldFeeSharingProxy": "0x740E6f892C0132D659Abcd2B6146D237A4B6b653", - "FeeSharingProxy": "0xedD92fb7C556E4A4faf8c4f5A90f471aDCD018f4", + "FeeSharingProxy": "0x85B19DD6E3c6cCC54D40c1bAEC15058962B8245b", + "FeeSharingProxy1DayStaking": "0xedD92fb7C556E4A4faf8c4f5A90f471aDCD018f4", "GovernorOwner": "0x058FD3F6a40b92b311B49E5e3E064300600021D7", + "TimelockOwner": "0xF09631d220f9Da04F707F4bfA24376b1cac630B1", "GovernorAdmin": "0x1528f0341a1Ea546780caD690F54b4FBE1834ED4", + "TimelockAdmin": "0xD97E5Ba368f86766b574657580aec49d4C3Be615", "VestingRegistry": "0x80ec7ADd6CC1003BBEa89527ce93722e1DaD5c2a", "VestingRegistry2": "0x068fbb3Bef062C3daBA7a4B12f53Cd614FBcBF1d", "VestingRegistry3": "0x52E4419b9D33C6e0ceb2e7c01D3aA1a04b21668C", "VestingLogic": "0xc1cECAC06c7a5d5480F158043A150acf06e206cD", + "FourYearVestingLogic": "0x75B8faC8907f196Bd791dB57D8492a779cdE09b5", + "FourYearVestingFactory": "0x2AC0b13c174f03B4bE8C174Daf9d86C05b21496e", "OriginInvestorsClaim": "0x9FBe4Bf89521088F790a4dD2F3e495B4f0dA7F42", "TokenSender": "0x4D1903BaAd894Fc6Ff70483d8518Db78F163F9ff", "RBTCWrapperProxy": "0x6b1a4735b1E25ccE9406B2d5D7417cE53d1cf90e", @@ -110,7 +124,7 @@ "WatcherContract": "0x3583155D5e87491dACDc15f7D0032C12D5D0ece0", "StakingRewardsProxy": "0x18eF0ff12f1b4D30104B4680D485D026C26D164D", "StakingRewards": "0x9762e0aA49248A58e14a5C09B4edE5c185b0d178", - "VestingRegistryProxy": "0x8eE1254c1b95FFaD975Ac6f348Bc1cd25CB0c22F", - "VestingRegistryLogic": "0x8Ea3bF5C621FFb93f874047a0c1eE6DffB00053E", + "VestingRegistryProxy": "0x09e8659B6d204C6b1bED2BFF8E3F43F834A5Bbc4", + "VestingRegistryLogic": "0x38B729f1c42095EEd5A7c22A56d186987F75CED5", "SovrynSwapFormula": "0x7FF1C363b5600834bce7c514B01109eF1c103507" } 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"] diff --git a/scripts/fouryearvesting/add_to_registry.py b/scripts/fouryearvesting/add_to_registry.py new file mode 100644 index 000000000..011ba5b52 --- /dev/null +++ b/scripts/fouryearvesting/add_to_registry.py @@ -0,0 +1,69 @@ +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) + + balanceBefore = acct.balance() + + 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) + + print("deployment cost:") + print((balanceBefore - acct.balance()) / 10**18) + + # 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..ffee384af --- /dev/null +++ b/scripts/fouryearvesting/addfouryearvestingstoregistry.csv @@ -0,0 +1,3 @@ +tokenOwner,vestingAddress +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 new file mode 100644 index 000000000..193e42b7c --- /dev/null +++ b/scripts/fouryearvesting/create_four_year_vestings.py @@ -0,0 +1,94 @@ +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'] + 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 + DAY = 24 * 60 * 60 + FOUR_WEEKS = 4 * 7 * DAY + 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 + 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 + extendDurationFor = row[2].replace(" ", "") + tx = fourYearVestingFactory.deployFourYearVesting(SOVtoken.address, stakingAddress, tokenOwner, feeSharingAddress, multisig, fourYearVestingLogic, extendDurationFor) + 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) + + SOVtoken.approve(vestingAddress, amount) + + remainingAmount = amount + 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") + print("=======================================") + print(stakes) + + # == Transfer ownership to multisig ============================================================================================= + #fourYearVestingFactory.transferOwnership(multisig) + + print("SOV Balance After:") + 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..ab42d3c74 --- /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) + + # 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/extendStakingCron.py b/scripts/fouryearvesting/extendStakingCron.py new file mode 100644 index 000000000..ef0e84867 --- /dev/null +++ b/scripts/fouryearvesting/extendStakingCron.py @@ -0,0 +1,79 @@ +from brownie import * +from brownie.network.contract import InterfaceContainer + +import json +import csv +import time +import math + +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) + datenow = time.time() + timeLockDate = readLockDate(datenow) + print('timeLockDate:', timeLockDate) + extendDurationFor = fourYearVestingLogic.extendDurationFor() + print('extendDurationFor:', extendDurationFor) + maxIterations = extendDurationFor / 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 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) <= maxIterations) and (timediff < 86400) ): + fourYearVestingLogic.extendStaking() \ No newline at end of file diff --git a/scripts/fouryearvesting/fouryearvestinglist.csv b/scripts/fouryearvesting/fouryearvestinglist.csv new file mode 100644 index 000000000..9a8c42adc --- /dev/null +++ b/scripts/fouryearvesting/fouryearvestinglist.csv @@ -0,0 +1,5 @@ +0x616cc7A216dBB411f0632e6b65CE2B1A9D9a05F3,0.20,31449600 +0x9E0816a71B53ca67201a5088df960fE90910DE55,0.02,31449600 + + + 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 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) diff --git a/tests/protocol/CloseDepositTestToken.test.js b/tests/protocol/CloseDepositTestToken.test.js index 847c81d3a..a5a0d8cd3 100644 --- a/tests/protocol/CloseDepositTestToken.test.js +++ b/tests/protocol/CloseDepositTestToken.test.js @@ -121,12 +121,8 @@ 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; + let checkTinyPosition; // Check that tiny position won't be created // Comparison must be in wrbtc format because TINY_AMOUNT is assumed as WRBTC @@ -139,10 +135,37 @@ contract("ProtocolCloseDeposit", (accounts) => { remainingAmountInWRBTC = remainingAmount.mul(rate).div(precision); if (remainingAmountInWRBTC.cmp(TINY_AMOUNT) <= 0) { - loan_close_amount = principal; + 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, + }), + "Tiny amount when closing with deposit" + ); + return; } } + 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, + }); + const receipt = tx.receipt; + const withdraw_amount = loan_close_amount.eq(principal) ? collateral : collateral.mul(loan_close_amount).div(principal); diff --git a/tests/vesting/FourYearVesting.js b/tests/vesting/FourYearVesting.js new file mode 100644 index 000000000..a3c636cd7 --- /dev/null +++ b/tests/vesting/FourYearVesting.js @@ -0,0 +1,1255 @@ +const { expect } = require("chai"); +const { expectRevert, expectEvent, constants, BN } = require("@openzeppelin/test-helpers"); +const { 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"); +const VestingFactory = artifacts.require("FourYearVestingFactory"); +//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) => { + 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, + 52 * WEEK + ); + 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(); + let _extendDurationFor = await vestingInstance.extendDurationFor(); + + 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); + assert.equal(_extendDurationFor, 52 * WEEK); + }); + }); + + describe("constructor", () => { + it("sets the expected values", async () => { + let vestingInstance = await Vesting.new( + vestingLogic.address, + token.address, + staking.address, + root, + feeSharingProxy.address, + 52 * WEEK + ); + 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(); + let _extendDurationFor = await vestingInstance.extendDurationFor(); + + 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); + assert.equal(_extendDurationFor, 52 * WEEK); + }); + + 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, + 52 * WEEK + ), + "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, + 52 * WEEK + ), + "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, + 52 * WEEK + ), + "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, + 52 * WEEK + ), + "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, + 52 * WEEK + ), + "_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, + 52 * WEEK + ), + "_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, + 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 + ), + "_feeSharingProxy not a contract" + ); + }); + + it("fails if extendDurationFor 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", () => { + 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, + 52 * WEEK + ); + 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, + 52 * WEEK + ); + 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, + 52 * WEEK + ); + 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, + 52 * WEEK + ); + 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, + 52 * WEEK + ); + 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, + 52 * WEEK + ); + 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, + 52 * WEEK + ); + 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, + 52 * WEEK + ); + 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, + 52 * WEEK + ); + 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, + 52 * WEEK + ); + 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, + 52 * WEEK + ); + 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, + 52 * WEEK + ); + 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 }), + "WS01" + ); + }); + + it("shouldn't be possible to use governanceWithdraw by user", async () => { + await expectRevert( + staking.governanceWithdraw(100, kickoffTS.toNumber() + 52 * WEEK, root), + "S07" + ); + }); + }); + + 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, + 52 * WEEK + ); + 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, + 52 * WEEK + ); + 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, + 52 * WEEK + ); + vesting = await VestingLogic.at(vesting.address); + await expectRevert(vesting.migrateToNewStakingContract(), "S19"); + }); + + 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, + 52 * WEEK + ); + 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, + 52 * WEEK + ); + 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, + 52 * WEEK + ); + 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, + 52 * WEEK + ); + 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, + 52 * WEEK + ); + 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, + 52 * WEEK + ); + 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, + 52 * WEEK + ); + 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, + 52 * WEEK + ); + 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, + 52 * WEEK + ); + 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/VestingRegistryMigrations.js b/tests/vesting/VestingRegistryMigrations.js index 6e7465294..21d381974 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)); @@ -23,6 +25,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; @@ -34,12 +37,13 @@ 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. before(async () => { - [root, account1, account2, account3, accounts4, ...accounts] = accounts; + [root, account1, account2, account3, account4, ...accounts] = accounts; }); beforeEach(async () => { @@ -65,6 +69,18 @@ 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, + 13 * FOUR_WEEKS + ); + fourYearVesting = await FourYearVestingLogic.at(fourYearVesting.address); }); describe("addDeployedVestings", () => { @@ -265,5 +281,77 @@ contract("VestingRegistryMigrations", (accounts) => { "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 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"); + }); + + 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" + ); + }); }); });