diff --git a/contracts/contracts/mocks/MockSFC.sol b/contracts/contracts/mocks/MockSFC.sol new file mode 100644 index 0000000000..bb842adebf --- /dev/null +++ b/contracts/contracts/mocks/MockSFC.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./MintableERC20.sol"; + +contract MockSFC { + error ZeroAmount(); + error TransferFailed(); + + // Mapping of delegator address to validator ID to amount delegated + mapping(address => mapping(uint256 => uint256)) public delegations; + // Mapping of delegator address to validator ID to withdrawal request ID to amount + mapping(address => mapping(uint256 => mapping(uint256 => uint256))) + public withdraws; + + function getStake(address delegator, uint256 validatorID) + external + view + returns (uint256) + { + return delegations[delegator][validatorID]; + } + + function delegate(uint256 validatorID) external payable { + if (msg.value == 0) { + revert ZeroAmount(); + } + delegations[msg.sender][validatorID] += msg.value; + } + + function undelegate( + uint256 validatorID, + uint256 wrID, + uint256 amount + ) external { + require( + delegations[msg.sender][validatorID] >= amount, + "insufficient stake" + ); + require( + withdraws[msg.sender][validatorID][wrID] == 0, + "withdrawal request already exists" + ); + + delegations[msg.sender][validatorID] -= amount; + withdraws[msg.sender][validatorID][wrID] = amount; + } + + function withdraw(uint256 validatorID, uint256 wrID) external { + require(withdraws[msg.sender][validatorID][wrID] > 0, "no withdrawal"); + + (bool sent, ) = msg.sender.call{ + value: withdraws[msg.sender][validatorID][wrID] + }(""); + if (!sent) { + revert TransferFailed(); + } + } + + function pendingRewards(address delegator, uint256 validatorID) + external + view + returns (uint256) + {} + + function claimRewards(uint256 validatorID) external {} + + function restakeRewards(uint256 validatorID) external {} +} diff --git a/contracts/contracts/strategies/sonic/SonicValidatorDelegator.sol b/contracts/contracts/strategies/sonic/SonicValidatorDelegator.sol index 70be9bd48e..db5bb5cfaa 100644 --- a/contracts/contracts/strategies/sonic/SonicValidatorDelegator.sol +++ b/contracts/contracts/strategies/sonic/SonicValidatorDelegator.sol @@ -6,11 +6,10 @@ import { IVault } from "../../interfaces/IVault.sol"; import { ISFC } from "../../interfaces/sonic/ISFC.sol"; import { IWrappedSonic } from "../../interfaces/sonic/IWrappedSonic.sol"; -import "hardhat/console.sol"; - /** * @title Manages delegation to Sonic validators - * @notice This contract implements all the required functionality to delegate to, undelegate from and withdraw from validators. + * @notice This contract implements all the required functionality to delegate to, + undelegate from and withdraw from validators. * @author Origin Protocol Inc */ abstract contract SonicValidatorDelegator is InitializableAbstractStrategy { @@ -121,9 +120,11 @@ abstract contract SonicValidatorDelegator is InitializableAbstractStrategy { // For each supported validator, get the staked amount and pending rewards for (uint256 i = 0; i < supportedValidators.length; i++) { // Get the staked amount and any pending rewards - balance += - ISFC(sfc).getStake(address(this), supportedValidators[i]); - ISFC(sfc).pendingRewards(address(this), supportedValidators[i]); + balance += ISFC(sfc).getStake( + address(this), + supportedValidators[i] + ); + ISFC(sfc).pendingRewards(address(this), supportedValidators[i]); } } @@ -285,7 +286,7 @@ abstract contract SonicValidatorDelegator is InitializableAbstractStrategy { } /// @notice Returns the length of the supportedValidators array - function supportedValidatorsLength() external view returns(uint256) { + function supportedValidatorsLength() external view returns (uint256) { return supportedValidators.length; } diff --git a/contracts/deploy/sonic/000_mock.js b/contracts/deploy/sonic/000_mock.js index 627b6521de..cbaf69d446 100644 --- a/contracts/deploy/sonic/000_mock.js +++ b/contracts/deploy/sonic/000_mock.js @@ -1,8 +1,12 @@ -const { deployWithConfirmation } = require("../../utils/deploy"); +const { + deployWithConfirmation, + withConfirmation, +} = require("../../utils/deploy"); const { isFork, isSonic, oethUnits } = require("../../test/helpers"); const deployMocks = async () => { await deployWithConfirmation("MockWS", []); + await deployWithConfirmation("MockSFC", []); }; const deployOracleRouter = async () => { @@ -117,10 +121,60 @@ const deployCore = async () => { await cOSonicVault.connect(sGovernor).unpauseCapital(); }; +const deployStakingStrategy = async () => { + const { deployerAddr, governorAddr } = await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); + + const cWS = await ethers.getContract("MockWS"); + const cSFC = await ethers.getContract("MockSFC"); + + const cOSonicVaultProxy = await ethers.getContract("OSonicVaultProxy"); + + // Get contract instances + const cOSonicVault = await ethers.getContractAt( + "IVault", + cOSonicVaultProxy.address + ); + + // Staking Strategy + await deployWithConfirmation("SonicStakingStrategyProxy"); + + const cSonicStakingStrategyProxy = await ethers.getContract( + "SonicStakingStrategyProxy" + ); + const dSonicStakingStrategy = await deployWithConfirmation( + "SonicStakingStrategy", + [ + [cSFC.address, cOSonicVault.address], // platformAddress, VaultAddress + cWS.address, + cSFC.address, + ] + ); + const cSonicStakingStrategy = await ethers.getContractAt( + "SonicStakingStrategy", + cSonicStakingStrategyProxy.address + ); + + // Init the Sonic Staking Strategy + const initSonicStakingStrategy = + cSonicStakingStrategy.interface.encodeFunctionData("initialize()", []); + // prettier-ignore + await withConfirmation( + cSonicStakingStrategyProxy + .connect(sDeployer)["initialize(address,address,bytes)"]( + dSonicStakingStrategy.address, + governorAddr, + initSonicStakingStrategy + ) + ); + console.log("Initialized SonicStakingStrategy proxy and implementation"); +}; + const main = async () => { await deployMocks(); await deployOracleRouter(); await deployCore(); + await deployStakingStrategy(); }; main.id = "000_mock"; diff --git a/contracts/test/_fixture-sonic.js b/contracts/test/_fixture-sonic.js index d23875206c..917859f387 100644 --- a/contracts/test/_fixture-sonic.js +++ b/contracts/test/_fixture-sonic.js @@ -61,8 +61,13 @@ const defaultSonicFixture = deployments.createFixture(async () => { ); // Sonic staking strategy - const sonicStakingStrategyProxy = await ethers.getContract("SonicStakingStrategyProxy"); - const sonicStakingStrategy = await ethers.getContractAt("SonicStakingStrategy", sonicStakingStrategyProxy.address); + const sonicStakingStrategyProxy = await ethers.getContract( + "SonicStakingStrategyProxy" + ); + const sonicStakingStrategy = await ethers.getContractAt( + "SonicStakingStrategy", + sonicStakingStrategyProxy.address + ); // let dripper, harvester; // if (isFork) { @@ -106,13 +111,15 @@ const defaultSonicFixture = deployments.createFixture(async () => { ); const oSonicVaultSigner = await impersonateAndFund(oSonicVault.address); - let strategist, validatorRegistrator; - if (isFork) { - // Impersonate strategist on Fork - strategist = await impersonateAndFund(strategistAddr); - strategist.address = strategistAddr; + // Impersonate strategist + const strategist = await impersonateAndFund(strategistAddr); + strategist.address = strategistAddr; - validatorRegistrator = await impersonateAndFund(addresses.sonic.validatorRegistrator); + let validatorRegistrator; + if (isFork) { + validatorRegistrator = await impersonateAndFund( + addresses.sonic.validatorRegistrator + ); validatorRegistrator.address = addresses.sonic.validatorRegistrator; await impersonateAndFund(governor.address); diff --git a/contracts/test/behaviour/sfcStakingStrategy.js b/contracts/test/behaviour/sfcStakingStrategy.js index ef3743d963..45ed64bbf9 100644 --- a/contracts/test/behaviour/sfcStakingStrategy.js +++ b/contracts/test/behaviour/sfcStakingStrategy.js @@ -24,12 +24,13 @@ const { oethUnits } = require("../helpers"); const shouldBehaveLikeASFCStakingStrategy = (context) => { describe("Initial setup", function () { it("Should verify the initial state", async () => { - const { sonicStakingStrategy, addresses, oSonicVault, testValidatorIds } = await context(); + const { sonicStakingStrategy, addresses, oSonicVault, testValidatorIds } = + await context(); expect(await sonicStakingStrategy.wrappedSonic()).to.equal( addresses.wS, "Incorrect wrapped sonic address set" ); - + expect(await sonicStakingStrategy.sfc()).to.equal( addresses.SFC, "Incorrect SFC address set" @@ -41,10 +42,9 @@ const shouldBehaveLikeASFCStakingStrategy = (context) => { ); for (const validatorId of testValidatorIds) { - expect(await sonicStakingStrategy.isSupportedValidator(validatorId)).to.equal( - true, - "Validator expected to be supported" - ); + expect( + await sonicStakingStrategy.isSupportedValidator(validatorId) + ).to.equal(true, "Validator expected to be supported"); } expect(await sonicStakingStrategy.platformAddress()).to.equal( @@ -62,20 +62,18 @@ const shouldBehaveLikeASFCStakingStrategy = (context) => { "Harvester address not empty" ); - expect((await sonicStakingStrategy.getRewardTokenAddresses()).length).to.equal( - 0, - "Incorrectly configured Reward Token Addresses" - ); + expect( + (await sonicStakingStrategy.getRewardTokenAddresses()).length + ).to.equal(0, "Incorrectly configured Reward Token Addresses"); }); }); describe("Deposit/Delegation", function () { const depositTokenAmount = async (depositAmount) => { - const { sonicStakingStrategy, oSonicVaultSigner, wS, clement } = await context(); + const { sonicStakingStrategy, oSonicVaultSigner, wS, clement } = + await context(); - const wsBalanceBefore = await wS.balanceOf( - sonicStakingStrategy.address - ); + const wsBalanceBefore = await wS.balanceOf(sonicStakingStrategy.address); const strategyBalanceBefore = await sonicStakingStrategy.checkBalance( wS.address @@ -99,9 +97,7 @@ const shouldBehaveLikeASFCStakingStrategy = (context) => { wsBalanceBefore.add(depositAmount), "WS not transferred" ); - expect( - await sonicStakingStrategy.checkBalance(wS.address) - ).to.equal( + expect(await sonicStakingStrategy.checkBalance(wS.address)).to.equal( strategyBalanceBefore.add(depositAmount), "strategy checkBalance not increased" ); @@ -117,17 +113,12 @@ const shouldBehaveLikeASFCStakingStrategy = (context) => { ).to.be.revertedWith("unsupported function"); await expect( - sonicStakingStrategy - .connect(governor) - .collectRewardTokens() + sonicStakingStrategy.connect(governor).collectRewardTokens() ).to.be.revertedWith("unsupported function"); await expect( - sonicStakingStrategy - .connect(governor) - .removePToken(wS.address) + sonicStakingStrategy.connect(governor).removePToken(wS.address) ).to.be.revertedWith("unsupported function"); - }); it("Should accept and handle S token allocation", async () => { @@ -136,7 +127,12 @@ const shouldBehaveLikeASFCStakingStrategy = (context) => { }); it("Should accept and handle S token allocation and delegation to SFC", async () => { - const { sonicStakingStrategy, validatorRegistrator, testValidatorIds, wS } = await context(); + const { + sonicStakingStrategy, + validatorRegistrator, + testValidatorIds, + wS, + } = await context(); const depositAmount = oethUnits("15000"); await depositTokenAmount(depositAmount); @@ -150,15 +146,11 @@ const shouldBehaveLikeASFCStakingStrategy = (context) => { .withArgs(testValidatorIds[0], depositAmount); // checkBalance should account for the full amount delegated to the validator - expect( - await sonicStakingStrategy.checkBalance(wS.address) - ).to.equal( + expect(await sonicStakingStrategy.checkBalance(wS.address)).to.equal( depositAmount, "Strategy checkBalance not expected" ); }); - - }); }; diff --git a/contracts/test/strategies/sonicStaking.sonic.fork-test.js b/contracts/test/strategies/sonicStaking.sonic.fork-test.js index 2345107cc0..f8c035c26b 100644 --- a/contracts/test/strategies/sonicStaking.sonic.fork-test.js +++ b/contracts/test/strategies/sonicStaking.sonic.fork-test.js @@ -1,5 +1,7 @@ const { defaultSonicFixture } = require("./../_fixture-sonic"); -const { shouldBehaveLikeASFCStakingStrategy } = require("../behaviour/sfcStakingStrategy"); +const { + shouldBehaveLikeASFCStakingStrategy, +} = require("../behaviour/sfcStakingStrategy"); const addresses = require("../../utils/addresses"); describe("Sonic ForkTest: Sonic Staking Strategy", function () { @@ -14,10 +16,7 @@ describe("Sonic ForkTest: Sonic Staking Strategy", function () { return { ...fixture, addresses: addresses.sonic, - sfcAddress: await ethers.getContractAt( - "ISFC", - addresses.sonic.SFC - ), + sfcAddress: await ethers.getContractAt("ISFC", addresses.sonic.SFC), // see validators here: https://explorer.soniclabs.com/staking testValidatorIds: [15, 16, 17, 18], }; diff --git a/contracts/test/vault/os-vault.sonic.js b/contracts/test/vault/os-vault.sonic.js index a47f1e41cb..504df6940e 100644 --- a/contracts/test/vault/os-vault.sonic.js +++ b/contracts/test/vault/os-vault.sonic.js @@ -3,10 +3,6 @@ const { parseUnits } = require("ethers/lib/utils"); const { createFixtureLoader } = require("../_fixture"); const { defaultSonicFixture } = require("../_fixture-sonic"); -const addresses = require("../../utils/addresses"); -const { impersonateAndFund } = require("../../utils/signers"); -const { oethUnits } = require("../helpers"); -const { deployWithConfirmation } = require("../../utils/deploy"); const sonicFixture = createFixtureLoader(defaultSonicFixture); @@ -116,170 +112,37 @@ describe("Origin S Vault", function () { }); }); - describe.skip("Mint Whitelist", function () { + describe("Administer Sonic Staking Strategy", function () { beforeEach(async () => { fixture = await sonicFixture(); }); - it("Should allow a strategy to be added to the whitelist", async () => { - const { oSonicVault, governor } = fixture; + it("Should allow governor to set validator registrator", async () => { + const { sonicStakingStrategy, governor } = fixture; - // Pretend addresses.dead is a strategy - await oSonicVault.connect(governor).approveStrategy(addresses.dead); + // generate a wallet + const newRegistrator = ethers.Wallet.createRandom(); - const tx = oSonicVault + const tx = await sonicStakingStrategy .connect(governor) - .addStrategyToMintWhitelist(addresses.dead); + .setRegistrator(newRegistrator.address); - await expect(tx).to.emit(oSonicVault, "StrategyAddedToMintWhitelist"); - expect(await oSonicVault.isMintWhitelistedStrategy(addresses.dead)).to.be - .true; - }); - - it("Should allow a strategy to be removed from the whitelist", async () => { - const { oethbVault, governor } = fixture; - - // Pretend addresses.dead is a strategy - await oethbVault.connect(governor).approveStrategy(addresses.dead); - await oethbVault - .connect(governor) - .addStrategyToMintWhitelist(addresses.dead); - - // Remove it - const tx = oethbVault - .connect(governor) - .removeStrategyFromMintWhitelist(addresses.dead); - - await expect(tx).to.emit(oethbVault, "StrategyRemovedFromMintWhitelist"); - expect(await oethbVault.isMintWhitelistedStrategy(addresses.dead)).to.be - .false; - }); - - it("Should not allow non-governor to add to whitelist", async () => { - const { oethbVault, rafael } = fixture; - const tx = oethbVault - .connect(rafael) - .addStrategyToMintWhitelist(addresses.dead); - await expect(tx).to.be.revertedWith("Caller is not the Governor"); - }); - - it("Should not allow non-governor to remove from whitelist", async () => { - const { oethbVault, rafael } = fixture; - const tx = oethbVault - .connect(rafael) - .removeStrategyFromMintWhitelist(addresses.dead); - await expect(tx).to.be.revertedWith("Caller is not the Governor"); - }); - - it("Should not allow adding unapproved strategy", async () => { - const { oethbVault, governor } = fixture; - const tx = oethbVault - .connect(governor) - .addStrategyToMintWhitelist(addresses.dead); - await expect(tx).to.be.revertedWith("Strategy not approved"); - }); - - it("Should not whitelist if already whitelisted", async () => { - const { oethbVault, governor } = fixture; - - await oethbVault.connect(governor).approveStrategy(addresses.dead); - await oethbVault - .connect(governor) - .addStrategyToMintWhitelist(addresses.dead); - - const tx = oethbVault - .connect(governor) - .addStrategyToMintWhitelist(addresses.dead); - await expect(tx).to.be.revertedWith("Already whitelisted"); - }); - - it("Should revert when removing unwhitelisted strategy", async () => { - const { oethbVault, governor } = fixture; - - const tx = oethbVault - .connect(governor) - .removeStrategyFromMintWhitelist(addresses.dead); - await expect(tx).to.be.revertedWith("Not whitelisted"); - }); - }); - - describe.skip("Mint & Burn For Strategy", function () { - let strategySigner, mockStrategy; - - beforeEach(async () => { - fixture = await sonicFixture(); - const { oethbVault, governor } = fixture; - - mockStrategy = await deployWithConfirmation("MockStrategy"); - - await oethbVault.connect(governor).approveStrategy(mockStrategy.address); - await oethbVault - .connect(governor) - .addStrategyToMintWhitelist(mockStrategy.address); - strategySigner = await impersonateAndFund(mockStrategy.address); - }); - - it("Should allow a whitelisted strategy to mint and burn", async () => { - const { oethbVault, oethb } = fixture; - - await oethbVault.rebase(); - - const amount = oethUnits("1"); - - const supplyBefore = await oethb.totalSupply(); - await oethbVault.connect(strategySigner).mintForStrategy(amount); - expect(await oethb.balanceOf(mockStrategy.address)).to.eq(amount); - expect(await oethb.totalSupply()).to.eq(supplyBefore.add(amount)); - - await oethbVault.connect(strategySigner).burnForStrategy(amount); - expect(await oethb.balanceOf(mockStrategy.address)).to.eq(0); - expect(await oethb.totalSupply()).to.eq(supplyBefore); - }); - - it("Should not allow a non-supported strategy to mint", async () => { - const { oethbVault, governor } = fixture; - - const amount = oethUnits("1"); - - const tx = oethbVault.connect(governor).mintForStrategy(amount); - await expect(tx).to.be.revertedWith("Unsupported strategy"); - }); - - it("Should not allow a non-supported strategy to burn", async () => { - const { oethbVault, governor } = fixture; - - const amount = oethUnits("1"); - - const tx = oethbVault.connect(governor).burnForStrategy(amount); - await expect(tx).to.be.revertedWith("Unsupported strategy"); - }); - - it("Should not allow a non-white listed strategy to mint", async () => { - const { oethbVault, governor } = fixture; - - // Pretend addresses.dead is a strategy - await oethbVault.connect(governor).approveStrategy(addresses.dead); - - const amount = oethUnits("1"); - - const tx = oethbVault - .connect(await impersonateAndFund(addresses.dead)) - .mintForStrategy(amount); - await expect(tx).to.be.revertedWith("Not whitelisted strategy"); + await expect(tx) + .to.emit(sonicStakingStrategy, "RegistratorChanged") + .withArgs(newRegistrator.address); + expect(await sonicStakingStrategy.validatorRegistrator()).to.eq( + newRegistrator.address + ); }); - it("Should not allow a non-white listed strategy to burn", async () => { - const { oethbVault, governor } = fixture; - - // Pretend addresses.dead is a strategy - await oethbVault.connect(governor).approveStrategy(addresses.dead); - - const amount = oethUnits("1"); + it("Should not allow set validator registrator by non-Governor", async () => { + const { sonicStakingStrategy, strategist, nick, rafael } = fixture; - const tx = oethbVault - .connect(await impersonateAndFund(addresses.dead)) - .burnForStrategy(amount); - await expect(tx).to.be.revertedWith("Not whitelisted strategy"); + for (const signer of [strategist, nick, rafael]) { + await expect( + sonicStakingStrategy.connect(signer).setRegistrator(signer.address) + ).to.be.revertedWith("Caller is not the Governor"); + } }); }); });