Skip to content

Commit 22b19a9

Browse files
committed
audit: EnsoVestingFactory and EnsoVestingWallet
1 parent e6e0868 commit 22b19a9

File tree

2 files changed

+28
-5
lines changed

2 files changed

+28
-5
lines changed

src/EnsoVestingFactory.sol

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,32 @@
11
// SPDX-License-Identifier: GPL-3.0-only
22
pragma solidity ^0.8.20;
33

4-
import { SafeERC20, IERC20 } from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol";
5-
import { Ownable } from "openzeppelin-contracts/access/Ownable.sol";
64
import { EnsoVestingWallet } from "./EnsoVestingWallet.sol";
5+
import { Ownable } from "openzeppelin-contracts/access/Ownable.sol";
6+
import { IERC20, SafeERC20 } from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol";
77

8+
// @audit-info consider making this contract a clone factory for gas efficiency
9+
// @audit-info 2-step ownership?
810
contract EnsoVestingFactory is Ownable {
911
using SafeERC20 for IERC20;
1012

1113
IERC20 private immutable _token;
1214

1315
struct VestingOptions {
16+
// @audit-info a single uint64 below (and up to 3 x uint48) can be packed with address in the same slot
17+
// However, the struct members are never stored on-chain on storage, but as immutables in the created
18+
// EnsoVestingWallet
1419
address beneficiary;
1520
uint256 amount;
21+
// @audit-info consider using `uint48` (safe for timestamps), which will pack 3 timestamps in 12 bytes
22+
// However, the struct members are never stored on-chain on storage, but as immutables in the created
23+
// EnsoVestingWallet
1624
uint64 startTimestamp;
1725
uint64 durationSeconds;
1826
uint64 cliffSeconds;
1927
}
2028

29+
// @audit-info no indexed parameters? Possibly index `wallet` and `beneficiary`
2130
event VestingWalletCreated(
2231
address wallet,
2332
address beneficiary,
@@ -64,4 +73,4 @@ contract EnsoVestingFactory is Ownable {
6473
options.cliffSeconds
6574
);
6675
}
67-
}
76+
}

src/EnsoVestingWallet.sol

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ pragma solidity ^0.8.20;
77
import { Ownable } from "openzeppelin-contracts/access/Ownable.sol";
88
import { IERC20, SafeERC20 } from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol";
99

10+
// @audit-info consider making this contract a clone for gas efficiency
11+
// @audit-info 2-step ownership?
12+
// @audit-info missing interface with errors and events?
13+
// @audit-info "total vested Amount" is not stored. Any concern regarding this design choice and that
14+
// `token.balanceOf(address(this))` can be increased anytime (which modifies `_vestingSchedule` return value)? All OK by
15+
// me btw!
1016
contract EnsoVestingWallet is Ownable {
1117
using SafeERC20 for IERC20;
1218

@@ -15,10 +21,13 @@ contract EnsoVestingWallet is Ownable {
1521

1622
uint256 public released;
1723
bool public revoked;
24+
// @audit-info `revoker == address(0)` means non-revocable forever.
1825
bool public immutable revocable;
1926

2027
IERC20 private immutable _token;
28+
// @audit-info revoker immutable? risks?
2129
address private immutable _revoker;
30+
// @audit-info consider using `uint48` (safe for timestamps), which will pack 3 timestamps in 12 bytes
2231
uint64 private immutable _start;
2332
uint64 private immutable _duration;
2433
uint64 private immutable _cliff;
@@ -45,6 +54,7 @@ contract EnsoVestingWallet is Ownable {
4554
_duration = durationSeconds;
4655
_cliff = startTimestamp + cliffSeconds;
4756
_token = token;
57+
// @audit-info simplify to `_revoker = revoker` if `address(0)` means non-revocable forever
4858
if (revoker != address(0)) {
4959
_revoker = revoker;
5060
revocable = true;
@@ -69,7 +79,6 @@ contract EnsoVestingWallet is Ownable {
6979
* @dev Getter for the end timestamp.
7080
*/
7181
function end() public view returns (uint256) {
72-
// @audit return _start + _duration?
7382
return start() + duration();
7483
}
7584

@@ -98,7 +107,6 @@ contract EnsoVestingWallet is Ownable {
98107
* @dev Getter for the amount of releasable tokens.
99108
*/
100109
function releasable() public view returns (uint256) {
101-
// @audit return `_vestingSchedule(...)`?
102110
return vestedAmount(uint64(block.timestamp)) - released;
103111
}
104112

@@ -116,7 +124,9 @@ contract EnsoVestingWallet is Ownable {
116124
*/
117125
function release() public {
118126
if (revoked) revert Revoked();
127+
// @audit-info call `_vestingSchedule(...)`?
119128
uint256 amount = releasable();
129+
// @audit-info skip checking amount for gas efficiency (and allow 0 transfers)
120130
if (amount > 0) {
121131
released += amount;
122132
emit TokenReleased(amount);
@@ -145,6 +155,10 @@ contract EnsoVestingWallet is Ownable {
145155
* an asset given its total historical allocation. Returns 0 if the {cliff} timestamp is not met.
146156
*/
147157
function _vestingSchedule(uint256 totalAllocation, uint64 timestamp) internal view returns (uint256) {
158+
// @audit-info if revoked revert with Revoked?
159+
// @audit-info if `timestamp < _cliff` revert with BlockTimestampLtCliff(uint256 blockTimestamp, uint256 cliff)
160+
// or `CliffNotReached(uint256 blockTimestamp, uint256 cliff)`?
161+
// @audit-info no need for `if..else if..else`
148162
if (revoked || timestamp < cliff()) {
149163
return 0;
150164
} else if (timestamp >= end()) {

0 commit comments

Comments
 (0)