diff --git a/README.md b/README.md index f683466..70d233a 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ The architecture consists of two contracts: through the Staking contract, keypers trust that the DAO will not set the minimum stake amount to an unreasonable value. -## Protocol Invariants [TBD] +## Protocol Invariants 1. On unstake, `keyperStake.timestamp + lockPeriod <= block.timestamp` if global `lockPeriod` is greater or equal to the stake lock period, otherwise `keyperStake.timestamp + keyperStake.lockPeriod <= block.timestamp`. 2. If `some(keyperStakes(keyper).length()) > 0` then `nextStakeId` != 0; diff --git a/docs/delegate-architecture.md b/docs/delegate-architecture.md index 73fc246..4640483 100644 --- a/docs/delegate-architecture.md +++ b/docs/delegate-architecture.md @@ -6,11 +6,10 @@ inherits from OpenZeppelin's ERC20VotesUpgradeable and OwnableUpgradeable. - The contract overrides the `transfer` and - `transferFrom` functions to prevent the sSHU token from being transferred. All + `transferFrom` functions to prevent the dSHU token from being transferred. All the other inherited functions follow the OpenZeppelin implementation. - To avoid rounding errors, the contract uses the FixedPointMathLib from Solmate library. -- The contract uses SafeTransferLib from solmate to interact with the SHU token. - The choosen mechanism for the rewards distribution is a ERC4626 vault implementation. ## Variables @@ -112,3 +111,5 @@ Get a list of stake ids belonging to a user. - The contract doesn't use the Ownable2Step pattern due to the 24KB contract size limit. +- The contract doesn't use safe transfer as the only token that can be + transferred is the SHU token, which is a trusted token. diff --git a/docs/rewards-distributor.md b/docs/rewards-distributor.md index 31bac2d..a7200bd 100644 --- a/docs/rewards-distributor.md +++ b/docs/rewards-distributor.md @@ -29,19 +29,41 @@ struct RewardConfiguration { ### `setRewardConfiguration(address receiver, uint256 emissionRate)` -Add, update or stop distributing rewards to a receiver. The emission rate is +Add, or update the distributing rewards to a receiver. The emission rate is the number of reward tokens distributed per second. This function can only be called by the Owner (DAO). If the emission rate for the specified receiver is not 0, the function will update the `emissionRate`. If the owner wants to stop -distributing rewards, they should set the emission rate to 0. +distributing rewards, they should call `removeRewardConfiguration`. + +### `removeRewardConfiguration(address receiver)` + +Remove the reward configuration for a specific receiver. This function can only +be called by the Owner. + +### `setRewardToken(address rewardToken)` + +This function can only be called by the Owner. +This function will first withdraw all the rewards from the previous reward +token and send them to the owner. Then it will set the new reward token +address. + +### `withdrawFunds(address token, address to, uint256 amount)` + +This function is useful in case someone transfer tokens to the contract by +mistake. The owner can withdraw any ERC20 token from the contract. ## Permissionless Functions -### `distributionRewards()` +### `collectRewards()` + +Distribute all the rewards to the receiver contract (msg.sender) accumulated until from the +`lastUpdate` timestamp to the current timestamp. + +### `collectRewardsTo(address receiver)` -Distribute all the rewards to the receiver contract accumulated until from the -`lastUpdate` timestamp to the current timestamp. If the msg.sender is not one of -the receivers, the function will revert. +Distribute all the rewards to the specified receiver contract accumulated until +from the `lastUpdate` timestamp to the current timestamp. If the receiver is +not a valid receiver, the function will revert. ## View Functions diff --git a/docs/staking-architecture.md b/docs/staking-architecture.md index 372a75e..6066eef 100644 --- a/docs/staking-architecture.md +++ b/docs/staking-architecture.md @@ -10,7 +10,6 @@ the other inherited functions follow the OpenZeppelin implementation. - To avoid rounding errors, the contract uses the FixedPointMathLib from Solmate library. -- The contract uses SafeTransferLib from solmate to interact with the SHU token. - The choosen mechanism for the rewards distribution is a ERC4626 vault implementation. ## Variables @@ -153,3 +152,5 @@ Get a list of stake ids belonging to a keyper. size limit. - If the Owner address gets compromised, the attacker can increase the minimum stake to a very high value, preventing keypers from unstaking their SHU tokens. +- The contract doesn't use safe transfer as the only token that can be + transferred is the SHU token, which is a trusted token. diff --git a/script/testnet/Constants.sol b/script/testnet/Constants.sol index d656f1a..d1b9224 100644 --- a/script/testnet/Constants.sol +++ b/script/testnet/Constants.sol @@ -5,11 +5,11 @@ uint256 constant MIN_STAKE = 50_000e18; uint256 constant REWARD_RATE = 0.1333333333e18; uint256 constant LOCK_PERIOD = 182 days; -address constant STAKING_CONTRACT_IMPL = 0x966aea71f391D044017143ab1D7e5DEd9a950e7e; -address constant STAKING_CONTRACT_PROXY = 0xe53a0850fDd90af0be3d4fDE02bD36C5EdFfc437; +address constant STAKING_CONTRACT_IMPL = 0xFaD109819176Ded391B663ceB621D24EF5E921d6; +address constant STAKING_CONTRACT_PROXY = 0x04c34f9c83A108153153a63CF2012761350B6667; address constant STAKING_TOKEN = 0xF2215e7eDfc4782D85BAfA06114f22A0654cA8aC; -address constant REWARDS_DISTRIBUTOR = 0x8aA01CcdEec887f0a6AF127b094702F283d244DE; -address constant DELEGATE_CONTRACT_IMPL = 0x82957f2a4270BCb3A544133c5A41F76ac4862CC3; -address constant DELEGATE_CONTRACT_PROXY = 0x46707609373E016D6F72fAA4c13cbFC9BF3AFF7c; +address constant REWARDS_DISTRIBUTOR = 0x2061c38E4F168294CcD989ecf427F44a77d9cC34; +address constant DELEGATE_CONTRACT_IMPL = 0x266ea1Ea3d1482cCd17dFb17E102dD8Ff2B26882; +address constant DELEGATE_CONTRACT_PROXY = 0x7F51584f23B61e4d3E4D1C8A4D5f8C39Acb53251; uint256 constant INITIAL_MINT = 10_000e18; diff --git a/src/BaseStaking.sol b/src/BaseStaking.sol index 9e8f20e..2fe421a 100644 --- a/src/BaseStaking.sol +++ b/src/BaseStaking.sol @@ -2,13 +2,13 @@ pragma solidity 0.8.26; import {OwnableUpgradeable} from "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; -import {ERC20VotesUpgradeable as ERC20} from "@openzeppelin-upgradeable/contracts/token/ERC20/extensions/ERC20VotesUpgradeable.sol"; +import {ERC20VotesUpgradeable as ERC20Votes} from "@openzeppelin-upgradeable/contracts/token/ERC20/extensions/ERC20VotesUpgradeable.sol"; import {EnumerableSetLib} from "./libraries/EnumerableSetLib.sol"; import {FixedPointMathLib} from "./libraries/FixedPointMathLib.sol"; import {IRewardsDistributor} from "./interfaces/IRewardsDistributor.sol"; -abstract contract BaseStaking is OwnableUpgradeable, ERC20 { +abstract contract BaseStaking is OwnableUpgradeable, ERC20Votes { /*////////////////////////////////////////////////////////////// LIBRARIES //////////////////////////////////////////////////////////////*/ @@ -22,7 +22,7 @@ abstract contract BaseStaking is OwnableUpgradeable, ERC20 { /// @notice the staking token, i.e. SHU /// @dev set in initialize, can't be changed - ERC20 public stakingToken; + ERC20Votes public stakingToken; /// @notice the rewards distributor contract /// @dev only owner can change @@ -96,8 +96,6 @@ abstract contract BaseStaking is OwnableUpgradeable, ERC20 { /// maximum withdrawable amount. The maximum withdrawable amount /// is the total amount of assets the user has minus the /// total locked amount - /// - If the claim results in a balance less than the total locked - /// amount, the claim will be rejected /// - The keyper can claim the rewards at any time as longs there is /// a reward to claim /// @param amount The amount of rewards to claim diff --git a/src/DelegateStaking.sol b/src/DelegateStaking.sol index 1573564..fb9fbb9 100644 --- a/src/DelegateStaking.sol +++ b/src/DelegateStaking.sol @@ -1,12 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; +import {ERC20VotesUpgradeable as ERC20Votes} from "@openzeppelin-upgradeable/contracts/token/ERC20/extensions/ERC20VotesUpgradeable.sol"; + import {BaseStaking} from "./BaseStaking.sol"; -import {IERC20} from "./interfaces/IERC20.sol"; import {EnumerableSetLib} from "./libraries/EnumerableSetLib.sol"; -import {FixedPointMathLib} from "./libraries/FixedPointMathLib.sol"; -import {IRewardsDistributor} from "./interfaces/IRewardsDistributor.sol"; -import {ERC20VotesUpgradeable as ERC20} from "@openzeppelin-upgradeable/contracts/token/ERC20/extensions/ERC20VotesUpgradeable.sol"; import {IRewardsDistributor} from "./interfaces/IRewardsDistributor.sol"; interface IStaking { @@ -126,7 +124,7 @@ contract DelegateStaking is BaseStaking { __ERC20_init("Delegated Staking SHU", "dSHU"); staking = IStaking(_stakingContract); - stakingToken = ERC20(_stakingToken); + stakingToken = ERC20Votes(_stakingToken); rewardsDistributor = IRewardsDistributor(_rewardsDistributor); lockPeriod = _lockPeriod; diff --git a/src/RewardsDistributor.sol b/src/RewardsDistributor.sol index 99c1094..bfc7282 100644 --- a/src/RewardsDistributor.sol +++ b/src/RewardsDistributor.sol @@ -3,15 +3,14 @@ pragma solidity 0.8.26; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {IRewardsDistributor} from "./interfaces/IRewardsDistributor.sol"; -import {SafeTransferLib} from "./libraries/SafeTransferLib.sol"; import {IERC20} from "./interfaces/IERC20.sol"; +/** + * @title Shutter Rewards Distributor Contract + * + * This contract lets the owner distribute rewards to the Staking and DelegateStaking contracts. + */ contract RewardsDistributor is Ownable, IRewardsDistributor { - /*////////////////////////////////////////////////////////////// - LIBRARIES - //////////////////////////////////////////////////////////////*/ - using SafeTransferLib for IERC20; - /*////////////////////////////////////////////////////////////// VARIABLES //////////////////////////////////////////////////////////////*/ @@ -30,7 +29,7 @@ contract RewardsDistributor is Ownable, IRewardsDistributor { } /*////////////////////////////////////////////////////////////// - MAPPINGS + MAPPINGS //////////////////////////////////////////////////////////////*/ mapping(address receiver => RewardConfiguration configuration) @@ -95,7 +94,7 @@ contract RewardsDistributor is Ownable, IRewardsDistributor { rewardConfiguration.lastUpdate = block.timestamp; // transfer the reward - rewardToken.safeTransfer(msg.sender, rewards); + rewardToken.transfer(msg.sender, rewards); emit RewardCollected(msg.sender, rewards); } @@ -129,7 +128,7 @@ contract RewardsDistributor is Ownable, IRewardsDistributor { rewardConfiguration.lastUpdate = block.timestamp; // transfer the reward - rewardToken.safeTransfer(receiver, rewards); + rewardToken.transfer(receiver, rewards); emit RewardCollected(receiver, rewards); } diff --git a/src/Staking.sol b/src/Staking.sol index eb4347d..636b1af 100644 --- a/src/Staking.sol +++ b/src/Staking.sol @@ -1,9 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; +import {ERC20VotesUpgradeable as ERC20Votes} from "@openzeppelin-upgradeable/contracts/token/ERC20/extensions/ERC20VotesUpgradeable.sol"; + import {BaseStaking} from "./BaseStaking.sol"; import {EnumerableSetLib} from "./libraries/EnumerableSetLib.sol"; -import {ERC20VotesUpgradeable as ERC20} from "@openzeppelin-upgradeable/contracts/token/ERC20/extensions/ERC20VotesUpgradeable.sol"; import {IRewardsDistributor} from "./interfaces/IRewardsDistributor.sol"; /** @@ -122,7 +123,7 @@ contract Staking is BaseStaking { __ERC20_init("Staked SHU", "sSHU"); minStake = _minStake; - stakingToken = ERC20(_stakingToken); + stakingToken = ERC20Votes(_stakingToken); rewardsDistributor = IRewardsDistributor(_rewardsDistributor); lockPeriod = _lockPeriod; diff --git a/src/libraries/SafeTransferLib.sol b/src/libraries/SafeTransferLib.sol deleted file mode 100644 index 3cc64e0..0000000 --- a/src/libraries/SafeTransferLib.sol +++ /dev/null @@ -1,97 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity 0.8.26; - -import {IERC20} from "../interfaces/IERC20.sol"; - -/// @notice Safe ETH and ERC20 transfer library that gracefully handles missing return values. -/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/SafeTransferLib.sol) -/// @dev Use with caution! Some functions in this library knowingly create dirty bits at the destination of the free memory pointer. -/// @dev Note that none of the functions in this library check that a token has code at all! That responsibility is delegated to the caller. -library SafeTransferLib { - /*////////////////////////////////////////////////////////////// - ERC20 OPERATIONS - //////////////////////////////////////////////////////////////*/ - - function safeTransferFrom( - IERC20 token, - address from, - address to, - uint256 amount - ) internal { - bool success; - - /// @solidity memory-safe-assembly - assembly { - // Get a pointer to some free memory. - let freeMemoryPointer := mload(0x40) - - // Write the abi-encoded calldata into memory, beginning with the function selector. - mstore( - freeMemoryPointer, - 0x23b872dd00000000000000000000000000000000000000000000000000000000 - ) - mstore( - add(freeMemoryPointer, 4), - and(from, 0xffffffffffffffffffffffffffffffffffffffff) - ) // Append and mask the "from" argument. - mstore( - add(freeMemoryPointer, 36), - and(to, 0xffffffffffffffffffffffffffffffffffffffff) - ) // Append and mask the "to" argument. - mstore(add(freeMemoryPointer, 68), amount) // Append the "amount" argument. Masking not required as it's a full 32 byte type. - - success := and( - // Set success to whether the call reverted, if not we check it either - // returned exactly 1 (can't just be non-zero data), or had no return data. - or( - and(eq(mload(0), 1), gt(returndatasize(), 31)), - iszero(returndatasize()) - ), - // We use 100 because the length of our calldata totals up like so: 4 + 32 * 3. - // We use 0 and 32 to copy up to 32 bytes of return data into the scratch space. - // Counterintuitively, this call must be positioned second to the or() call in the - // surrounding and() call or else returndatasize() will be zero during the computation. - call(gas(), token, 0, freeMemoryPointer, 100, 0, 32) - ) - } - - require(success, "TRANSFER_FROM_FAILED"); - } - - function safeTransfer(IERC20 token, address to, uint256 amount) internal { - bool success; - - /// @solidity memory-safe-assembly - assembly { - // Get a pointer to some free memory. - let freeMemoryPointer := mload(0x40) - - // Write the abi-encoded calldata into memory, beginning with the function selector. - mstore( - freeMemoryPointer, - 0xa9059cbb00000000000000000000000000000000000000000000000000000000 - ) - mstore( - add(freeMemoryPointer, 4), - and(to, 0xffffffffffffffffffffffffffffffffffffffff) - ) // Append and mask the "to" argument. - mstore(add(freeMemoryPointer, 36), amount) // Append the "amount" argument. Masking not required as it's a full 32 byte type. - - success := and( - // Set success to whether the call reverted, if not we check it either - // returned exactly 1 (can't just be non-zero data), or had no return data. - or( - and(eq(mload(0), 1), gt(returndatasize(), 31)), - iszero(returndatasize()) - ), - // We use 68 because the length of our calldata totals up like so: 4 + 32 * 2. - // We use 0 and 32 to copy up to 32 bytes of return data into the scratch space. - // Counterintuitively, this call must be positioned second to the or() call in the - // surrounding and() call or else returndatasize() will be zero during the computation. - call(gas(), token, 0, freeMemoryPointer, 68, 0, 32) - ) - } - - require(success, "TRANSFER_FAILED"); - } -}