Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions protocol-contracts/staking/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ NUM_OPERATOR_STAKING_COPRO=2
# OperatorStaking coprocessor 1
OPERATOR_STAKING_COPRO_TOKEN_NAME_0="OperatorStakingCopro1"
OPERATOR_STAKING_COPRO_TOKEN_SYMBOL_0="stZAMAcopro1"
OPERATOR_STAKING_COPRO_OWNER_ADDRESS_0="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" # accounts[0] (address)

# Initial assets (in the smallest unit of $ZAMA, using 10^18 decimals) and receiver
OPERATOR_STAKING_COPRO_INITIAL_DEPOSIT_ASSETS_0=100000000000000000000000
Expand All @@ -72,7 +71,6 @@ OPERATOR_REWARDER_COPRO_FEE_0=2000
# OperatorStaking coprocessor 2
OPERATOR_STAKING_COPRO_TOKEN_NAME_1="OperatorStakingCopro2"
OPERATOR_STAKING_COPRO_TOKEN_SYMBOL_1="stZAMAcopro2"
OPERATOR_STAKING_COPRO_OWNER_ADDRESS_1="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" # accounts[0] (address)

# Initial assets (in the smallest unit of $ZAMA, using 10^18 decimals) and receiver
OPERATOR_STAKING_COPRO_INITIAL_DEPOSIT_ASSETS_1=250000000000000000000000
Expand All @@ -95,7 +93,6 @@ NUM_OPERATOR_STAKING_KMS=3
# OperatorStaking KMS 1
OPERATOR_STAKING_KMS_TOKEN_NAME_0="OperatorStakingKms1"
OPERATOR_STAKING_KMS_TOKEN_SYMBOL_0="stZAMAkms1"
OPERATOR_STAKING_KMS_OWNER_ADDRESS_0="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" # accounts[0] (address)

# Initial assets (in the smallest unit of $ZAMA, using 10^18 decimals) and receiver
OPERATOR_STAKING_KMS_INITIAL_DEPOSIT_ASSETS_0=500000000000000000000000
Expand All @@ -112,7 +109,6 @@ OPERATOR_REWARDER_KMS_FEE_0=1500
# OperatorStaking KMS 2
OPERATOR_STAKING_KMS_TOKEN_NAME_1="OperatorStakingKms2"
OPERATOR_STAKING_KMS_TOKEN_SYMBOL_1="stZAMAkms2"
OPERATOR_STAKING_KMS_OWNER_ADDRESS_1="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" # accounts[0] (address)

# Initial assets (in the smallest unit of $ZAMA, using 10^18 decimals) and receiver
OPERATOR_STAKING_KMS_INITIAL_DEPOSIT_ASSETS_1=750000000000000000000000
Expand All @@ -129,7 +125,6 @@ OPERATOR_REWARDER_KMS_FEE_1=1000
# OperatorStaking KMS 3
OPERATOR_STAKING_KMS_TOKEN_NAME_2="OperatorStakingKms3"
OPERATOR_STAKING_KMS_TOKEN_SYMBOL_2="stZAMAkms3"
OPERATOR_STAKING_KMS_OWNER_ADDRESS_2="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" # accounts[0] (address)

# Initial assets (in the smallest unit of $ZAMA, using 10^18 decimals) and receiver
OPERATOR_STAKING_KMS_INITIAL_DEPOSIT_ASSETS_2=1000000000000000000000000
Expand Down
24 changes: 19 additions & 5 deletions protocol-contracts/staking/contracts/OperatorRewarder.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

