diff --git a/contracts/interfaces/IOmnichainStaking.sol b/contracts/interfaces/IOmnichainStaking.sol index 9b9ed4f..39ff515 100644 --- a/contracts/interfaces/IOmnichainStaking.sol +++ b/contracts/interfaces/IOmnichainStaking.sol @@ -19,7 +19,7 @@ interface IOmnichainStaking is IVotes { // An omni-chain staking contract that allows users to stake their veNFT // and get some voting power. Once staked the voting power is available cross-chain. - function unstakeToken(uint256 tokenId) external; + function unstakeAndWithdraw(uint256 tokenId) external; // function totalSupply() external view returns (uint256); diff --git a/contracts/locker/staking/OmnichainStakingBase.sol b/contracts/locker/staking/OmnichainStakingBase.sol index 2adfb38..66211ca 100644 --- a/contracts/locker/staking/OmnichainStakingBase.sol +++ b/contracts/locker/staking/OmnichainStakingBase.sol @@ -168,24 +168,20 @@ abstract contract OmnichainStakingBase is * @dev Unstakes a regular token NFT and transfers it back to the user. * @param tokenId The ID of the regular token NFT to unstake. */ - function unstakeToken(uint256 tokenId) external updateReward(msg.sender) { - require(lockedByToken[tokenId] != address(0), "!tokenId"); - address lockedBy_ = lockedByToken[tokenId]; - if (_msgSender() != lockedBy_) - revert InvalidUnstaker(_msgSender(), lockedBy_); - - delete lockedByToken[tokenId]; - lockedTokenIdNfts[_msgSender()] = deleteAnElement( - lockedTokenIdNfts[_msgSender()], - tokenId - ); - - // reset and burn voting power - _burn(msg.sender, tokenPower[tokenId]); - tokenPower[tokenId] = 0; - votingPowerCombined.reset(msg.sender); + function unstakeToken(uint256 tokenId) external { + _unstakeToken(tokenId); + } - locker.safeTransferFrom(address(this), msg.sender, tokenId); + /** + * @notice A single withdraw function to unstake NFT, transfer it back to + * the user, and also withdraw all tokens for `tokenId` from the locker. + * @param tokenId The ID of the regular token NFT to unstake and withdraw. + */ + function unstakeAndWithdraw(uint256 tokenId) external nonReentrant { + _unstakeToken(tokenId); + uint256 lockedAmount = locker.locked(tokenId).amount; + locker.withdraw(tokenId); + assert(rewardsToken.transfer(msg.sender, lockedAmount)); } /** @@ -408,6 +404,29 @@ abstract contract OmnichainStakingBase is } } + /** + * @dev Unstakes a regular token NFT, doesn't send the NFT back to user. + * @param tokenId The ID of the regular token NFT to unstake. + */ + function _unstakeToken(uint256 tokenId) internal updateReward(msg.sender) { + address sender = msg.sender; + require(lockedByToken[tokenId] != address(0), "!tokenId"); + address lockedBy_ = lockedByToken[tokenId]; + if (sender != lockedBy_) + revert InvalidUnstaker(sender, lockedBy_); + + delete lockedByToken[tokenId]; + lockedTokenIdNfts[sender] = deleteAnElement( + lockedTokenIdNfts[sender], + tokenId + ); + + // reset and burn voting power + _burn(sender, tokenPower[tokenId]); + tokenPower[tokenId] = 0; + votingPowerCombined.reset(sender); + } + /** * @dev Deletes an element from an array. * @param elements The array to delete from. diff --git a/contracts/voter/PoolVoter.sol b/contracts/voter/PoolVoter.sol index b82f833..1812b94 100644 --- a/contracts/voter/PoolVoter.sol +++ b/contracts/voter/PoolVoter.sol @@ -42,11 +42,12 @@ contract PoolVoter is * @param _staking The address of the staking token (VE token). * @param _reward The address of the reward token. */ - function init(address _staking, address _reward) external reinitializer(1) { + function init(address _staking, address _reward, address _votingPowerCombined) external reinitializer(1) { staking = IVotes(_staking); reward = IERC20(_reward); __ReentrancyGuard_init(); __Ownable_init(msg.sender); + votingPowerCombined = _votingPowerCombined; } /** diff --git a/scripts/upgrade-poolVoter.ts b/scripts/upgrade-poolVoter.ts new file mode 100644 index 0000000..7d8e4d3 --- /dev/null +++ b/scripts/upgrade-poolVoter.ts @@ -0,0 +1,28 @@ +import {ethers, upgrades} from "hardhat"; + +const ZERO_TOKEN_ADDRESS = "0x78354f8DcCB269a615A7e0a24f9B0718FDC3C7A7"; +const OMNICHAIN_STAKING_ADDRESS = "0xf374229a18ff691406f99CCBD93e8a3f16B68888"; + +async function main() { + + const poolVoterProxy = "0x5346e9ab27D7874Db95993667D1Cb8338913f0aF" + const poolVoter = await ethers.getContractFactory("PoolVoter"); + + console.log("Upgradig to new PoolVoter implementation"); + + if (ZERO_TOKEN_ADDRESS.length && OMNICHAIN_STAKING_ADDRESS.length) { + + const upgradedPoolVoter = await upgrades.upgradeProxy(poolVoterProxy, poolVoter); + const tx = await upgradedPoolVoter.init(OMNICHAIN_STAKING_ADDRESS, ZERO_TOKEN_ADDRESS); + await tx.wait(); + + console.log("PoolVoter upgraded to", upgradedPoolVoter.address); + } else { + throw new Error("Invalid init arguments"); + } +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/test/fork/VotingPowerCombined/linea.test.ts b/test/fork/VotingPowerCombined/linea.test.ts new file mode 100644 index 0000000..5d29b0c --- /dev/null +++ b/test/fork/VotingPowerCombined/linea.test.ts @@ -0,0 +1,54 @@ +import { expect, should } from "chai"; +import { e18, initMainnetUser } from "../../fixtures/utils"; +import { VestedZeroNFT, VotingPowerCombined } from "../../../typechain-types"; +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; +import { time } from "@nomicfoundation/hardhat-network-helpers"; +import { setForkBlock } from "../utils"; +import { getNetworkDetails } from "../constants"; +import { + Contract, + ContractTransactionResponse, + parseEther, + parseUnits, +} from "ethers"; +import { ethers } from "hardhat"; +import { getGovernanceContracts } from "../helper"; + +const FORK = process.env.FORK === "true"; +const FORKED_NETWORK = process.env.FORKED_NETWORK ?? ""; +const BLOCK_NUMBER = 5969983; + +const STAKING_ADDRESS = "0xf374229a18ff691406f99CCBD93e8a3f16B68888"; +const REWARD_ADDRESS = "0x78354f8DcCB269a615A7e0a24f9B0718FDC3C7A7"; +const OWNER = "0x0F6e98A756A40dD050dC78959f45559F98d3289d"; + +if (FORK) { + let votingPowerCombined: VotingPowerCombined; + let deployerForked: SignerWithAddress; + describe.only("VotingPowerCombined ForkTests", async () => { + beforeEach(async () => { + votingPowerCombined = await ethers.getContractAt( + "VotingPowerCombined", + "0x2666951A62d82860E8e1385581E2FB7669097647" + ); + [deployerForked] = await ethers.getSigners(); + await setForkBlock(BLOCK_NUMBER); + + // + + const poolVoterFactory = await ethers.getContractFactory("PoolVoter"); + const poolVoter = await poolVoterFactory.deploy(); + await poolVoter.init(STAKING_ADDRESS, REWARD_ADDRESS, votingPowerCombined.target); + + const owner = await initMainnetUser(OWNER); + await votingPowerCombined.connect(owner).setAddresses(STAKING_ADDRESS, REWARD_ADDRESS, poolVoter.target); + }); + + it("Should reset voting power", async () => { + const resetterWallet = await initMainnetUser("0x7Ff4e6A2b7B43cEAB1fC07B0CBa00f834846ADEd", parseEther('100')); + + const resetTransaction = votingPowerCombined.connect(resetterWallet).reset(resetterWallet.address); + await expect(resetTransaction).to.not.be.revertedWith('Invalid reset performed'); + }); + }); +}