# Assignment
Try to figure out all the different vulnerabilities which are present under the contract and also provide recommendations on how to improve/safeguard the contract.
Note: There is a critical vulnerability under the withdraw function. Do make sure to
carefully study the code.
_______________________________________________________
# Solution
:::info
## Ownable.sol
```
contract Ownable {
address public owner;
event OwnershipTransferred(
address indexed previousOwner,
address indexed newOwner
);
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Ownable: caller is not the owner");
_;
}
function renounceOwnership() public onlyOwner {
emit OwnershipTransferred(owner, address(0));
owner = address(0);
}
function transferOwnership(address newOwner) public onlyOwner {
_transferOwnership(newOwner);
}
function _transferOwnership(address newOwner) internal {
require(
newOwner != address(0),
"Ownable: new owner is the zeroaddress"
);
emit OwnershipTransferred(owner, newOwner);
owner = newOwner;
}
}
```
#### Issue: Gas optimization
#### Severity: Low
#### Recommendation
Contract looks clean generally, however, I would consider the following slight changes for gas optimization:
:small_blue_diamond: replace the `require` checks in the mothod `_transferOwnership(address newOwner)`, and in the modifier `onlyOwner()`, by combining an `if` statement with a `revert` with the custom error.
:small_blue_diamond: avoid arithemetic computations using state variables `owner` directly. Declare a variable in memory and assign its value to the state variable variable. Now you can make computations with a reduced gas cost.
:small_blue_diamond: change the visibility of the `public` methods `renounceOwnership()` and `transferOwnership(address newOwner)` to `external`
#### :bulb: Improved code: Ownable
```
pragma solidity ^0.8.7;
error NonOwnerErr();
error ZeroAddressErr();
contract Ownable {
address public owner;
event OwnershipTransferred(
address indexed previousOwner,
address indexed newOwner
);
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
address owner_ = owner;
if (msg.sender != owner_) revert NonOwnerErr();
_;
}
function renounceOwnership() external onlyOwner {
emit OwnershipTransferred(owner, address(0));
owner = address(0);
}
function transferOwnership(address newOwner) external onlyOwner {
_transferOwnership(newOwner);
}
function _transferOwnership(address newOwner) internal {
if (newOwner == address(0)) revert ZeroAddressErr();
emit OwnershipTransferred(owner, newOwner);
owner = newOwner;
}
}
```
:::
_______________________________________________________
:::info
## IERC20.sol
```
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount)
external
returns (bool);
function allowance(address owner, address spender)
external
view
returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(
address sender,
address recipient,
uint256 amount
) external returns (bool);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(
address indexed owner,
address indexed spender,
uint256 value
);
}
```
#### Issue: None
:::
_______________________________________________________
:::info
## StakedWrapper.sol
```
contract StakedWrapper {
uint256 public totalSupply;
uint128 public buyback = 2; //defined in percentage
mapping(address => uint256) private _balances;
IERC20 public stakedToken;
event Staked(address indexed user, uint256 amount);
event Withdrawn(address indexed user, uint256 amount);
address public beneficiary =
address(0xDbfd6dAbD2Eaf53e5dBDc5E96fFB7E5E6B201F69);
function balanceOf(address account) public view returns (uint256) {
return _balances[account];
}
string constant _transferErrorMessage = "staked token transfer failed";
function stakeFor(address forWhom, uint128 amount) public payable virtual {
IERC20 st = stakedToken;
if (st == IERC20(address(0))) {
//eth
unchecked {
totalSupply += msg.value;
_balances[forWhom] += msg.value;
}
} else {
require(msg.value == 0, "Zero Eth not allowed");
require(amount > 0, "Stake should be greater than zero");
require(
st.transferFrom(msg.sender, address(this), amount),
_transferErrorMessage
);
unchecked {
totalSupply += amount;
_balances[forWhom] += amount;
}
}
emit Staked(forWhom, amount);
}
function withdraw(uint128 amount) public virtual {
require(amount <= _balances[msg.sender], "withdraw: balance islower");
unchecked {
_balances[msg.sender] -= amount;
totalSupply = totalSupply - amount;
}
IERC20 st = stakedToken;
if (st == IERC20(address(0))) {
//eth
uint128 val = (amount * buyback) / 100;
beneficiary.call{value: val}("");
(bool success_, ) = msg.sender.call{value: amount - val}("");
require(success_, "eth transfer failure");
} else {
require(
stakedToken.transfer(msg.sender, amount),
_transferErrorMessage
);
}
emit Withdrawn(msg.sender, amount);
}
}
```
#### Issue: Gas optimization
#### Severity: Low
#### Recommendation
I would consider the following changes for gas optimization:
:small_blue_diamond: replace the state variable `_transferErrorMessage` with a custom error.
:small_blue_diamond: replace the`require` checks in the mothods `stakeFor(address forWhom, uint128 amount)` and `withdraw(uint128 amount)`, by combining an `if` statement with `revert` and custom errors.
:small_blue_diamond: avoid using state variables `_balances` and `buyback` in conditionals and arithemetic computations directly respectively. Declare a variable in memory and assign its value to the state variable. Then you can use the conditional with a reduced gas cost.
:small_blue_diamond: create a private method `_handleTransfer(address who, uint128 amount)` and use it in the `withdraw(uint128 amount)` method for the two transfer transactions for the `beneficiary` and `msg.sender`.
#### Issue: Insufficient checks
#### Severity: Medium
#### Recommendation
The following vulnerability could potentially lead to an increased gas usage:
:small_blue_diamond:
the method `stakeFor(address forWhom, uint128 amount)` should have a check to restrict the caller from using it with 0 value sent. This would help protect the network from spam transactions.
#### Issue: Revenue omission
#### Severity: Medium
#### Recommendation
The following vulnerability could potentially lead to loss of `buyback` revenue:
:small_blue_diamond:
in the method `withdraw(uint128 amount)`, for stakes made in ERC20 tokens, there is no collection of fees for buyback as made available in native currency stakes. **This could be a bug or intentional**
#### Issue: Revenue omission
#### Severity: Critical
#### Recommendation
The following vulnerabilities would lead to a DoS and loss of funds:
:small_blue_diamond:
the state variable `beneficiary` should be made `payable` on declaration, or should be casted to payable in the method `withdraw(uint128 amount)` to avoid DoS.
:small_blue_diamond:
`msg.sender` in solidity 0.8 needs to be casted to payable (`payable(msg.sender)`) when transferring ether in the method `withdraw(uint128 amount)`.
#### :bulb: Improved code: StakedWrapper
```
pragma solidity ^0.8.7;
error TokenTransferErr(address caller);
error TransferErr(bytes data);
error LowValueErr();
error ValueNotAllowed();
error Insufficient(string message);
contract StakedWrapper {
uint256 public totalSupply;
uint128 public buyback = 2; //defined in percentage
mapping(address => uint256) private _balances;
IERC20 public stakedToken;
event Staked(address indexed user, uint256 amount);
event Withdrawn(address indexed user, uint256 amount);
address public beneficiary =
address(0xDbfd6dAbD2Eaf53e5dBDc5E96fFB7E5E6B201F69);
function balanceOf(address account) public view returns (uint256) {
return _balances[account];
}
function stakeFor(address forWhom, uint128 amount) public payable virtual {
IERC20 st = stakedToken;
if (st == IERC20(address(0))) {
//eth
if (msg.value == 0) revert LowValueErr();
unchecked {
totalSupply += msg.value;
_balances[forWhom] += msg.value;
}
} else {
if (msg.value > 0) revert ValueNotAllowed();
if (amount == 0) revert Insufficient("Amount");
if (!st.transferFrom(msg.sender, address(this), amount))
revert TokenTransferErr(address(st));
unchecked {
totalSupply += amount;
_balances[forWhom] += amount;
}
}
emit Staked(forWhom, amount);
}
function withdraw(uint128 amount) public virtual {
uint256 bal = _balances[msg.sender];
if (amount > bal) revert Insufficient("Amount");
unchecked {
_balances[msg.sender] -= amount;
totalSupply = totalSupply - amount;
}
IERC20 st = stakedToken;
if (st == IERC20(address(0))) {
//eth
uint128 bback = buyback;
uint128 val = (amount * bback) / 100;
_handleTransfer(payable(beneficiary), val);
_handleTransfer(payable(msg.sender), amount - val);
} else {
if (!stakedToken.transfer(msg.sender, amount))
revert TokenTransferErr(address(st));
}
emit Withdrawn(msg.sender, amount);
}
function _handleTransfer(address payable who, uint128 amount) private {
(bool success, bytes memory data) = who.call{value: amount}("");
if (!success) revert TransferErr(data);
}
}
```
:::
_______________________________________________________
:::info
## RewardsETH.sol
pragma solidity ^0.8.7;
error ExceededLimit(uint256 amount, uint256 limit);
```
contract RewardsETH is StakedTokenWrapper, Ownable {
IERC20 public rewardToken;
uint256 public rewardRate;
uint64 public periodFinish;
uint64 public lastUpdateTime;
uint128 public rewardPerTokenStored;
struct UserRewards {
uint128 userRewardPerTokenPaid;
uint128 rewards;
}
mapping(address => UserRewards) public userRewards;
event RewardAdded(uint256 reward);
event RewardPaid(address indexed user, uint256 reward);
uint256 public maxStakingAmount = 2 * 10**0 * 10**17; //0.2 ETH
constructor(IERC20 _rewardToken, IERC20 _stakedToken) {
rewardToken = _rewardToken;
stakedToken = _stakedToken;
}
modifier updateReward(address account) {
uint128 _rewardPerTokenStored = rewardPerToken();
lastUpdateTime = lastTimeRewardApplicable();
rewardPerTokenStored = _rewardPerTokenStored;
userRewards[account].rewards = earned(account);
userRewards[account].userRewardPerTokenPaid = _rewardPerTokenStored;
_;
}
function lastTimeRewardApplicable() public view returns (uint64) {
uint64 blockTimestamp = uint64(block.timestamp);
return blockTimestamp < periodFinish ? blockTimestamp : periodFinish;
}
function rewardPerToken() public view returns (uint128) {
uint256 totalStakedSupply = totalSupply;
if (totalStakedSupply == 0) {
return rewardPerTokenStored;
}
unchecked {
uint256 rewardDuration = lastTimeRewardApplicable() -
lastUpdateTime;
return
uint128(
rewardPerTokenStored +
(rewardDuration * rewardRate * 1e18) /
totalStakedSupply
);
}
}
function earned(address account) public view returns (uint128) {
unchecked {
return
uint128(
(balanceOf(account) *
(rewardPerToken() -
userRewards[account].userRewardPerTokenPaid)) /
1e18 +
userRewards[account].rewards
);
}
}
function stake(uint128 amount) external payable {
require(amount < maxStakingAmount, "amount exceed max staking amount");
stakeFor(msg.sender, amount);
}
function stakeFor(address forWhom, uint128 amount)
public
payable
override
updateReward(forWhom)
{
super.stakeFor(forWhom, amount);
}
function withdraw(uint128 amount) public override updateReward(msg.sender) {
super.withdraw(amount);
}
function exit() external {
getReward();
withdraw(uint128(balanceOf(msg.sender)));
}
function getReward() public updateReward(msg.sender) {
uint256 reward = earned(msg.sender);
if (reward > 0) {
userRewards[msg.sender].rewards = 0;
require(
rewardToken.transfer(msg.sender, reward),
"reward transfer failed"
);
emit RewardPaid(msg.sender, reward);
}
}
function setRewardParams(uint128 reward, uint64 duration)
external
onlyOwner
{
unchecked {
require(reward > 0);
rewardPerTokenStored = rewardPerToken();
uint64 blockTimestamp = uint64(block.timestamp);
uint256 maxRewardSupply = rewardToken.balanceOf(address(this));
if (rewardToken == stakedToken) maxRewardSupply -= totalSupply;
uint256 leftover = 0;
if (blockTimestamp >= periodFinish) {
rewardRate = reward / duration;
} else {
uint256 remaining = periodFinish - blockTimestamp;
leftover = remaining * rewardRate;
rewardRate = (reward + leftover) / duration;
}
require(reward + leftover <= maxRewardSupply, "not enough tokens");
lastUpdateTime = blockTimestamp;
periodFinish = blockTimestamp + duration;
emit RewardAdded(reward);
}
}
function withdrawReward() external onlyOwner {
uint256 rewardSupply = rewardToken.balanceOf(address(this));
//ensure funds staked by users can't be transferred out
if (rewardToken == stakedToken) rewardSupply -= totalSupply;
require(rewardToken.transfer(msg.sender, rewardSupply));
rewardRate = 0;
periodFinish = uint64(block.timestamp);
}
function setMaxStakingAmount(uint256 value) external onlyOwner {
require(value > 0);
maxStakingAmount = value;
}
function setBuyback(uint128 value) external onlyOwner {
buyback = value;
}
function setBuyBackAddr(address addr) external onlyOwner {
beneficiary = addr;
}
}
```
#### Issue: Undeclared importation
#### Severity: High
#### Recommendation
I would consider the following change for contract compilation:
:small_blue_diamond: replace the imported undeclared contract `StakedTokenWrapper` to `StakedWrapper`.
#### Issue: Gas optimization
#### Severity: low
#### Recommendation
I would consider the following changes for gas optimization.
:small_blue_diamond: replace the value of `maxStakingAmount` from `2 * 10**0 * 10**17` to `0.2 ether`.
:small_blue_diamond: create a new private method `_updateReward(address account)` and put all the code in the modifier `updateReward(address account)` inside this private method.
:small_blue_diamond: avoid using state variables `rewardRate` and `userRewards`, and `rewardToken` and `stakeToken` in arithemetic computations directly. Declare a variable in memory and assign its value to the state variable. Then you can use the conditional with a reduced gas cost.
:small_blue_diamond: replace the `require` checks in the mothods `stake(uint128 amount)`, `getReward()`, `setRewardParams(uint128 reward, uint64 duration)`, `withdrawReward()`, and `setMaxStakingAmount(uint256 value)` by combining an `if` statement with a `revert` with the custom error.
#### Issue: Insufficient Events
#### Severity: Low
#### Recommendation
Consider adding events to provide offchain information for future use on the following methods:
`setBuyBackAddr(address addr)`, `setBuyback(uint128 value)`, `setMaxStakingAmount(uint256 value)`, `withdrawReward()`, and `exit()`.
#### Issue: Insufficient checks
#### Severity: Medium
#### Recommendation
The following vulnerability could potentially mess up the staking logic:
:small_blue_diamond: the method `stakeFor(address forWhom, uint128 amount)` should have a check to restrict the caller from using it with a value greater than the `maxStakingAmount` as available in the method `stake(uint128 amount)`. This would help protect the network from spam transactions. Using a modifier would be the most optimized way of dealing with this to eliminate redundancy.
:small_blue_diamond: restrict users from calling the `getRewards()` method with zero rewards available for them.
#### Issue: Wrong call location
#### Severity: low
#### Recommendation
This issue would be critical if the value being sent is a native currency rather than an ERC20 standard asset.
:small_blue_diamond: it is always recommended to update the smart contract info before transferring value as shown in the method `withdrawReward()`
#### :bulb: Improved code: RewardsETH
```
error ExceededLimit(uint256 amount, uint256 limit);
contract RewardsETH is StakedWrapper, Ownable {
IERC20 public rewardToken;
uint256 public rewardRate;
uint64 public periodFinish;
uint64 public lastUpdateTime;
uint128 public rewardPerTokenStored;
struct UserRewards {
uint128 userRewardPerTokenPaid;
uint128 rewards;
}
mapping(address => UserRewards) public userRewards;
event SetBuyBackAddr(address indexed addr);
event SetBuyback(uint128 value);
event SetMaxStakingAmount(uint256 value);
event WithdrawReward(uint256 rewardSupply);
event Exit(address indexed user);
event RewardAdded(uint256 reward);
event RewardPaid(address indexed user, uint256 reward);
uint256 public maxStakingAmount = 0.2 ether; //0.2 ETH
constructor(IERC20 _rewardToken, IERC20 _stakedToken) {
rewardToken = _rewardToken;
stakedToken = _stakedToken;
}
modifier updateReward(address account) {
_updateReward(account);
_;
}
modifier limitCheck(uint256 amount) {
_limitCheck(amount);
_;
}
function _updateReward(address account) private {
uint128 _rewardPerTokenStored = rewardPerToken();
lastUpdateTime = lastTimeRewardApplicable();
rewardPerTokenStored = _rewardPerTokenStored;
userRewards[account].rewards = earned(account);
userRewards[account].userRewardPerTokenPaid = _rewardPerTokenStored;
}
function _limitCheck(uint256 amount) private view {
uint256 max = maxStakingAmount;
if (amount >= max) revert ExceededLimit(amount, max);
}
function lastTimeRewardApplicable() public view returns (uint64) {
uint64 blockTimestamp = uint64(block.timestamp);
return blockTimestamp < periodFinish ? blockTimestamp : periodFinish;
}
function rewardPerToken() public view returns (uint128) {
uint256 totalStakedSupply = totalSupply;
if (totalStakedSupply == 0) {
return rewardPerTokenStored;
}
unchecked {
uint256 rewardDuration = lastTimeRewardApplicable() -
lastUpdateTime;
uint256 rRate = rewardRate;
return
uint128(
rewardPerTokenStored +
(rewardDuration * rRate * 1e18) /
totalStakedSupply
);
}
}
function earned(address account) public view returns (uint128) {
UserRewards memory uRewards = userRewards[account];
unchecked {
return
uint128(
(balanceOf(account) *
(rewardPerToken() - uRewards.userRewardPerTokenPaid)) /
1e18 +
uRewards.rewards
);
}
}
function stake(uint128 amount) external payable limitCheck(amount) {
stakeFor(msg.sender, amount);
}
function stakeFor(address forWhom, uint128 amount)
public
payable
override
updateReward(forWhom)
limitCheck(amount)
{
super.stakeFor(forWhom, amount);
}
function withdraw(uint128 amount) public override updateReward(msg.sender) {
super.withdraw(amount);
}
function exit() external {
getReward();
withdraw(uint128(balanceOf(msg.sender)));
emit Exit(msg.sender);
}
function getReward() public updateReward(msg.sender) {
uint256 reward = earned(msg.sender);
if (reward == 0) revert Insufficient("Reward");
userRewards[msg.sender].rewards = 0;
if (!rewardToken.transfer(msg.sender, reward))
revert TokenTransferErr(address(rewardToken));
emit RewardPaid(msg.sender, reward);
}
function setRewardParams(uint128 reward, uint64 duration)
external
onlyOwner
{
unchecked {
if (reward == 0) revert Insufficient("Reward Param");
rewardPerTokenStored = rewardPerToken();
uint64 blockTimestamp = uint64(block.timestamp);
uint256 maxRewardSupply = rewardToken.balanceOf(address(this));
(IERC20 rToken, IERC20 sToken) = (rewardToken, stakedToken);
if (rToken == sToken) maxRewardSupply -= totalSupply;
uint256 leftover = 0;
if (blockTimestamp >= periodFinish) {
rewardRate = reward / duration;
} else {
uint256 remaining = periodFinish - blockTimestamp;
leftover = remaining * rewardRate;
rewardRate = (reward + leftover) / duration;
}
if (reward + leftover > maxRewardSupply)
revert Insufficient("Tokens");
lastUpdateTime = blockTimestamp;
periodFinish = blockTimestamp + duration;
emit RewardAdded(reward);
}
}
function withdrawReward() external onlyOwner {
uint256 rewardSupply = rewardToken.balanceOf(address(this));
//ensure funds staked by users can't be transferred out
(IERC20 rToken, IERC20 sToken) = (rewardToken, stakedToken);
if (rToken == sToken) rewardSupply -= totalSupply;
rewardRate = 0;
periodFinish = uint64(block.timestamp);
if (!rewardToken.transfer(msg.sender, rewardSupply))
revert TokenTransferErr(address(rewardToken));
emit WithdrawReward(rewardSupply);
}
function setMaxStakingAmount(uint256 value) external onlyOwner {
require(value > 0);
maxStakingAmount = value;
emit SetMaxStakingAmount(value);
}
function setBuyback(uint128 value) external onlyOwner {
buyback = value;
emit SetBuyback(value);
}
function setBuyBackAddr(address addr) external onlyOwner {
beneficiary = addr;
emit SetBuyBackAddr(addr);
}
}
```
:::