pragma solidity ^0.8.27;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
Expand All @@ -19,7 +18,7 @@ import {ProtocolStaking} from "./ProtocolStaking.sol";
* This contract receives rewards directly from `ProtocolStaking` and distributes them to `OperatorStaking` staker.
* The owner of this contract can opt to take a fee on the rewards.
*/
contract OperatorRewarder is Ownable {
contract OperatorRewarder {
using SafeERC20 for IERC20;
using Math for uint256;

Expand Down Expand Up @@ -60,6 +59,9 @@ contract OperatorRewarder is Ownable {
/// @notice Emitted when an address is not authorized to claim rewards on behalf of the receiver address.
error ClaimerNotAuthorized(address receiver, address claimer);

/// @notice Thrown when the caller is not the ProtocolStaking's owner.
error CallerNotProtocolStakingOwner(address caller);

/// @notice Error for unauthorized caller (not OperatorStaking).
error CallerNotOperatorStaking(address caller);

Expand Down Expand Up @@ -92,6 +94,11 @@ contract OperatorRewarder is Ownable {
_;
}

modifier onlyOwner() {
require(msg.sender == owner(), CallerNotProtocolStakingOwner(msg.sender));
_;
}

modifier onlyBeneficiary() {
require(msg.sender == _beneficiary, CallerNotBeneficiary(msg.sender));
_;
Expand All @@ -104,21 +111,19 @@ contract OperatorRewarder is Ownable {

/**
* @notice Initializes the OperatorRewarder contract.
* @param owner The owner address.
* @param beneficiary_ The address that can set and claim fees.
* @param protocolStaking_ The ProtocolStaking contract address.
* @param operatorStaking_ The OperatorStaking contract address.
* @param initialMaxFeeBasisPoints_ The initial max fee basis points.
* @param initialFeeBasisPoints_ The initial fee basis points.
*/
constructor(
address owner,
address beneficiary_,
ProtocolStaking protocolStaking_,
OperatorStaking operatorStaking_,
uint16 initialMaxFeeBasisPoints_,
uint16 initialFeeBasisPoints_
) Ownable(owner) {
) {
_transferBeneficiary(beneficiary_);
_token = IERC20(protocolStaking_.stakingToken());
_protocolStaking = protocolStaking_;
Expand Down Expand Up @@ -232,6 +237,15 @@ contract OperatorRewarder is Ownable {
}
}

/**
* @notice Returns the owner address, the ProtocolStaking owner address, which can set the
* beneficiary and max fee.
* @return TheProtocolStaking owner address.
*/
function owner() public view virtual returns (address) {
return protocolStaking().owner();
}

/**
* @notice Returns the beneficiary address, the address that can set and claim fees.
* @return The beneficiary address.
Expand Down
58 changes: 32 additions & 26 deletions protocol-contracts/staking/contracts/OperatorStaking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@

pragma solidity ^0.8.27;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
import {ERC1363} from "@openzeppelin/contracts/token/ERC20/extensions/ERC1363.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC1363} from "@openzeppelin/contracts/token/ERC20/extensions/ERC1363.sol";
import {ERC4626, IERC4626} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
Expand All @@ -28,7 +27,7 @@ import {ProtocolStaking} from "./ProtocolStaking.sol";
* may decrease due to slashing. These losses are symmetrically passed to delegators on the `OperatorStaking` level.
* Slashing must first decrease the `ProtocolStaking` balance of this contract before affecting pending withdrawals.
*/
contract OperatorStaking is ERC1363, Ownable, ReentrancyGuardTransient {
contract OperatorStaking is ERC1363, ReentrancyGuardTransient {
using Math for uint256;
using Checkpoints for Checkpoints.Trace208;

Expand Down Expand Up @@ -56,7 +55,10 @@ contract OperatorStaking is ERC1363, Ownable, ReentrancyGuardTransient {
/// @dev Emitted when the rewarder contract is set.
event RewarderSet(address oldRewarder, address newRewarder);

/// @dev Throw when the rewarder address is not valid during {setRewarder}.
/// @dev Thrown when the caller is not the ProtocolStaking's owner.
error CallerNotProtocolStakingOwner(address caller);

/// @dev Thrown when the rewarder address is not valid during {setRewarder}.
error InvalidRewarder(address rewarder);

/// @dev Thrown when the sender does not have authorization to perform an action.
Expand All @@ -65,39 +67,35 @@ contract OperatorStaking is ERC1363, Ownable, ReentrancyGuardTransient {
/// @dev Thrown when the controller address is not valid (e.g., zero address).
error InvalidController();

modifier onlyOwner() {
require(msg.sender == owner(), CallerNotProtocolStakingOwner(msg.sender));
_;
}

/**
* @notice Initializes the OperatorStaking contract.
* @param name The name of the ERC20 token.
* @param symbol The symbol of the ERC20 token.
* @param protocolStaking_ The ProtocolStaking contract address.
* @param owner The owner address.
* @param beneficiary The address that can set and claim fees.
* @param beneficiary_ The address that can set and claim fees.
* @param initialMaxFeeBasisPoints_ The initial maximum fee basis points for the OperatorRewarder contract.
* @param initialFeeBasisPoints_ The initial fee basis points for the OperatorRewarder contract.
*/
constructor(
string memory name,
string memory symbol,
ProtocolStaking protocolStaking_,
address owner,
address beneficiary,
address beneficiary_,
uint16 initialMaxFeeBasisPoints_,
uint16 initialFeeBasisPoints_
) ERC20(name, symbol) Ownable(owner) {
) ERC20(name, symbol) {
_asset = IERC20(protocolStaking_.stakingToken());
_protocolStaking = protocolStaking_;

IERC20(asset()).approve(address(protocolStaking_), type(uint256).max);

address rewarder_ = address(
new OperatorRewarder(
owner,
beneficiary,
protocolStaking_,
this,
initialMaxFeeBasisPoints_,
initialFeeBasisPoints_
)
new OperatorRewarder(beneficiary_, protocolStaking_, this, initialMaxFeeBasisPoints_, initialFeeBasisPoints_)
);
protocolStaking_.setRewardsRecipient(rewarder_);
_rewarder = rewarder_;
Expand Down Expand Up @@ -125,15 +123,15 @@ contract OperatorStaking is ERC1363, Ownable, ReentrancyGuardTransient {
* @notice Request to redeem shares for assets, subject to cooldown.
* @param shares Amount of shares to redeem.
* @param controller The controller address for the request.
* @param owner The owner of the shares.
* @param ownerRedeem The owner of the shares.
*/
function requestRedeem(uint208 shares, address controller, address owner) public virtual {
function requestRedeem(uint208 shares, address controller, address ownerRedeem) public virtual {
if (shares == 0) return;
require(controller != address(0), InvalidController());
if (msg.sender != owner) {
_spendAllowance(owner, msg.sender, shares);
if (msg.sender != ownerRedeem) {
_spendAllowance(ownerRedeem, msg.sender, shares);
}
_burn(owner, shares);
_burn(ownerRedeem, shares);

uint256 newTotalSharesInRedemption = totalSharesInRedemption() + shares;
_totalSharesInRedemption = newTotalSharesInRedemption;
Expand All @@ -149,7 +147,7 @@ contract OperatorStaking is ERC1363, Ownable, ReentrancyGuardTransient {
assert(releaseTime >= lastReleaseTime); // should never happen
_redeemRequests[controller].push(releaseTime, controllerSharesRedeemed + shares);

emit RedeemRequest(controller, owner, 0, msg.sender, shares, releaseTime);
emit RedeemRequest(controller, ownerRedeem, 0, msg.sender, shares, releaseTime);
}

/**
Expand Down Expand Up @@ -227,6 +225,14 @@ contract OperatorStaking is ERC1363, Ownable, ReentrancyGuardTransient {
emit OperatorSet(msg.sender, operator, approved);
}

/**
* @notice Returns the owner address, the ProtocolStaking owner address, which can set the rewarder.
* @return The owner address.
*/
function owner() public view virtual returns (address) {
return protocolStaking().owner();
}

/**
* @notice Returns the address of the staking asset.
* @return The asset address.
Expand Down Expand Up @@ -299,11 +305,11 @@ contract OperatorStaking is ERC1363, Ownable, ReentrancyGuardTransient {

/**
* @notice Returns the maximum redeemable shares for an owner.
* @param owner The owner address.
* @param ownerRedeem The owner address.
* @return The maximum redeemable shares.
*/
function maxRedeem(address owner) public view virtual returns (uint256) {
return claimableRedeemRequest(0, owner);
function maxRedeem(address ownerRedeem) public view virtual returns (uint256) {
return claimableRedeemRequest(0, ownerRedeem);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ async function deployOperatorStaking(
tokenName: string,
symbol: string,
protocolStakingAddress: string,
ownerAddress: string,
beneficiaryAddress: string,
initialMaxFeeBasisPoints: number,
initialFeeBasisPoints: number,
Expand All @@ -40,7 +39,6 @@ async function deployOperatorStaking(
tokenName,
symbol,
protocolStakingAddress,
ownerAddress,
beneficiaryAddress,
initialMaxFeeBasisPoints,
initialFeeBasisPoints,
Expand Down Expand Up @@ -93,7 +91,6 @@ task('task:deployOperatorStakingCopro')
// Get the env vars for the coprocessor operator staking contract
const coproTokenName = getRequiredEnvVar(`OPERATOR_STAKING_COPRO_TOKEN_NAME_${index}`);
const coproTokenSymbol = getRequiredEnvVar(`OPERATOR_STAKING_COPRO_TOKEN_SYMBOL_${index}`);
const coproOwnerAddress = getRequiredEnvVar(`OPERATOR_STAKING_COPRO_OWNER_ADDRESS_${index}`);

// Get the env vars for the coprocessor operator rewarder contract (deployed at the same time)
const coproBeneficiaryAddress = getRequiredEnvVar(`OPERATOR_REWARDER_COPRO_BENEFICIARY_${index}`);
Expand All @@ -104,7 +101,6 @@ task('task:deployOperatorStakingCopro')
coproTokenName,
coproTokenSymbol,
protocolStakingCoproProxyAddress,
coproOwnerAddress,
coproBeneficiaryAddress,
coproInitialMaxFeeBasisPoints,
coproInitialFeeBasisPoints,
Expand Down Expand Up @@ -133,7 +129,6 @@ task('task:deployOperatorStakingKMS')
// Get the env vars for the KMS operator staking contract
const kmsTokenName = getRequiredEnvVar(`OPERATOR_STAKING_KMS_TOKEN_NAME_${index}`);
const kmsTokenSymbol = getRequiredEnvVar(`OPERATOR_STAKING_KMS_TOKEN_SYMBOL_${index}`);
const kmsOwnerAddress = getRequiredEnvVar(`OPERATOR_STAKING_KMS_OWNER_ADDRESS_${index}`);

// Get the env vars for the KMS operator rewarder contract (deployed at the same time)
const kmsBeneficiaryAddress = getRequiredEnvVar(`OPERATOR_REWARDER_KMS_BENEFICIARY_${index}`);
Expand All @@ -144,7 +139,6 @@ task('task:deployOperatorStakingKMS')
kmsTokenName,
kmsTokenSymbol,
protocolStakingKMSProxyAddress,
kmsOwnerAddress,
kmsBeneficiaryAddress,
kmsInitialMaxFeeBasisPoints,
kmsInitialFeeBasisPoints,
Expand Down
25 changes: 21 additions & 4 deletions protocol-contracts/staking/test/OperatorRewarder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ describe('OperatorRewarder', function () {
'OPStake',
'OP',
protocolStaking,
admin.address,
beneficiary.address,
10000, // 100% maximum fee
0,
Expand Down Expand Up @@ -84,11 +83,30 @@ describe('OperatorRewarder', function () {

it('should not transfer beneficiary address if not owner', async function () {
await expect(this.mock.connect(this.anyone).transferBeneficiary(this.anyone.address))
.to.be.revertedWithCustomError(this.mock, 'OwnableUnauthorizedAccount')
.to.be.revertedWithCustomError(this.mock, 'CallerNotProtocolStakingOwner')
.withArgs(this.anyone.address);
});
});

describe('Access Control', function () {
it('should be same owner as ProtocolStaking owner', async function () {
const protocolStakingOwner = await this.protocolStaking.owner();
const operatorRewarderOwner = await this.mock.owner();

expect(operatorRewarderOwner).to.equal(protocolStakingOwner);
});

it('should update ProtocolStaking and OperatorStaking owner if ProtocolStaking owner is changed', async function () {
await this.protocolStaking.connect(this.admin).beginDefaultAdminTransfer(this.anyone);
await this.protocolStaking.connect(this.anyone).acceptDefaultAdminTransfer();

const protocolStakingOwner = await this.protocolStaking.owner();
const operatorRewarderOwner = await this.mock.owner();
expect(protocolStakingOwner).to.equal(this.anyone);
expect(operatorRewarderOwner).to.equal(this.anyone);
});
});

describe('View and claim delegator reward', async function () {
it('should give all to solo delegator', async function () {
await this.operatorStaking.connect(this.delegator1).deposit(ethers.parseEther('1'), this.delegator1);
Expand Down Expand Up @@ -380,7 +398,7 @@ describe('OperatorRewarder', function () {

it('should not set max fee if not owner', async function () {
await expect(this.mock.connect(this.anyone).setMaxFee(1234))
.to.be.revertedWithCustomError(this.mock, 'OwnableUnauthorizedAccount')
.to.be.revertedWithCustomError(this.mock, 'CallerNotProtocolStakingOwner')
.withArgs(this.anyone);
});

Expand Down Expand Up @@ -501,7 +519,6 @@ describe('OperatorRewarder', function () {
await timeIncreaseNoMine(9);

const newRewarder = await ethers.deployContract('OperatorRewarder', [
this.admin,
this.beneficiary.address,
this.protocolStaking,
this.operatorStaking,
Expand Down
Loading