Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Demo challenge, just for fun #15

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Compiler files
cache/
out/

# Ignores development broadcast logs
!/broadcast
/broadcast/*
/broadcast/*/31337/
/.env
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "lib/solmate"]
path = lib/solmate
url = https://github.com/rari-capital/solmate
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,46 @@

### Comments

Build in foundry.

Some domain assumptions;
- each week is a different pool with different rewards
- week number is defined by current timestamp / (7*24*60*60)
- if you deposit 10 ether in week 1, and withdraw 10 ether in week 2, your rewards will be 0% for week 1 and 2
- if you deposit 10 ether in week 1, and withdraw 5 ether in week 2, and on week 3 you withdraw 5 ether your rewards will be 0% for week 1, (5 ether / total deposits week 2)% of rewards for week 2, and 0 for week 3
- Use WETH but send ether on withdraw and claims, it would be easier just use WETH
- didnt fully tested, it may be really buggy

### Deployed and verify on Goerli
[0x729da725cfdb6c0f240aa6c473066c76fcdec57f](https://goerli.etherscan.io/address/0x729da725cfdb6c0f240aa6c473066c76fcdec57f)

### Test coverage

100% test coverge, however it may need mor test cases

```bash
(base) ➜ exactly-solidity-challenge git:(main) ✗ forge coverage
[⠊] Compiling...
[⠒] Compiling 15 files with 0.8.4
[⠆] Solc 0.8.4 finished in 1.07s
Compiler run successful
Analysing contracts...
Running tests...
+---------------------+-----------------+-----------------+-----------------+-----------------+
| File | % Lines | % Statements | % Branches | % Funcs |
+=============================================================================================+
| script/Deploy.s.sol | 0.00% (0/4) | 0.00% (0/4) | 100.00% (0/0) | 0.00% (0/2) |
|---------------------+-----------------+-----------------+-----------------+-----------------|
| src/ETHPool.sol | 100.00% (87/87) | 100.00% (96/96) | 100.00% (36/36) | 100.00% (13/13) |
|---------------------+-----------------+-----------------+-----------------+-----------------|
| src/mock/WETH.sol | 100.00% (3/3) | 100.00% (3/3) | 100.00% (0/0) | 100.00% (2/2) |
|---------------------+-----------------+-----------------+-----------------+-----------------|
| Total | 95.74% (90/94) | 96.12% (99/103) | 100.00% (36/36) | 88.24% (15/17) |
+---------------------+-----------------+-----------------+-----------------+-----------------+
```

---

# Smart Contract Challenge

## A) Challenge
Expand Down
24 changes: 24 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[profile.default]
solc = '0.8.4'
src = 'src'
out = 'out'
libs = ['lib']
fuzz_runs = 1000
optimizer_runs = 10_000

[profile.optimized]
via_ir = true
out = 'out-via-ir'
fuzz_runs = 5000

[profile.test]
via_ir = true
out = 'out-via-ir'
fuzz_runs = 5000
src = 'test'

[rpc_endpoints]
goerli = "https://eth-goerli.alchemyapi.io/v2/${GOERLI_API_KEY}"

# See more config options https://github.com/foundry-rs/foundry/tree/master/config

1 change: 1 addition & 0 deletions lib/forge-std
Submodule forge-std added at 8d93b5
1 change: 1 addition & 0 deletions lib/solmate
Submodule solmate added at 9cf142
3 changes: 3 additions & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ds-test/=lib/solmate/lib/ds-test/src/
forge-std/=lib/forge-std/src/
solmate/=lib/solmate/src/
19 changes: 19 additions & 0 deletions script/Deploy.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import "forge-std/Script.sol";
import "src/ETHPool.sol";

contract DeployScript is Script {
function setUp() public {}

function run() public {
// goerli weth address
// https://goerli.etherscan.io/address/0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6
address WETH = 0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6;

vm.startBroadcast();
new ETHPool(WETH);
vm.stopBroadcast();
}
}
251 changes: 251 additions & 0 deletions src/ETHPool.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.4;

// for testing porpourses
// import "forge-std/console2.sol";

// Challenge, just for fun
// https://github.com/eugenioclrc/solidity-challenge

// assumption, each week is a different pool with different rewards
// a week number is defined by current timestamp / (7*24*60*60)
// if you deposit 10 ether in week 1, and withdraw 10 ether in
// week 2, your rewards will be 0 for week 1 and 2
// if you deposit 10 ether in week 1, and withdraw 5 ether in
// week 2, and on week 3 you withdraw 5 ether your rewards will
// be 0 for week 1, 100% of rewards for week 1, and 0 for week 3

// test coverage is 100%, try to run al edge cases that i could
// imagine, how ever might not be enough, also

// i use WETH but send ether on withdraw and claims, it would be
// easier just use WETH

import {ReentrancyGuard} from "solmate/utils/ReentrancyGuard.sol";
import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol";
import {Owned} from "solmate/auth/Owned.sol";
import {ERC20} from "solmate/tokens/ERC20.sol";

import {IWETH} from "./IWETH.sol";

contract ETHPool is Owned(msg.sender), ReentrancyGuard {
uint256 constant WEEK = 60 * 60 * 24 * 7;

// contract week start number
uint32 immutable GENESIS;
// weth contract
IWETH immutable WETH;

address team;

// total deposits
uint256 public totalDeposits;
// total deposits per week
mapping(uint32 => uint256) public totalDepositsWeek;

// rewards per week
mapping(uint32 => uint256) public weekRewards;

// user balances
mapping(address => uint256) balances;
// user balances per week
mapping(address => mapping(uint32 => uint256)) public weekBalance;

mapping(address => uint32) lastUserClaim;
mapping(address => uint32) lastUserUpdate;
uint32 _lastUpdate;

event Withdraw(address indexed user, uint32 indexed week, uint256 amount);
event Deposit(address indexed user, uint32 indexed week, uint256 amount);
event Claim(address indexed user, uint256 amount);

event SetTeamWallet(address teamWallet);
event AddReward(uint32 week, uint256 amount);
event RescueReward(uint32 week, uint256 amount);

constructor(address _WETH) {
WETH = IWETH(_WETH);
GENESIS = currentWeek();
_lastUpdate = currentWeek();
}

// private functions

function batchCalcsPredeposit(uint32 _currentWeek) private {
if (_currentWeek > _lastUpdate) {
for (uint32 i = _lastUpdate; i < _currentWeek;) {
unchecked {
totalDepositsWeek[i + 1] = totalDepositsWeek[i];
i++;
}
}
}

_lastUpdate = _currentWeek;
}

function min(uint256 a, uint256 b) private pure returns (uint256) {
return a < b ? a : b;
}

// tricky batch calculations
function batchCalculations(address user, uint32 _currentWeek) private {
// user week calcs
if (lastUserUpdate[user] == 0) {
lastUserUpdate[user] = _currentWeek;
return;
}

if (_currentWeek > lastUserUpdate[user]) {
for (uint32 i = lastUserUpdate[user]; i < _currentWeek;) {
unchecked {
weekBalance[user][i + 1] = weekBalance[user][i];
i++;
}
}
}

lastUserUpdate[user] = _currentWeek;
}

// owner functions
function setTeam(address _team) external onlyOwner {
team = _team;
emit SetTeamWallet(_team);
}

// team functions
function addRewards() external payable {
addRewards(currentWeek() + 1);
}

function addRewards(uint32 week) public payable {
require(msg.sender == team, "only team can add rewards");
require(msg.value > 0, "send some ETH!");
require(week > currentWeek(), "week must => current week");

weekRewards[week] += msg.value;
WETH.deposit{value: msg.value}();
emit AddReward(week, msg.value);
}

function withdrawStuckRewards(uint32 week) public payable {
require(_lastUpdate > week, "week must > last update");
require(msg.sender == team, "only team can add rewards");
require(week < currentWeek(), "week must < current week");

require(min(totalDepositsWeek[week], totalDepositsWeek[week - 1]) == 0, "rewards arent stuck");

uint256 _rewards = weekRewards[week];
weekRewards[week] = 0;
WETH.withdraw(_rewards);
SafeTransferLib.safeTransferETH(team, _rewards);

emit RescueReward(week, _rewards);
}

// public-external functions

function deposit() external payable {
require(msg.value > 0, "deposit must be greater than 0");
deposit(0);
}

function deposit(uint256 amount) public payable nonReentrant {
uint32 _currentWeek = currentWeek();
batchCalcsPredeposit(_currentWeek);
batchCalculations(msg.sender, _currentWeek);
claim();

if (msg.value > 0) {
WETH.deposit{value: msg.value}();
amount = msg.value;
} else if (amount > 0) {
require(msg.value == 0, "Using WETH, dont send ether");
SafeTransferLib.safeTransferFrom(ERC20(address(WETH)), msg.sender, address(this), amount);
}

totalDeposits += amount;
balances[msg.sender] += amount;

totalDepositsWeek[_currentWeek] = totalDeposits;
weekBalance[msg.sender][_currentWeek] = balances[msg.sender];

emit Deposit(msg.sender, _currentWeek, amount);
}

function withdraw(uint256 amount) external nonReentrant {
uint32 _currentWeek = currentWeek();
batchCalcsPredeposit(_currentWeek);
batchCalculations(msg.sender, _currentWeek);
claim();

balances[msg.sender] -= amount;
totalDeposits -= amount;

totalDepositsWeek[_currentWeek] = totalDeposits;
weekBalance[msg.sender][_currentWeek] = balances[msg.sender];

WETH.withdraw(amount);
SafeTransferLib.safeTransferETH(msg.sender, amount);

emit Withdraw(msg.sender, _currentWeek, amount);
}

function claim() public {
uint32 _currentWeek = currentWeek();

totalDepositsWeek[_currentWeek] = totalDeposits;
weekBalance[msg.sender][_currentWeek] = balances[msg.sender];

uint256 _earn = pendingRewards(msg.sender);

lastUserClaim[msg.sender] = _currentWeek;

if (_earn > 0) {
WETH.withdraw(_earn);
SafeTransferLib.safeTransferETH(msg.sender, _earn);
emit Claim(msg.sender, _earn);
}
}

function pendingRewards(address user) public /*view*/ returns (uint256 _earn) {
uint32 _currentWeek = currentWeek();
totalDepositsWeek[_currentWeek] = totalDeposits;
weekBalance[msg.sender][_currentWeek] = balances[msg.sender];

batchCalcsPredeposit(_currentWeek);
batchCalculations(user, _currentWeek);

uint32 start = lastUserClaim[user];
if (start == 0) {
start = GENESIS;
}

for (uint32 i = start; i < _currentWeek;) {
uint256 _rewards = weekRewards[i];
if (_rewards == 0) {
unchecked {
i++;
}
continue;
}
uint256 denominator = min(totalDepositsWeek[i], totalDepositsWeek[i - 1]);
if (totalDepositsWeek[i] > 0) {
_earn += (_rewards * min(weekBalance[user][i], weekBalance[user][i - 1])) / denominator;
}
unchecked {
i++;
}
}
}

function currentWeek() public view returns (uint32) {
return uint32(block.timestamp / WEEK);
}

// we can receive ether from the WETH contract
// but if someone sends ether to the contract, it will get stuck
// it would be best just use WETH
receive() external payable {}
}
20 changes: 20 additions & 0 deletions src/IWETH.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

interface IWETH {
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 src, address dst, uint256 wad) external returns (bool);

function deposit() external payable;

function withdraw(uint256 wad) external;
}
Loading