From cb0005e20feaaf79236159820a150a7cabc2d43b Mon Sep 17 00:00:00 2001 From: Nazar Duchak Date: Thu, 23 Mar 2023 12:31:27 +0200 Subject: [PATCH 01/10] chore: add base --- src/Farm.sol | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Farm.sol b/src/Farm.sol index 94e3f03..83454cb 100644 --- a/src/Farm.sol +++ b/src/Farm.sol @@ -59,11 +59,6 @@ contract Farm is ReentrancyGuard { _; } - modifier onlyRewardsDistribution() { - require(msg.sender == rewardsDistribution, "Farm/not-rewards-distribution"); - _; - } - modifier updateReward(address account) { rewardPerTokenStored = rewardPerToken(); lastUpdateTime = lastTimeRewardApplicable(); @@ -172,17 +167,21 @@ contract Farm is ReentrancyGuard { function stake(uint256 amount) external nonReentrant notPaused updateReward(msg.sender) { require(amount > 0, "Farm/invalid-amount"); + _totalSupply = _totalSupply.add(amount); _balances[msg.sender] = _balances[msg.sender].add(amount); gem.transferFrom(msg.sender, address(this), amount); + emit Staked(msg.sender, amount); } function withdraw(uint256 amount) public nonReentrant updateReward(msg.sender) { require(amount > 0, "Farm/invalid-amount"); + _totalSupply = _totalSupply.sub(amount); _balances[msg.sender] = _balances[msg.sender].sub(amount); gem.transfer(msg.sender, amount); + emit Withdrawn(msg.sender, amount); } @@ -202,11 +201,15 @@ contract Farm is ReentrancyGuard { function recoverERC20(address token, uint256 amt, address to) external auth { require(token != address(gem), "Farm/gem-not-allowed"); + GemAbstract(token).transfer(to, amt); + emit Recovered(token, amt, to); } - function notifyRewardAmount(uint256 reward) external onlyRewardsDistribution updateReward(address(0)) { + function notifyRewardAmount(uint256 reward) external updateReward(address(0)) { + require(wards[msg.sender] == 1 || msg.sender == rewardsDistribution, "Farm/not-authorized"); + if (block.timestamp >= periodFinish) { rewardRate = reward.div(rewardsDuration); } else { @@ -224,6 +227,7 @@ contract Farm is ReentrancyGuard { lastUpdateTime = block.timestamp; periodFinish = block.timestamp.add(rewardsDuration); + emit RewardAdded(reward); } } From 67b7013d15ef6af50d421cca9f931de4d6afde50 Mon Sep 17 00:00:00 2001 From: Nazar Duchak Date: Thu, 23 Mar 2023 12:32:28 +0200 Subject: [PATCH 02/10] chore: add base --- src/Farm.t.sol | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/Farm.t.sol diff --git a/src/Farm.t.sol b/src/Farm.t.sol new file mode 100644 index 0000000..866f354 --- /dev/null +++ b/src/Farm.t.sol @@ -0,0 +1,15 @@ +pragma solidity ^0.8.14; + +import {Test} from "forge-std/Test.sol"; + +// import "ds-token/token.sol"; + +contract FarmTest is Test { + uint256 a = 1; +} + +// contract TestToken is DSToken { +// constructor(string memory symbol_, uint8 decimals_) public DSToken(symbol_) { +// decimals = decimals_; +// } +// } From 04b19f43ee278de759bb31e6e825a2669461d907 Mon Sep 17 00:00:00 2001 From: Nazar Duchak Date: Mon, 27 Mar 2023 14:56:36 +0300 Subject: [PATCH 03/10] chore: add tests --- .gitmodules | 3 + Makefile | 2 +- lib/ds-token | 1 + src/Farm.sol | 2 +- src/Farm.t.sol | 423 +++++++++++++++++++++++++++++++++- src/utils/ReentrancyGuard.sol | 6 +- src/utils/SafeMath.sol | 64 ++--- 7 files changed, 446 insertions(+), 55 deletions(-) create mode 160000 lib/ds-token diff --git a/.gitmodules b/.gitmodules index fbe83c7..636d835 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "lib/ds-token"] + path = lib/ds-token + url = https://github.com/dapphub/ds-token diff --git a/Makefile b/Makefile index 46d6d4e..0c289d0 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ # install solc version # example to install other versions: `make solc 0_8_14` -SOLC_VERSION := 0_8_14 +SOLC_VERSION := 0_6_12 solc:; nix-env -f https://github.com/dapphub/dapptools/archive/master.tar.gz -iA solc-static-versions.solc_${SOLC_VERSION} clean:; forge clean diff --git a/lib/ds-token b/lib/ds-token new file mode 160000 index 0000000..16f187a --- /dev/null +++ b/lib/ds-token @@ -0,0 +1 @@ +Subproject commit 16f187acc15dd839589be60173ad1ebd0716eb82 diff --git a/src/Farm.sol b/src/Farm.sol index 83454cb..67fbf5f 100644 --- a/src/Farm.sol +++ b/src/Farm.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.8.14; +pragma solidity ^0.6.12; import {GemAbstract} from "dss-interfaces/ERC/GemAbstract.sol"; import {SafeMath} from "./utils/SafeMath.sol"; diff --git a/src/Farm.t.sol b/src/Farm.t.sol index 866f354..fc8a13a 100644 --- a/src/Farm.t.sol +++ b/src/Farm.t.sol @@ -1,15 +1,418 @@ -pragma solidity ^0.8.14; +pragma experimental ABIEncoderV2; +pragma solidity ^0.6.12; -import {Test} from "forge-std/Test.sol"; - -// import "ds-token/token.sol"; +import "forge-std/Test.sol"; +import {DSToken} from "ds-token/token.sol"; +import {Farm} from "./Farm.sol"; contract FarmTest is Test { - uint256 a = 1; + uint256 internal constant WAD = 10 ** 18; + + TestToken rewardGem; + TestToken gem; + Farm farm; + + event Rely(address indexed usr); + event Deny(address indexed usr); + event PauseChanged(bool isPaused); + event RewardAdded(uint256 reward); + event Staked(address indexed user, uint256 amount); + event Withdrawn(address indexed user, uint256 amount); + event RewardPaid(address indexed user, uint256 reward); + event RewardsDurationUpdated(uint256 newDuration); + event Recovered(address token, uint256 amt, address to); + + function div(uint256 a, uint256 b) internal pure returns (uint256) { + require(b > 0, "ERR"); + return a / b; + } + + function setupReward(uint256 amt) internal { + rewardGem.mint(amt); + rewardGem.transfer(address(farm), amt); + farm.notifyRewardAmount(amt); + } + + function setupStakingToken(uint256 amt) internal { + gem.mint(amt); + gem.approve(address(farm), amt); + } + + function setUp() public { + rewardGem = new TestToken("SubDaoT", 18); + gem = new TestToken("MKR", 18); + + farm = new Farm(address(this), address(rewardGem), address(gem)); + } + + function testConstructor() public { + vm.expectEmit(true, true, true, true); + emit Rely(address(this)); + Farm f = new Farm(address(this), address(rewardGem), address(gem)); + + assertEq(address(f.rewardGem()), address(rewardGem)); + assertEq(address(f.gem()), address(gem)); + assertEq(f.rewardsDistribution(), address(this)); + assertEq(f.wards(address(this)), 1); + } + + function testRelyDeny() public { + assertEq(farm.wards(address(0)), 0); + + // -------------------- + vm.expectEmit(true, false, false, false); + emit Rely(address(0)); + + farm.rely(address(0)); + + assertEq(farm.wards(address(0)), 1); + + // -------------------- + vm.expectEmit(true, false, false, false); + emit Deny(address(0)); + + farm.deny(address(0)); + + assertEq(farm.wards(address(0)), 0); + } + + function testSetRewardDIstribution() public { + farm.setRewardsDistribution(address(0)); + assertEq(farm.rewardsDistribution(), address(0)); + } + + function testRevertOnUnauthorizedMethods() public { + vm.startPrank(address(0)); + + vm.expectRevert("Farm/not-authorized"); + farm.rely(address(0)); + + vm.expectRevert("Farm/not-authorized"); + farm.deny(address(0)); + + vm.expectRevert("Farm/not-authorized"); + farm.setRewardsDistribution(address(0)); + + vm.expectRevert("Farm/not-authorized"); + farm.setRewardsDuration(1 days); + + vm.expectRevert("Farm/not-authorized"); + farm.setPaused(true); + + vm.expectRevert("Farm/not-authorized"); + farm.recoverERC20(address(0), 1, address(0)); + } + + function testRevertWhenPausedMethods() public { + farm.setPaused(true); + + vm.expectRevert("Farm/is-paused"); + farm.stake(1); + } + + function testSetPause() public { + vm.expectEmit(true, true, true, true); + emit PauseChanged(true); + + farm.setPaused(true); + assertEq(farm.lastPauseTime(), block.timestamp); + } + + function testRevertOnRecoverStakingToken() public { + vm.expectRevert("Farm/gem-not-allowed"); + farm.recoverERC20(address(gem), 1, address(this)); + } + + function testRecoverERC20() public { + TestToken t = new TestToken("TT", 18); + t.mint(10); + t.transfer(address(farm), 10); + + assertEq(t.balanceOf(address(farm)), 10); + + vm.expectEmit(true, true, true, true); + emit Recovered(address(t), 10, address(this)); + + farm.recoverERC20(address(t), 10, address(this)); + + assertEq(t.balanceOf(address(farm)), 0); + assertEq(t.balanceOf(address(this)), 10); + } + + function testLastTimeRewardApplicable() public { + assertEq(farm.lastTimeRewardApplicable(), 0); + + setupReward(10 * WAD); + + assertEq(farm.lastTimeRewardApplicable(), block.timestamp); + } + + function testRewardPerToken() public { + assertEq(farm.rewardPerToken(), 0); + + setupStakingToken(100 * WAD); + farm.stake(100 * WAD); + + assertEq(farm.totalSupply(), 100 * WAD); + + setupReward(5000 * WAD); + + skip(1 days); + + assert(farm.rewardPerToken() > 0); + } + + function testStakeEmitEvent() public { + setupStakingToken(100 * WAD); + + vm.expectEmit(true, true, true, true); + emit Staked(address(this), 100 * WAD); + farm.stake(100 * WAD); + } + + function testStaking() public { + setupStakingToken(100 * WAD); + + uint256 gemBalance = gem.balanceOf(address(this)); + + farm.stake(100 * WAD); + + assertEq(farm.balanceOf(address(this)), 100 * WAD); + assertEq(gem.balanceOf(address(this)), gemBalance - 100 * WAD); + assertEq(gem.balanceOf(address(farm)), 100 * WAD); + } + + function testRevertOnZeroStake() public { + vm.expectRevert("Farm/invalid-amount"); + farm.stake(0); + } + + function testEarned() public { + assertEq(farm.earned(address(this)), 0); + + setupStakingToken(100 * WAD); + farm.stake(100 * WAD); + + setupReward(5000 * WAD); + + skip(1 days); + + assert(farm.earned(address(this)) > 0); + } + + function testRewardRateIncreaseOnNewRewardBeforeDurationEnd() public { + setupReward(5000 * WAD); + + uint256 rewardRate = farm.rewardRate(); + + setupReward(5000 * WAD); + + assert(rewardRate > 0); + assert(farm.rewardRate() > rewardRate); + } + + function earnedShouldIncreaseAfterDuration() public { + setupStakingToken(100 * WAD); + farm.stake(100 * WAD); + + setupStakingToken(5000 * WAD); + + skip(7 days); + + uint256 earned = farm.earned(address(this)); + + setupStakingToken(5000 * WAD); + + skip(7 days); + + assertEq(farm.earned(address(this)), earned + earned); + } + + function testGetRewardEvent() public { + setupStakingToken(100 * WAD); + farm.stake(100 * WAD); + + setupReward(5000 * WAD); + + skip(1 days); + + vm.expectEmit(true, true, true, true); + emit RewardPaid(address(this), farm.rewardRate() * 1 days); + farm.getReward(); + } + + function testGetReward() public { + setupStakingToken(100 * WAD); + farm.stake(100 * WAD); + + setupReward(5000 * WAD); + + skip(1 days); + + uint256 rewardBalance = rewardGem.balanceOf(address(this)); + uint256 earned = farm.earned(address(this)); + + farm.getReward(); + + assert(farm.earned(address(this)) < earned); + assert(rewardGem.balanceOf(address(this)) > rewardBalance); + } + + function testSetRewardDurationEvent() public { + vm.expectEmit(true, true, true, true); + emit RewardsDurationUpdated(70 days); + farm.setRewardsDuration(70 days); + } + + function testSetRewardDurationBeforeDistribution() public { + assertEq(farm.rewardsDuration(), 7 days); + + farm.setRewardsDuration(70 days); + + assertEq(farm.rewardsDuration(), 70 days); + } + + function testRevertSetRewardDurationOnActiveDistribution() public { + setupStakingToken(100 * WAD); + farm.stake(100 * WAD); + + setupReward(100 * WAD); + + skip(1 days); + + vm.expectRevert("Farm/period-no-finished"); + farm.setRewardsDuration(70 days); + } + + function testSetRewardDurationAfterDistributionPeriod() public { + setupStakingToken(100 * WAD); + farm.stake(100 * WAD); + + setupReward(100 * WAD); + + skip(8 days); + + farm.setRewardsDuration(70 days); + assertEq(farm.rewardsDuration(), 70 days); + } + + function testGetRewardForDuration() public { + setupReward(5000 * WAD); + + uint256 rewardForDuration = farm.getRewardForDuration(); + uint256 rewardDuration = farm.rewardsDuration(); + uint256 rewardRate = farm.rewardRate(); + + assert(rewardForDuration > 0); + assertEq(rewardForDuration, rewardRate * rewardDuration); + } + + function testWithdrawalEvent() public { + setupStakingToken(100 * WAD); + farm.stake(100 * WAD); + + vm.expectEmit(true, true, true, true); + emit Withdrawn(address(this), 1 * WAD); + farm.withdraw(1 * WAD); + } + + function testFailtIfNothingToWithdraw() public { + farm.withdraw(1); + } + + function testRevertOnZeroWithdraw() public { + vm.expectRevert("Farm/invalid-amount"); + farm.withdraw(0); + } + + function testWithdrwal() public { + setupStakingToken(100 * WAD); + farm.stake(100 * WAD); + + uint256 initialStakeBalance = farm.balanceOf(address(this)); + + farm.withdraw(100 * WAD); + + assertEq(initialStakeBalance, farm.balanceOf(address(this)) + 100 * WAD); + assertEq(gem.balanceOf(address(this)), 100 * WAD); + } + + function testExit() public { + setupStakingToken(100 * WAD); + farm.stake(100 * WAD); + + setupReward(500 * WAD); + + skip(1 days); + + farm.exit(); + + assertEq(farm.earned(address(this)), 0); + assertEq(gem.balanceOf(address(this)), 100 * WAD); + assertEq(rewardGem.balanceOf(address(this)), farm.rewardRate() * 1 days); + } + + function testNotifyRewardEvent() public { + vm.expectEmit(true, true, true, true); + emit RewardAdded(100 * WAD); + + setupReward(100 * WAD); + } + + function testRevertOnRewardGreaterThenBalance() public { + rewardGem.mint(100 * WAD); + rewardGem.transfer(address(farm), 100 * WAD); + + vm.expectRevert("Farm/invalid-reward"); + farm.notifyRewardAmount(101 * WAD); + } + + function testRevertOnRewardGreaterThenBalancePlusRollOverBalance() public { + setupReward(100 * WAD); + + rewardGem.mint(100 * WAD); + rewardGem.transfer(address(farm), 100 * WAD); + + vm.expectRevert("Farm/invalid-reward"); + farm.notifyRewardAmount(101 * WAD); + } + + function testFarm() public { + uint256 staked = 100 * WAD; + + setupStakingToken(staked); + farm.stake(staked); + + setupReward(5000 * WAD); + + // Period finish should be 7 days from now + assertEq(farm.periodFinish(), block.timestamp + 7 days); + + // Reward duration is 7 days, so we'll + // skip by 6 days to prevent expiration + skip(6 days); + + // Make sure we earned in proportion to reward per token + assertEq(farm.earned(address(this)), (farm.rewardPerToken() * staked) / WAD); + + // Make sure we get staking token after withdrawal and we still have the same amount earned + farm.withdraw(20 * WAD); + assertEq(gem.balanceOf(address(this)), 20 * WAD); + assertEq(farm.earned(address(this)), (farm.rewardPerToken() * staked) / WAD); + + // Get rewards + farm.getReward(); + assertEq(rewardGem.balanceOf(address(this)), (farm.rewardPerToken() * staked) / WAD); + assertEq(farm.earned(address(this)), 0); + + // exit + farm.exit(); + assertEq(gem.balanceOf(address(this)), staked); + } } -// contract TestToken is DSToken { -// constructor(string memory symbol_, uint8 decimals_) public DSToken(symbol_) { -// decimals = decimals_; -// } -// } +contract TestToken is DSToken { + constructor(string memory symbol_, uint8 decimals_) public DSToken(symbol_) { + decimals = decimals_; + } +} diff --git a/src/utils/ReentrancyGuard.sol b/src/utils/ReentrancyGuard.sol index bb8112b..557a687 100644 --- a/src/utils/ReentrancyGuard.sol +++ b/src/utils/ReentrancyGuard.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.8.0) (security/ReentrancyGuard.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.6.0; /** * @dev Contract module that helps prevent reentrant calls to a function. @@ -36,7 +36,7 @@ abstract contract ReentrancyGuard { uint256 private _status; - constructor() { + constructor() internal { _status = _NOT_ENTERED; } @@ -74,4 +74,4 @@ abstract contract ReentrancyGuard { function _reentrancyGuardEntered() internal view returns (bool) { return _status == _ENTERED; } -} \ No newline at end of file +} diff --git a/src/utils/SafeMath.sol b/src/utils/SafeMath.sol index 7f6c7d8..ce64ded 100644 --- a/src/utils/SafeMath.sol +++ b/src/utils/SafeMath.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.6.0) (utils/math/SafeMath.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.6.0; // CAUTION // This version of SafeMath should only be used with Solidity 0.8 or later, @@ -20,11 +20,9 @@ library SafeMath { * _Available since v3.4._ */ function tryAdd(uint256 a, uint256 b) internal pure returns (bool, uint256) { - unchecked { - uint256 c = a + b; - if (c < a) return (false, 0); - return (true, c); - } + uint256 c = a + b; + if (c < a) return (false, 0); + return (true, c); } /** @@ -33,10 +31,8 @@ library SafeMath { * _Available since v3.4._ */ function trySub(uint256 a, uint256 b) internal pure returns (bool, uint256) { - unchecked { - if (b > a) return (false, 0); - return (true, a - b); - } + if (b > a) return (false, 0); + return (true, a - b); } /** @@ -45,15 +41,13 @@ library SafeMath { * _Available since v3.4._ */ function tryMul(uint256 a, uint256 b) internal pure returns (bool, uint256) { - unchecked { - // Gas optimization: this is cheaper than requiring 'a' not being zero, but the - // benefit is lost if 'b' is also tested. - // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 - if (a == 0) return (true, 0); - uint256 c = a * b; - if (c / a != b) return (false, 0); - return (true, c); - } + // Gas optimization: this is cheaper than requiring 'a' not being zero, but the + // benefit is lost if 'b' is also tested. + // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 + if (a == 0) return (true, 0); + uint256 c = a * b; + if (c / a != b) return (false, 0); + return (true, c); } /** @@ -62,10 +56,8 @@ library SafeMath { * _Available since v3.4._ */ function tryDiv(uint256 a, uint256 b) internal pure returns (bool, uint256) { - unchecked { - if (b == 0) return (false, 0); - return (true, a / b); - } + if (b == 0) return (false, 0); + return (true, a / b); } /** @@ -74,10 +66,8 @@ library SafeMath { * _Available since v3.4._ */ function tryMod(uint256 a, uint256 b) internal pure returns (bool, uint256) { - unchecked { - if (b == 0) return (false, 0); - return (true, a % b); - } + if (b == 0) return (false, 0); + return (true, a % b); } /** @@ -166,10 +156,8 @@ library SafeMath { * - Subtraction cannot overflow. */ function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { - unchecked { - require(b <= a, errorMessage); - return a - b; - } + require(b <= a, errorMessage); + return a - b; } /** @@ -185,10 +173,8 @@ library SafeMath { * - The divisor cannot be zero. */ function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { - unchecked { - require(b > 0, errorMessage); - return a / b; - } + require(b > 0, errorMessage); + return a / b; } /** @@ -207,9 +193,7 @@ library SafeMath { * - The divisor cannot be zero. */ function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { - unchecked { - require(b > 0, errorMessage); - return a % b; - } + require(b > 0, errorMessage); + return a % b; } -} \ No newline at end of file +} From c81ae07008aa6ba004eb57d5fcb26dd127a0fdd6 Mon Sep 17 00:00:00 2001 From: Nazar Duchak Date: Mon, 27 Mar 2023 15:31:29 +0300 Subject: [PATCH 04/10] chore: add inline math --- src/Farm.sol | 76 +++++++++---- src/Farm.t.sol | 12 +- src/utils/ReentrancyGuard.sol | 77 ------------- src/utils/SafeMath.sol | 199 ---------------------------------- 4 files changed, 63 insertions(+), 301 deletions(-) delete mode 100644 src/utils/ReentrancyGuard.sol delete mode 100644 src/utils/SafeMath.sol diff --git a/src/Farm.sol b/src/Farm.sol index 67fbf5f..3f59f6b 100644 --- a/src/Farm.sol +++ b/src/Farm.sol @@ -1,12 +1,9 @@ pragma solidity ^0.6.12; import {GemAbstract} from "dss-interfaces/ERC/GemAbstract.sol"; -import {SafeMath} from "./utils/SafeMath.sol"; import {ReentrancyGuard} from "./utils/ReentrancyGuard.sol"; contract Farm is ReentrancyGuard { - using SafeMath for uint256; - GemAbstract public immutable rewardGem; GemAbstract public immutable gem; @@ -78,6 +75,10 @@ contract Farm is ReentrancyGuard { emit Rely(msg.sender); } + /*////////////////////////////////// + Authorization + //////////////////////////////////*/ + /** * @notice Grants `usr` admin access to this contract. * @param usr The user address. @@ -96,6 +97,10 @@ contract Farm is ReentrancyGuard { emit Deny(usr); } + /*////////////////////////////////// + Administration + //////////////////////////////////*/ + function setRewardsDuration(uint256 _rewardsDuration) external auth { require(block.timestamp > periodFinish, "Farm/period-no-finished"); rewardsDuration = _rewardsDuration; @@ -128,7 +133,9 @@ contract Farm is ReentrancyGuard { emit PauseChanged(paused); } - /* ========== VIEWS ========== */ + /*////////////////////////////////// + View + //////////////////////////////////*/ function totalSupply() external view returns (uint256) { return _totalSupply; @@ -147,45 +154,49 @@ contract Farm is ReentrancyGuard { return rewardPerTokenStored; } return - rewardPerTokenStored.add( - lastTimeRewardApplicable().sub(lastUpdateTime).mul(rewardRate).mul(1e18).div(_totalSupply) + _add( + rewardPerTokenStored, + _div(_mul(_sub(lastTimeRewardApplicable(), lastUpdateTime), rewardRate * 1e18), _totalSupply) ); } function earned(address account) public view returns (uint256) { return - _balances[account].mul(rewardPerToken().sub(userRewardPerTokenPaid[account])).div(1e18).add( + _add( + _div(_mul(_balances[account], _sub(rewardPerToken(), userRewardPerTokenPaid[account])), 1e18), rewards[account] ); } function getRewardForDuration() external view returns (uint256) { - return rewardRate.mul(rewardsDuration); + return _mul(rewardRate, rewardsDuration); } - /* ========== MUTATIVE FUNCTIONS ========== */ + /*////////////////////////////////// + Operations + //////////////////////////////////*/ - function stake(uint256 amount) external nonReentrant notPaused updateReward(msg.sender) { + function stake(uint256 amount) external notPaused updateReward(msg.sender) { require(amount > 0, "Farm/invalid-amount"); - _totalSupply = _totalSupply.add(amount); - _balances[msg.sender] = _balances[msg.sender].add(amount); + _totalSupply = _add(_totalSupply, amount); + _balances[msg.sender] = _add(_balances[msg.sender], amount); gem.transferFrom(msg.sender, address(this), amount); emit Staked(msg.sender, amount); } - function withdraw(uint256 amount) public nonReentrant updateReward(msg.sender) { + function withdraw(uint256 amount) public updateReward(msg.sender) { require(amount > 0, "Farm/invalid-amount"); - _totalSupply = _totalSupply.sub(amount); - _balances[msg.sender] = _balances[msg.sender].sub(amount); + _totalSupply = _sub(_totalSupply, amount); + _balances[msg.sender] = _sub(_balances[msg.sender], amount); gem.transfer(msg.sender, amount); emit Withdrawn(msg.sender, amount); } - function getReward() public nonReentrant updateReward(msg.sender) { + function getReward() public updateReward(msg.sender) { uint256 reward = rewards[msg.sender]; if (reward > 0) { rewards[msg.sender] = 0; @@ -211,11 +222,11 @@ contract Farm is ReentrancyGuard { require(wards[msg.sender] == 1 || msg.sender == rewardsDistribution, "Farm/not-authorized"); if (block.timestamp >= periodFinish) { - rewardRate = reward.div(rewardsDuration); + rewardRate = _div(reward, rewardsDuration); } else { - uint256 remaining = periodFinish.sub(block.timestamp); - uint256 leftover = remaining.mul(rewardRate); - rewardRate = reward.add(leftover).div(rewardsDuration); + uint256 remaining = _sub(periodFinish, block.timestamp); + uint256 leftover = _mul(remaining, rewardRate); + rewardRate = _div(_add(reward, leftover), rewardsDuration); } // Ensure the provided reward amount is not more than the balance in the contract. @@ -223,11 +234,32 @@ contract Farm is ReentrancyGuard { // very high values of rewardRate in the earned and rewardsPerToken functions; // Reward + leftover must be less than 2^256 / 10^18 to avoid overflow. uint balance = rewardGem.balanceOf(address(this)); - require(rewardRate <= balance.div(rewardsDuration), "Farm/invalid-reward"); + require(rewardRate <= _div(balance, rewardsDuration), "Farm/invalid-reward"); lastUpdateTime = block.timestamp; - periodFinish = block.timestamp.add(rewardsDuration); + periodFinish = _add(block.timestamp, rewardsDuration); emit RewardAdded(reward); } + + /*////////////////////////////////// + Math + //////////////////////////////////*/ + + function _add(uint256 x, uint256 y) internal pure returns (uint256 z) { + require((z = x + y) >= x, "Math/add-overflow"); + } + + function _sub(uint256 x, uint256 y) internal pure returns (uint256 z) { + require((z = x - y) <= x, "Math/sub-overflow"); + } + + function _mul(uint256 x, uint256 y) internal pure returns (uint256 z) { + require(y == 0 || (z = x * y) / y == x, "Math/mul-overflow"); + } + + function _div(uint x, uint y) internal pure returns (uint z) { + require(y > 0, "Math/divide-by-zero"); + return x / y; + } } diff --git a/src/Farm.t.sol b/src/Farm.t.sol index fc8a13a..5f75c67 100644 --- a/src/Farm.t.sol +++ b/src/Farm.t.sol @@ -8,9 +8,9 @@ import {Farm} from "./Farm.sol"; contract FarmTest is Test { uint256 internal constant WAD = 10 ** 18; - TestToken rewardGem; - TestToken gem; - Farm farm; + TestToken internal rewardGem; + TestToken internal gem; + Farm internal farm; event Rely(address indexed usr); event Deny(address indexed usr); @@ -359,6 +359,12 @@ contract FarmTest is Test { setupReward(100 * WAD); } + function testRevertOnNotBeingRewardDistributor() public { + vm.prank(address(0)); + vm.expectRevert("Farm/not-authorized"); + farm.notifyRewardAmount(1); + } + function testRevertOnRewardGreaterThenBalance() public { rewardGem.mint(100 * WAD); rewardGem.transfer(address(farm), 100 * WAD); diff --git a/src/utils/ReentrancyGuard.sol b/src/utils/ReentrancyGuard.sol deleted file mode 100644 index 557a687..0000000 --- a/src/utils/ReentrancyGuard.sol +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (security/ReentrancyGuard.sol) - -pragma solidity ^0.6.0; - -/** - * @dev Contract module that helps prevent reentrant calls to a function. - * - * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier - * available, which can be applied to functions to make sure there are no nested - * (reentrant) calls to them. - * - * Note that because there is a single `nonReentrant` guard, functions marked as - * `nonReentrant` may not call one another. This can be worked around by making - * those functions `private`, and then adding `external` `nonReentrant` entry - * points to them. - * - * TIP: If you would like to learn more about reentrancy and alternative ways - * to protect against it, check out our blog post - * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul]. - */ -abstract contract ReentrancyGuard { - // Booleans are more expensive than uint256 or any type that takes up a full - // word because each write operation emits an extra SLOAD to first read the - // slot's contents, replace the bits taken up by the boolean, and then write - // back. This is the compiler's defense against contract upgrades and - // pointer aliasing, and it cannot be disabled. - - // The values being non-zero value makes deployment a bit more expensive, - // but in exchange the refund on every call to nonReentrant will be lower in - // amount. Since refunds are capped to a percentage of the total - // transaction's gas, it is best to keep them low in cases like this one, to - // increase the likelihood of the full refund coming into effect. - uint256 private constant _NOT_ENTERED = 1; - uint256 private constant _ENTERED = 2; - - uint256 private _status; - - constructor() internal { - _status = _NOT_ENTERED; - } - - /** - * @dev Prevents a contract from calling itself, directly or indirectly. - * Calling a `nonReentrant` function from another `nonReentrant` - * function is not supported. It is possible to prevent this from happening - * by making the `nonReentrant` function external, and making it call a - * `private` function that does the actual work. - */ - modifier nonReentrant() { - _nonReentrantBefore(); - _; - _nonReentrantAfter(); - } - - function _nonReentrantBefore() private { - // On the first call to nonReentrant, _status will be _NOT_ENTERED - require(_status != _ENTERED, "ReentrancyGuard: reentrant call"); - - // Any calls to nonReentrant after this point will fail - _status = _ENTERED; - } - - function _nonReentrantAfter() private { - // By storing the original value once again, a refund is triggered (see - // https://eips.ethereum.org/EIPS/eip-2200) - _status = _NOT_ENTERED; - } - - /** - * @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a - * `nonReentrant` function in the call stack. - */ - function _reentrancyGuardEntered() internal view returns (bool) { - return _status == _ENTERED; - } -} diff --git a/src/utils/SafeMath.sol b/src/utils/SafeMath.sol deleted file mode 100644 index ce64ded..0000000 --- a/src/utils/SafeMath.sol +++ /dev/null @@ -1,199 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (utils/math/SafeMath.sol) - -pragma solidity ^0.6.0; - -// CAUTION -// This version of SafeMath should only be used with Solidity 0.8 or later, -// because it relies on the compiler's built in overflow checks. - -/** - * @dev Wrappers over Solidity's arithmetic operations. - * - * NOTE: `SafeMath` is generally not needed starting with Solidity 0.8, since the compiler - * now has built in overflow checking. - */ -library SafeMath { - /** - * @dev Returns the addition of two unsigned integers, with an overflow flag. - * - * _Available since v3.4._ - */ - function tryAdd(uint256 a, uint256 b) internal pure returns (bool, uint256) { - uint256 c = a + b; - if (c < a) return (false, 0); - return (true, c); - } - - /** - * @dev Returns the subtraction of two unsigned integers, with an overflow flag. - * - * _Available since v3.4._ - */ - function trySub(uint256 a, uint256 b) internal pure returns (bool, uint256) { - if (b > a) return (false, 0); - return (true, a - b); - } - - /** - * @dev Returns the multiplication of two unsigned integers, with an overflow flag. - * - * _Available since v3.4._ - */ - function tryMul(uint256 a, uint256 b) internal pure returns (bool, uint256) { - // Gas optimization: this is cheaper than requiring 'a' not being zero, but the - // benefit is lost if 'b' is also tested. - // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 - if (a == 0) return (true, 0); - uint256 c = a * b; - if (c / a != b) return (false, 0); - return (true, c); - } - - /** - * @dev Returns the division of two unsigned integers, with a division by zero flag. - * - * _Available since v3.4._ - */ - function tryDiv(uint256 a, uint256 b) internal pure returns (bool, uint256) { - if (b == 0) return (false, 0); - return (true, a / b); - } - - /** - * @dev Returns the remainder of dividing two unsigned integers, with a division by zero flag. - * - * _Available since v3.4._ - */ - function tryMod(uint256 a, uint256 b) internal pure returns (bool, uint256) { - if (b == 0) return (false, 0); - return (true, a % b); - } - - /** - * @dev Returns the addition of two unsigned integers, reverting on - * overflow. - * - * Counterpart to Solidity's `+` operator. - * - * Requirements: - * - * - Addition cannot overflow. - */ - function add(uint256 a, uint256 b) internal pure returns (uint256) { - return a + b; - } - - /** - * @dev Returns the subtraction of two unsigned integers, reverting on - * overflow (when the result is negative). - * - * Counterpart to Solidity's `-` operator. - * - * Requirements: - * - * - Subtraction cannot overflow. - */ - function sub(uint256 a, uint256 b) internal pure returns (uint256) { - return a - b; - } - - /** - * @dev Returns the multiplication of two unsigned integers, reverting on - * overflow. - * - * Counterpart to Solidity's `*` operator. - * - * Requirements: - * - * - Multiplication cannot overflow. - */ - function mul(uint256 a, uint256 b) internal pure returns (uint256) { - return a * b; - } - - /** - * @dev Returns the integer division of two unsigned integers, reverting on - * division by zero. The result is rounded towards zero. - * - * Counterpart to Solidity's `/` operator. - * - * Requirements: - * - * - The divisor cannot be zero. - */ - function div(uint256 a, uint256 b) internal pure returns (uint256) { - return a / b; - } - - /** - * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), - * reverting when dividing by zero. - * - * Counterpart to Solidity's `%` operator. This function uses a `revert` - * opcode (which leaves remaining gas untouched) while Solidity uses an - * invalid opcode to revert (consuming all remaining gas). - * - * Requirements: - * - * - The divisor cannot be zero. - */ - function mod(uint256 a, uint256 b) internal pure returns (uint256) { - return a % b; - } - - /** - * @dev Returns the subtraction of two unsigned integers, reverting with custom message on - * overflow (when the result is negative). - * - * CAUTION: This function is deprecated because it requires allocating memory for the error - * message unnecessarily. For custom revert reasons use {trySub}. - * - * Counterpart to Solidity's `-` operator. - * - * Requirements: - * - * - Subtraction cannot overflow. - */ - function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { - require(b <= a, errorMessage); - return a - b; - } - - /** - * @dev Returns the integer division of two unsigned integers, reverting with custom message on - * division by zero. The result is rounded towards zero. - * - * Counterpart to Solidity's `/` operator. Note: this function uses a - * `revert` opcode (which leaves remaining gas untouched) while Solidity - * uses an invalid opcode to revert (consuming all remaining gas). - * - * Requirements: - * - * - The divisor cannot be zero. - */ - function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { - require(b > 0, errorMessage); - return a / b; - } - - /** - * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), - * reverting with custom message when dividing by zero. - * - * CAUTION: This function is deprecated because it requires allocating memory for the error - * message unnecessarily. For custom revert reasons use {tryMod}. - * - * Counterpart to Solidity's `%` operator. This function uses a `revert` - * opcode (which leaves remaining gas untouched) while Solidity uses an - * invalid opcode to revert (consuming all remaining gas). - * - * Requirements: - * - * - The divisor cannot be zero. - */ - function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { - require(b > 0, errorMessage); - return a % b; - } -} From 690d544a418ff7ff40e3e1b7a9c6ed0abdb74f54 Mon Sep 17 00:00:00 2001 From: Nazar Duchak Date: Mon, 27 Mar 2023 15:34:02 +0300 Subject: [PATCH 05/10] chore: add remmapings to gitignore --- .gitignore | 3 +++ src/Farm.sol | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 22f4761..7f15958 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,6 @@ report/ # Node.js deps node_modules/ + +# remappings +remappings.txt diff --git a/src/Farm.sol b/src/Farm.sol index 3f59f6b..f7aa14b 100644 --- a/src/Farm.sol +++ b/src/Farm.sol @@ -1,7 +1,6 @@ pragma solidity ^0.6.12; import {GemAbstract} from "dss-interfaces/ERC/GemAbstract.sol"; -import {ReentrancyGuard} from "./utils/ReentrancyGuard.sol"; contract Farm is ReentrancyGuard { GemAbstract public immutable rewardGem; From 4060b95cbabc8b967953300767beeac7912dd8b4 Mon Sep 17 00:00:00 2001 From: Nazar Duchak Date: Mon, 27 Mar 2023 19:16:28 +0300 Subject: [PATCH 06/10] chore: fix --- src/Farm.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Farm.sol b/src/Farm.sol index f7aa14b..2c1a8f4 100644 --- a/src/Farm.sol +++ b/src/Farm.sol @@ -2,7 +2,7 @@ pragma solidity ^0.6.12; import {GemAbstract} from "dss-interfaces/ERC/GemAbstract.sol"; -contract Farm is ReentrancyGuard { +contract Farm { GemAbstract public immutable rewardGem; GemAbstract public immutable gem; From 50c462bcd11ba522cb7b990c960c3ee0d7fe5bb5 Mon Sep 17 00:00:00 2001 From: Nazar Duchak Date: Thu, 30 Mar 2023 16:30:28 +0300 Subject: [PATCH 07/10] chore: use pattern for setters --- src/Farm.sol | 48 +++++++++++++++++++++++++++++++++++++++++------- src/Farm.t.sol | 29 +++++++++++++++-------------- 2 files changed, 56 insertions(+), 21 deletions(-) diff --git a/src/Farm.sol b/src/Farm.sol index 2c1a8f4..753b3d3 100644 --- a/src/Farm.sol +++ b/src/Farm.sol @@ -34,12 +34,24 @@ contract Farm { * @param usr The user address. */ event Deny(address indexed usr); + /** + * @notice A contract parameter was updated. + * @param what The changed parameter name. Currently the supported values are: "rewardDuration". + * @param data The new value of the parameter. + */ + event File(bytes32 indexed what, uint256 data); + /** + * @notice A contract parameter was updated. + * @param what The changed parameter name. Currently the supported values are: "rewardsDistribution". + * @param data The new value of the parameter. + */ + event File(bytes32 indexed what, address data); + event PauseChanged(bool isPaused); event RewardAdded(uint256 reward); event Staked(address indexed user, uint256 amount); event Withdrawn(address indexed user, uint256 amount); event RewardPaid(address indexed user, uint256 reward); - event RewardsDurationUpdated(uint256 newDuration); event Recovered(address token, uint256 amt, address to); /** @@ -100,14 +112,36 @@ contract Farm { Administration //////////////////////////////////*/ - function setRewardsDuration(uint256 _rewardsDuration) external auth { - require(block.timestamp > periodFinish, "Farm/period-no-finished"); - rewardsDuration = _rewardsDuration; - emit RewardsDurationUpdated(rewardsDuration); + /** + * @notice Updates a contract parameter. + * @dev Reward duration can be updated only when previouse distribution is done + * @param what The changed parameter name. `rewardDuration` + * @param data The new value of the parameter. + */ + function file(bytes32 what, uint256 data) external auth { + if (what == "rewardDuration") { + require(block.timestamp > periodFinish, "Farm/period-no-finished"); + rewardsDuration = data; + } else { + revert("Farm/unrecognised-param"); + } + + emit File(what, data); } - function setRewardsDistribution(address _rewardsDistribution) external auth { - rewardsDistribution = _rewardsDistribution; + /** + * @notice Updates a contract parameter. + * @param what The changed parameter name. `rewardDistribution` + * @param data The new value of the parameter. + */ + function file(bytes32 what, address data) external auth { + if (what == "rewardDistribution") { + rewardsDistribution = data; + } else { + revert("Farm/unrecognised-param"); + } + + emit File(what, data); } /** diff --git a/src/Farm.t.sol b/src/Farm.t.sol index 5f75c67..6a198aa 100644 --- a/src/Farm.t.sol +++ b/src/Farm.t.sol @@ -19,8 +19,9 @@ contract FarmTest is Test { event Staked(address indexed user, uint256 amount); event Withdrawn(address indexed user, uint256 amount); event RewardPaid(address indexed user, uint256 reward); - event RewardsDurationUpdated(uint256 newDuration); event Recovered(address token, uint256 amt, address to); + event File(bytes32 indexed what, uint256 data); + event File(bytes32 indexed what, address data); function div(uint256 a, uint256 b) internal pure returns (uint256) { require(b > 0, "ERR"); @@ -76,8 +77,8 @@ contract FarmTest is Test { assertEq(farm.wards(address(0)), 0); } - function testSetRewardDIstribution() public { - farm.setRewardsDistribution(address(0)); + function testFileRewardDistribution() public { + farm.file(bytes32("rewardDistribution"), address(0)); assertEq(farm.rewardsDistribution(), address(0)); } @@ -91,10 +92,10 @@ contract FarmTest is Test { farm.deny(address(0)); vm.expectRevert("Farm/not-authorized"); - farm.setRewardsDistribution(address(0)); + farm.file(bytes32("rewardsDistribution"), address(0)); vm.expectRevert("Farm/not-authorized"); - farm.setRewardsDuration(1 days); + farm.file(bytes32("rewardsDuration"), 1 days); vm.expectRevert("Farm/not-authorized"); farm.setPaused(true); @@ -258,21 +259,21 @@ contract FarmTest is Test { assert(rewardGem.balanceOf(address(this)) > rewardBalance); } - function testSetRewardDurationEvent() public { + function testFileRewardDurationEvent() public { vm.expectEmit(true, true, true, true); - emit RewardsDurationUpdated(70 days); - farm.setRewardsDuration(70 days); + emit File(bytes32("rewardDuration"), 70 days); + farm.file(bytes32("rewardDuration"), 70 days); } - function testSetRewardDurationBeforeDistribution() public { + function testFileRewardDurationBeforeDistribution() public { assertEq(farm.rewardsDuration(), 7 days); - farm.setRewardsDuration(70 days); + farm.file(bytes32("rewardDuration"), 70 days); assertEq(farm.rewardsDuration(), 70 days); } - function testRevertSetRewardDurationOnActiveDistribution() public { + function testRevertFileRewardDurationOnActiveDistribution() public { setupStakingToken(100 * WAD); farm.stake(100 * WAD); @@ -281,10 +282,10 @@ contract FarmTest is Test { skip(1 days); vm.expectRevert("Farm/period-no-finished"); - farm.setRewardsDuration(70 days); + farm.file(bytes32("rewardDuration"), 70 days); } - function testSetRewardDurationAfterDistributionPeriod() public { + function testFileRewardDurationAfterDistributionPeriod() public { setupStakingToken(100 * WAD); farm.stake(100 * WAD); @@ -292,7 +293,7 @@ contract FarmTest is Test { skip(8 days); - farm.setRewardsDuration(70 days); + farm.file(bytes32("rewardDuration"), 70 days); assertEq(farm.rewardsDuration(), 70 days); } From 24a8d28213d695c419ee4aa3b2226c58e9004d55 Mon Sep 17 00:00:00 2001 From: Nazar Duchak Date: Thu, 30 Mar 2023 16:39:54 +0300 Subject: [PATCH 08/10] chore: use solmate. upgrade solidity to 0.8.14 --- .gitmodules | 3 +++ Makefile | 2 +- lib/solmate | 1 + src/Farm.sol | 4 ++-- src/Farm.t.sol | 12 +++++++----- 5 files changed, 14 insertions(+), 8 deletions(-) create mode 160000 lib/solmate diff --git a/.gitmodules b/.gitmodules index 636d835..3a51f01 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "lib/ds-token"] path = lib/ds-token url = https://github.com/dapphub/ds-token +[submodule "lib/solmate"] + path = lib/solmate + url = https://github.com/transmissions11/solmate diff --git a/Makefile b/Makefile index 0c289d0..46d6d4e 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ # install solc version # example to install other versions: `make solc 0_8_14` -SOLC_VERSION := 0_6_12 +SOLC_VERSION := 0_8_14 solc:; nix-env -f https://github.com/dapphub/dapptools/archive/master.tar.gz -iA solc-static-versions.solc_${SOLC_VERSION} clean:; forge clean diff --git a/lib/solmate b/lib/solmate new file mode 160000 index 0000000..2001af4 --- /dev/null +++ b/lib/solmate @@ -0,0 +1 @@ +Subproject commit 2001af43aedb46fdc2335d2a7714fb2dae7cfcd1 diff --git a/src/Farm.sol b/src/Farm.sol index 753b3d3..8b38e1c 100644 --- a/src/Farm.sol +++ b/src/Farm.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.6.12; +pragma solidity ^0.8.14; import {GemAbstract} from "dss-interfaces/ERC/GemAbstract.sol"; @@ -77,7 +77,7 @@ contract Farm { _; } - constructor(address _rewardsDistribution, address _rewardGem, address _gem) public { + constructor(address _rewardsDistribution, address _rewardGem, address _gem) { rewardGem = GemAbstract(_rewardGem); gem = GemAbstract(_gem); rewardsDistribution = _rewardsDistribution; diff --git a/src/Farm.t.sol b/src/Farm.t.sol index 6a198aa..12d26b5 100644 --- a/src/Farm.t.sol +++ b/src/Farm.t.sol @@ -1,8 +1,8 @@ pragma experimental ABIEncoderV2; -pragma solidity ^0.6.12; +pragma solidity ^0.8.14; import "forge-std/Test.sol"; -import {DSToken} from "ds-token/token.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; import {Farm} from "./Farm.sol"; contract FarmTest is Test { @@ -418,8 +418,10 @@ contract FarmTest is Test { } } -contract TestToken is DSToken { - constructor(string memory symbol_, uint8 decimals_) public DSToken(symbol_) { - decimals = decimals_; +contract TestToken is ERC20 { + constructor(string memory symbol_, uint8 decimals_) ERC20("TestToken", symbol_, decimals) {} + + function mint(uint256 wad) external { + _mint(msg.sender, wad); } } From 33940159e572d4224aa59d82039805d78158610f Mon Sep 17 00:00:00 2001 From: Nazar Duchak Date: Thu, 30 Mar 2023 16:40:36 +0300 Subject: [PATCH 09/10] chore: remove ds-token --- .gitmodules | 3 --- lib/ds-token | 1 - 2 files changed, 4 deletions(-) delete mode 160000 lib/ds-token diff --git a/.gitmodules b/.gitmodules index 3a51f01..3689dcf 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,9 +4,6 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std -[submodule "lib/ds-token"] - path = lib/ds-token - url = https://github.com/dapphub/ds-token [submodule "lib/solmate"] path = lib/solmate url = https://github.com/transmissions11/solmate diff --git a/lib/ds-token b/lib/ds-token deleted file mode 160000 index 16f187a..0000000 --- a/lib/ds-token +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 16f187acc15dd839589be60173ad1ebd0716eb82 From 07e959ea02a3155950b45f09e8ee68b2220dce5f Mon Sep 17 00:00:00 2001 From: Nazar Duchak Date: Thu, 30 Mar 2023 17:07:14 +0300 Subject: [PATCH 10/10] chore: use cage/escape instead of pause --- src/Farm.sol | 77 +++++++++++++++++++++++++++++--------------------- src/Farm.t.sol | 40 +++++++++++++++----------- 2 files changed, 68 insertions(+), 49 deletions(-) diff --git a/src/Farm.sol b/src/Farm.sol index 8b38e1c..56e9587 100644 --- a/src/Farm.sol +++ b/src/Farm.sol @@ -11,17 +11,16 @@ contract Farm { mapping(address => uint256) public userRewardPerTokenPaid; mapping(address => uint256) public rewards; + uint256 public live; uint256 public periodFinish = 0; uint256 public rewardRate = 0; uint256 public rewardsDuration = 7 days; uint256 public lastUpdateTime; uint256 public rewardPerTokenStored; - uint public lastPauseTime; - bool public paused; address public rewardsDistribution; - uint256 private _totalSupply; mapping(address => uint256) private _balances; + uint256 private _totalSupply; /** * @notice `usr` was granted admin access. @@ -46,8 +45,14 @@ contract Farm { * @param data The new value of the parameter. */ event File(bytes32 indexed what, address data); + /** + * @notice Recover ERC20 token `amt` to `usr`. + * @param token The token address. + * @param usr The destination address. + * @param amt The amount of `token` flushed out. + */ + event Yank(address indexed token, address indexed usr, uint256 amt); - event PauseChanged(bool isPaused); event RewardAdded(uint256 reward); event Staked(address indexed user, uint256 amount); event Withdrawn(address indexed user, uint256 amount); @@ -62,8 +67,8 @@ contract Farm { _; } - modifier notPaused() { - require(!paused, "Farm/is-paused"); + modifier isLive() { + require(live == 1, "Farm/not-live"); _; } @@ -82,6 +87,7 @@ contract Farm { gem = GemAbstract(_gem); rewardsDistribution = _rewardsDistribution; + live = 1; wards[msg.sender] = 1; emit Rely(msg.sender); } @@ -145,25 +151,17 @@ contract Farm { } /** - * @notice Change the paused state of the contract - * @dev Only the contract owner may call this. + * @notice Cage farm */ - function setPaused(bool _paused) external auth { - // Ensure we're actually changing the state before we do anything - if (_paused == paused) { - return; - } - - // Set our paused state. - paused = _paused; - - // If applicable, set the last pause time. - if (paused) { - lastPauseTime = block.timestamp; - } + function cage() external auth { + live = 0; + } - // Let everyone know that our pause state has changed. - emit PauseChanged(paused); + /** + * @notice Escape from cage + */ + function escape() external auth { + live = 1; } /*////////////////////////////////// @@ -209,7 +207,7 @@ contract Farm { Operations //////////////////////////////////*/ - function stake(uint256 amount) external notPaused updateReward(msg.sender) { + function stake(uint256 amount) external isLive updateReward(msg.sender) { require(amount > 0, "Farm/invalid-amount"); _totalSupply = _add(_totalSupply, amount); @@ -243,12 +241,19 @@ contract Farm { getReward(); } - function recoverERC20(address token, uint256 amt, address to) external auth { + /** + * @notice Flushes out `amt` of `token` sitting in this contract to `usr` address. + * @dev Can only be called by the admin. + * @param token Token address. + * @param amt Token amount. + * @param usr Destination address. + */ + function yank(address token, uint256 amt, address usr) external auth { require(token != address(gem), "Farm/gem-not-allowed"); - GemAbstract(token).transfer(to, amt); + GemAbstract(token).transfer(usr, amt); - emit Recovered(token, amt, to); + emit Yank(token, usr, amt); } function notifyRewardAmount(uint256 reward) external updateReward(address(0)) { @@ -280,19 +285,27 @@ contract Farm { //////////////////////////////////*/ function _add(uint256 x, uint256 y) internal pure returns (uint256 z) { - require((z = x + y) >= x, "Math/add-overflow"); + unchecked { + require((z = x + y) >= x, "Math/add-overflow"); + } } function _sub(uint256 x, uint256 y) internal pure returns (uint256 z) { - require((z = x - y) <= x, "Math/sub-overflow"); + unchecked { + require((z = x - y) <= x, "Math/sub-overflow"); + } } function _mul(uint256 x, uint256 y) internal pure returns (uint256 z) { - require(y == 0 || (z = x * y) / y == x, "Math/mul-overflow"); + unchecked { + require(y == 0 || (z = x * y) / y == x, "Math/mul-overflow"); + } } function _div(uint x, uint y) internal pure returns (uint z) { - require(y > 0, "Math/divide-by-zero"); - return x / y; + unchecked { + require(y > 0, "Math/divide-by-zero"); + return x / y; + } } } diff --git a/src/Farm.t.sol b/src/Farm.t.sol index 12d26b5..d6c96e0 100644 --- a/src/Farm.t.sol +++ b/src/Farm.t.sol @@ -14,12 +14,11 @@ contract FarmTest is Test { event Rely(address indexed usr); event Deny(address indexed usr); - event PauseChanged(bool isPaused); event RewardAdded(uint256 reward); event Staked(address indexed user, uint256 amount); event Withdrawn(address indexed user, uint256 amount); event RewardPaid(address indexed user, uint256 reward); - event Recovered(address token, uint256 amt, address to); + event Yank(address indexed token, address indexed usr, uint256 amt); event File(bytes32 indexed what, uint256 data); event File(bytes32 indexed what, address data); @@ -98,33 +97,40 @@ contract FarmTest is Test { farm.file(bytes32("rewardsDuration"), 1 days); vm.expectRevert("Farm/not-authorized"); - farm.setPaused(true); + farm.cage(); vm.expectRevert("Farm/not-authorized"); - farm.recoverERC20(address(0), 1, address(0)); + farm.escape(); + + vm.expectRevert("Farm/not-authorized"); + farm.yank(address(0), 1, address(0)); } - function testRevertWhenPausedMethods() public { - farm.setPaused(true); + function testRevertStakeWhenCaged() public { + farm.cage(); - vm.expectRevert("Farm/is-paused"); + vm.expectRevert("Farm/not-live"); farm.stake(1); } - function testSetPause() public { - vm.expectEmit(true, true, true, true); - emit PauseChanged(true); + function testEscape() public { + farm.cage(); - farm.setPaused(true); - assertEq(farm.lastPauseTime(), block.timestamp); + vm.expectRevert("Farm/not-live"); + farm.stake(1); + + farm.escape(); + + setupStakingToken(1); + farm.stake(1); } - function testRevertOnRecoverStakingToken() public { + function testRevertOnYankingStakingToken() public { vm.expectRevert("Farm/gem-not-allowed"); - farm.recoverERC20(address(gem), 1, address(this)); + farm.yank(address(gem), 1, address(this)); } - function testRecoverERC20() public { + function testYank() public { TestToken t = new TestToken("TT", 18); t.mint(10); t.transfer(address(farm), 10); @@ -132,9 +138,9 @@ contract FarmTest is Test { assertEq(t.balanceOf(address(farm)), 10); vm.expectEmit(true, true, true, true); - emit Recovered(address(t), 10, address(this)); + emit Yank(address(t), address(this), 10); - farm.recoverERC20(address(t), 10, address(this)); + farm.yank(address(t), 10, address(this)); assertEq(t.balanceOf(address(farm)), 0); assertEq(t.balanceOf(address(this)), 10);