Skip to content

Commit 4fdfc29

Browse files
authored
Merge pull request #3 from wesenditmedia/feature/wesendit-3.0
Feature/wesendit 3.0
2 parents 284ff0a + 7298d05 commit 4fdfc29

26 files changed

+3569
-116
lines changed

.devcontainer/devcontainer.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
2+
// README at: https://github.com/devcontainers/templates/tree/main/src/alpine
3+
{
4+
"name": "ETH Security Toolbox",
5+
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
6+
"image": "ghcr.io/trailofbits/eth-security-toolbox:nightly",
7+
8+
// Features to add to the dev container. More info: https://containers.dev/features.
9+
// "features": {},
10+
11+
// Use 'forwardPorts' to make a list of ports inside the container available locally.
12+
// "forwardPorts": [],
13+
14+
// Use 'postCreateCommand' to run commands after the container is created.
15+
"postCreateCommand": "solc-select install 0.8.17 && sudo n 16"
16+
17+
// Configure tool-specific properties.
18+
// "customizations": {},
19+
20+
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
21+
// "remoteUser": "root"
22+
}

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,8 @@ artifacts
1111
flattened.sol
1212

1313
docs
14-
abi
14+
abi
15+
generated
16+
17+
scripts/*.json
18+
scripts/*.csv

LICENSE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2022 WeSendit Media AG
3+
Copyright (c) 2023 WeSendit Media AG
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,32 @@
11
# <img src="https://wesendit.io/wp-content/uploads/2022/04/cropped-WSI_Favicon-192x192.png" width="20px" height="20px"></img> WeSendit Smart Contracts 🚀
22

3-
> This repository contains the core smart contracts used by the WeSendit crypto project.
3+
This repository contains a set of smart contracts used by WeSendit.
44

55
## Deployment
66

77
```shell
88
npx hardhat run --network bscTestnet scripts/deploy.ts
99
```
1010

11-
## Included Smart Contracts
11+
## Smart Contracts
1212

1313
| Sourcefile | Description |
1414
|---|---|
1515
| [WeSenditToken.sol](contracts/WeSenditToken.sol) | WeSendit ERC20 Token Contract, with small modifications to support fee reflection using Dynamic Fee Manager.
1616
| [DynamicFeeManager.sol](contracts/DynamicFeeManager.sol) | Dynamic Fee Manager, able to dynamically add or remove fees for reflection on transactions.
17+
| [EmergencyGuard.sol](contracts/EmergencyGuard.sol) | Emergency Guard, provides emergency ETH / token withdrawal functions from contract address.
18+
| [StakingPool.sol](contracts/StakingPool.sol) | WeSendit Staking Pool Contract.
19+
| [WeStakeitToken.sol](contracts/WeStakeitToken.sol) | WeSendit Staking Pool Proof NFT.
20+
| [VestingWallet.sol](contracts/VestingWallet.sol) | WeSendit Vesting Wallet, based on OpenZeppelin Vesting Wallet.
21+
| [TokenVault.sol](contracts/TokenVault.sol) | WeSendit Token Vault, used to lock token on it.
22+
| [MultiVestingWallet.sol](contracts/MultiVestingWallet.sol) | WeSendit Multi Vesting Wallet, used to vest token for multiple beneficiaries.
23+
| [PaymentProcessor.sol](contracts/PaymentProcessor.sol) | WeSendit 3.0 Payment Processor Smart Contract, used for web3 payments.
24+
| [RewardDistributor.sol](contracts/RewardDistributor.sol) | WeSendit 3.0 Reward Distributor Smart Contract, used for web3 activity rewards.
1725

1826
## Audits
1927

2028
| Report | Audit Date | Organization |
2129
|---|---|---|
22-
| [WeSendit_SCAudit_Report_Hacken_io.pdf](WeSendit_SCAudit_Report_Hacken_io.pdf) | 20th Oct. 2022 | [Hacken](https://hacken.io)
23-
| [WeSendit_SCAudit_Report_Solidproof_io.pdf](WeSendit_SCAudit_Report_Solidproof_io.pdf) | 22th Oct. 2022 | [Solidproof](https://solidproof.io)
30+
| [WeSendit_SCAudit_Report_Hacken_io.pdf](audits/WeSendit_SCAudit_Report_Hacken_io.pdf) | 20th Oct. 2022 | [Hacken](https://hacken.io)
31+
| [WeSendit_SCAudit_Report_Solidproof_io.pdf](audits/WeSendit_SCAudit_Report_Solidproof_io.pdf) | 22nd Oct. 2022 | [Solidproof](https://solidproof.io)
32+
| [SmartContract_Audit_Solidproof_WesendIt_PaymentDistributor.pdf](audits/SmartContract_Audit_Solidproof_WesendIt_PaymentDistributor.pdf) | 21st Dec. 2022 | [Solidproof](https://solidproof.io)
Binary file not shown.

contracts/DynamicFeeManager.sol

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import "./BaseDynamicFeeManager.sol";
77
import "./interfaces/IFeeReceiver.sol";
88
import "./interfaces/IWeSenditToken.sol";
99

10+
import "hardhat/console.sol";
11+
1012
/**
1113
* @title Dynamic Fee Manager for ERC20 token
1214
*
@@ -134,7 +136,10 @@ contract DynamicFeeManager is BaseDynamicFeeManager {
134136
for (uint256 i = 0; i < feeAmount; i++) {
135137
FeeEntry memory fee = feeEntries[i];
136138

137-
if (_isFeeEntryValid(fee) && _isFeeEntryMatching(fee, from, to)) {
139+
if (
140+
_isFeeEntryValid(fee) &&
141+
(_isFeeEntryMatching(fee, from, to, amount))
142+
) {
138143
uint256 tFee = _calculateFee(amount, fee.percentage);
139144
uint256 tempPercentage = totalFeePercentage + fee.percentage;
140145

@@ -152,6 +157,40 @@ contract DynamicFeeManager is BaseDynamicFeeManager {
152157
return (tTotal, tFees);
153158
}
154159

160+
function _isFeeMatchingStakingUnclaim(
161+
FeeEntry memory fee,
162+
address to,
163+
uint256 amount
164+
) private view returns (bool matching) {
165+
// Get users staking nfts balance
166+
uint256 balance = weStakeitToken().balanceOf(to);
167+
168+
for (uint256 i = 0; i < balance; i++) {
169+
// Get staking token id
170+
uint256 tokenId = weStakeitToken().tokenOfOwnerByIndex(to, i);
171+
172+
// Get staking entry from pool
173+
PoolEntry memory entry = stakingPool().poolEntry(tokenId);
174+
175+
/**
176+
* Check if entry is:
177+
* - unstaked (happens right before transfer)
178+
* - claimed with this block (happens likely directly before transfer)
179+
* - fee amount is matching 3% of initial stake amount
180+
*/
181+
if (
182+
entry.isUnstaked &&
183+
entry.lastClaimedAt == block.timestamp &&
184+
(amount * fee.percentage) / FEE_DIVIDER ==
185+
(entry.amount * fee.percentage) / FEE_DIVIDER
186+
) {
187+
return true;
188+
}
189+
}
190+
191+
return false;
192+
}
193+
155194
/**
156195
* Reflects a single fee
157196
*
@@ -168,8 +207,8 @@ contract DynamicFeeManager is BaseDynamicFeeManager {
168207
FeeEntry memory fee,
169208
bool bypassSwapAndLiquify
170209
) private {
171-
// add to liquify / swap amount or transfer to fee destination
172210
if (fee.doLiquify || fee.doSwapForBusd) {
211+
// add to liquify / swap amount or transfer to fee destination
173212
require(
174213
IWeSenditToken(address(token())).transferFromNoFees(
175214
from,
@@ -275,8 +314,14 @@ contract DynamicFeeManager is BaseDynamicFeeManager {
275314
function _isFeeEntryMatching(
276315
FeeEntry memory fee,
277316
address from,
278-
address to
317+
address to,
318+
uint256 amount
279319
) private view returns (bool matching) {
320+
// Staking pool customization
321+
if (fee.from == address(stakingPool())) {
322+
return _isFeeMatchingStakingUnclaim(fee, to, amount);
323+
}
324+
280325
return
281326
((fee.from == WHITELIST_ADDRESS &&
282327
fee.to == WHITELIST_ADDRESS &&

contracts/MultiVestingWallet.sol

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
// SPDX-License-Identifier: MIT
2+
// Based on VestingWallet from OpenZeppelin Contracts (last updated v4.8.0) (finance/VestingWallet.sol)
3+
pragma solidity 0.8.17;
4+
5+
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
6+
import "@openzeppelin/contracts/utils/Address.sol";
7+
import "@openzeppelin/contracts/utils/Context.sol";
8+
import "@openzeppelin/contracts/utils/math/Math.sol";
9+
import "@openzeppelin/contracts/access/Ownable.sol";
10+
11+
import "./interfaces/IMultiVestingWallet.sol";
12+
import "./EmergencyGuard.sol";
13+
14+
import "hardhat/console.sol";
15+
16+
contract MultiVestingWallet is
17+
IMultiVestingWallet,
18+
Context,
19+
Ownable,
20+
EmergencyGuard
21+
{
22+
uint64 private immutable _start;
23+
uint64 private immutable _duration;
24+
25+
// Total initial ETH amount
26+
uint256 private _totalInitialETH;
27+
28+
// Mapping from token to total initial token amount
29+
mapping(address => uint256) private _totalInitialToken;
30+
31+
// Total released token amount
32+
uint256 private _totalReleasedETH;
33+
34+
// Mapping from ttoken to total released token amount
35+
mapping(address => uint256) private _totalReleasedToken;
36+
37+
// Mapping from beneficiary address to initial ETH
38+
mapping(address => uint256) private _userInitialETH;
39+
40+
// Mapping from beneficiary address to release ETH
41+
mapping(address => uint256) private _userReleaseETH;
42+
43+
// Mapping from beneficiary address to token address to initial token
44+
mapping(address => mapping(address => uint256)) private _userInitialToken;
45+
46+
// Mapping from beneficiary address to token address to release token
47+
mapping(address => mapping(address => uint256)) private _userReleasedToken;
48+
49+
/**
50+
* @dev Set the start timestamp and vesting duration of the vesting wallet.
51+
*/
52+
constructor(uint64 startTimestamp, uint64 durationSeconds) payable {
53+
_start = startTimestamp;
54+
_duration = durationSeconds;
55+
}
56+
57+
receive() external payable virtual {}
58+
59+
function addBeneficiaries(
60+
address[] calldata beneficiaries,
61+
uint256[] calldata amounts
62+
) external virtual onlyOwner {
63+
require(
64+
beneficiaries.length == amounts.length,
65+
"MultiVestingWallet: mismatching beneficiaries / amounts pair"
66+
);
67+
68+
for (uint256 i = 0; i < beneficiaries.length; i++) {
69+
addBeneficiary(beneficiaries[i], amounts[i]);
70+
}
71+
}
72+
73+
function addBeneficiaries(
74+
address[] calldata beneficiaries,
75+
address token,
76+
uint256[] calldata amounts
77+
) external virtual onlyOwner {
78+
require(
79+
beneficiaries.length == amounts.length,
80+
"MultiVestingWallet: mismatching beneficiaries / amounts pair"
81+
);
82+
83+
for (uint256 i = 0; i < beneficiaries.length; i++) {
84+
addBeneficiary(beneficiaries[i], token, amounts[i]);
85+
}
86+
}
87+
88+
function addBeneficiary(
89+
address beneficiary,
90+
uint256 amount
91+
) public virtual onlyOwner {
92+
require(
93+
address(this).balance + _totalReleasedETH - _totalInitialETH >=
94+
amount,
95+
"MultiVestingWallet: ETH amount exceeds balance"
96+
);
97+
98+
_userInitialETH[beneficiary] = amount;
99+
_totalInitialETH += amount;
100+
}
101+
102+
function addBeneficiary(
103+
address beneficiary,
104+
address token,
105+
uint256 amount
106+
) public virtual onlyOwner {
107+
require(
108+
IERC20(token).balanceOf(address(this)) +
109+
_totalReleasedToken[token] -
110+
_totalInitialToken[token] >=
111+
amount,
112+
"MultiVestingWallet: Token amount exceeds balance"
113+
);
114+
115+
_userInitialToken[beneficiary][token] = amount;
116+
_totalInitialToken[token] += amount;
117+
}
118+
119+
function start() public view virtual returns (uint256) {
120+
return _start;
121+
}
122+
123+
function duration() public view virtual returns (uint256) {
124+
return _duration;
125+
}
126+
127+
function initial(
128+
address beneficiary
129+
) public view virtual returns (uint256) {
130+
return _userInitialETH[beneficiary];
131+
}
132+
133+
function initial(
134+
address beneficiary,
135+
address token
136+
) public view virtual returns (uint256) {
137+
return _userInitialToken[beneficiary][token];
138+
}
139+
140+
function released(
141+
address beneficiary
142+
) public view virtual returns (uint256) {
143+
return _userReleaseETH[beneficiary];
144+
}
145+
146+
function released(
147+
address beneficiary,
148+
address token
149+
) public view virtual returns (uint256) {
150+
return _userReleasedToken[beneficiary][token];
151+
}
152+
153+
function releasable(
154+
address beneficiary
155+
) public view virtual returns (uint256) {
156+
return
157+
vestedAmount(beneficiary, uint64(block.timestamp)) -
158+
released(beneficiary);
159+
}
160+
161+
function releasable(
162+
address beneficiary,
163+
address token
164+
) public view virtual returns (uint256) {
165+
return
166+
vestedAmount(beneficiary, token, uint64(block.timestamp)) -
167+
released(beneficiary, token);
168+
}
169+
170+
function release(address beneficiary) public virtual {
171+
uint256 amount = releasable(beneficiary);
172+
_userReleaseETH[beneficiary] += amount;
173+
_totalReleasedETH += amount;
174+
emit EtherReleased(beneficiary, amount);
175+
Address.sendValue(payable(beneficiary), amount);
176+
}
177+
178+
function release(address beneficiary, address token) public virtual {
179+
uint256 amount = releasable(beneficiary, token);
180+
_userReleasedToken[beneficiary][token] += amount;
181+
_totalReleasedToken[token] += amount;
182+
emit ERC20Released(beneficiary, token, amount);
183+
SafeERC20.safeTransfer(IERC20(token), beneficiary, amount);
184+
}
185+
186+
function vestedAmount(
187+
address beneficiary,
188+
uint64 timestamp
189+
) public view virtual returns (uint256) {
190+
return _vestingSchedule(_userInitialETH[beneficiary], timestamp);
191+
}
192+
193+
function vestedAmount(
194+
address beneficiary,
195+
address token,
196+
uint64 timestamp
197+
) public view virtual returns (uint256) {
198+
return
199+
_vestingSchedule(_userInitialToken[beneficiary][token], timestamp);
200+
}
201+
202+
/**
203+
* @dev Virtual implementation of the vesting formula. This returns the amount vested, as a function of time, for
204+
* an asset given its total historical allocation.
205+
*/
206+
function _vestingSchedule(
207+
uint256 totalAllocation,
208+
uint64 timestamp
209+
) internal view virtual returns (uint256) {
210+
if (timestamp < start()) {
211+
return 0;
212+
} else if (timestamp > start() + duration()) {
213+
return totalAllocation;
214+
} else {
215+
return (totalAllocation * (timestamp - start())) / duration();
216+
}
217+
}
218+
219+
function emergencyWithdraw(uint256 amount) external override onlyOwner {
220+
super._emergencyWithdraw(amount);
221+
}
222+
223+
function emergencyWithdrawToken(
224+
address token,
225+
uint256 amount
226+
) external override onlyOwner {
227+
super._emergencyWithdrawToken(token, amount);
228+
}
229+
}

0 commit comments

Comments
 (0)