Skip to content

Commit 359472e

Browse files
committed
audit: airdrop.sol
1 parent b4a2abb commit 359472e

File tree

6 files changed

+89
-18
lines changed

6 files changed

+89
-18
lines changed

TODO_VN.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# TODO
2+
3+
## Repo
4+
5+
### Audits
6+
7+
- Missing previous audit reports (if any).
8+
9+
### Dependencies
10+
11+
- Adopt Soldeer to pin dependencies (e.g., OZ) by version.
12+
- Remove unused dependencies.
13+
- Adopt latest forge
14+
15+
### Pre-commit pipeline:
16+
17+
- Enforce `forge lint` before code is commited. Prettify any non-Solidity code.
18+
19+
### CI
20+
21+
- Consider running tests grouped by scope.
22+
23+
### Contracts
24+
25+
Upgradeability:
26+
27+
- Consider using https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades due to OZ's UUPS.
28+
29+
Uncompilable:
30+
31+
- src/EnsoMint.sol
32+
- src/EnsoStaking.sol

foundry.lock

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"lib/forge-std": {
3+
"rev": "77041d2ce690e692d6e03cc812b57d1ddaa4d505"
4+
},
5+
"lib/openzeppelin-contracts": {
6+
"rev": "e4f70216d759d8e6a64144a9e1f7bbeed78e7079"
7+
},
8+
"lib/openzeppelin-contracts-upgradeable": {
9+
"rev": "60b305a8f3ff0c7688f02ac470417b6bbf1c4d27"
10+
}
11+
}

src/Airdrop.sol

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// SPDX-License-Identifier: GPL-3.0-only
22
pragma solidity ^0.8.20;
33

4+
// @audit-info 2-step ownership
45
import { Ownable } from "openzeppelin-contracts/access/Ownable.sol";
56
import { IERC20 } from "openzeppelin-contracts/token/ERC20/IERC20.sol";
67
import { MerkleProof } from "openzeppelin-contracts/utils/cryptography/MerkleProof.sol";
@@ -16,6 +17,10 @@ contract Airdrop is Ownable {
1617
IERC20 public immutable token;
1718
bytes32 public immutable root;
1819
uint256 public immutable expiration;
20+
// @audit consider using a `mapping(uint256 bucket => uint256 bitmap) bucketBitmap` instead of
21+
// `mapping(bytes32 leafHash => bool isClaimed)` for gas efficiency
22+
// https://github.com/Uniswap/merkle-distributor/blob/master/contracts/MerkleDistributor.sol
23+
// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/structs/BitMaps.sol
1924
mapping(bytes32 => bool) public claimed;
2025

2126
constructor(address _token, bytes32 _root, uint256 _expiration, address _owner) Ownable(_owner) {
@@ -24,10 +29,13 @@ contract Airdrop is Ownable {
2429
expiration = _expiration;
2530
}
2631

32+
// @audit ideally hash twice to prevent any possible "node as leaf" attack, where the `hash(address, amount)` equals
33+
// to an existing node hash
2734
function getLeafHash(address to, uint256 amount) public pure returns (bytes32) {
2835
return keccak256(abi.encode(to, amount));
2936
}
3037

38+
// @audit `to` instead of `msg.sender` => tax consequences. Consider validating `to == msg.sender`
3139
function claim(bytes32[] memory proof, address to, uint256 amount) external {
3240
if (block.timestamp > expiration) revert AirdropExpired();
3341

@@ -40,11 +48,13 @@ contract Airdrop is Ownable {
4048

4149
token.transfer(to, amount);
4250

51+
// @audit-info Claimed, past participle
4352
emit Claim(to, amount);
4453
}
4554

4655
function sweep(address to) external onlyOwner {
4756
if (block.timestamp <= expiration) revert AirdropNotExpired();
4857
token.transfer(to, token.balanceOf(address(this)));
58+
// @audit-info emit Swept event
4959
}
5060
}

src/EnsoMint.sol

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
pragma solidity ^0.8.20;
33

44
import { EnsoValidatorWallet } from "./EnsoValidatorWallet.sol";
5-
import { Ownable } from "openzeppelin-contracts/access/Ownable.sol";
5+
66
import { MintToken } from "./interfaces/MintToken.sol";
7+
import { Ownable } from "openzeppelin-contracts/access/Ownable.sol";
78

89
contract EnsoMint is Ownable {
910
MintToken public token;
@@ -36,11 +37,17 @@ contract EnsoMint is Ownable {
3637
emit ValidatorAdded(address(validatorWallet));
3738
}
3839

39-
function issueFunds(uint64 timestamp, uint256 totalShares, ValidatorIssuance[] calldata issuance) external onlyOwner {
40+
function issueFunds(
41+
uint64 timestamp,
42+
uint256 totalShares,
43+
ValidatorIssuance[] calldata issuance
44+
)
45+
external
46+
onlyOwner
47+
{
4048
uint256 period = timestamp - lastTimestamp;
4149
uint256 totalFunds = tokensPerSecond * period;
4250
lastTimestamp = timestamp;
43-
4451

4552
uint256 shareCount;
4653
for (uint256 i; i < issuance.length; i++) {
@@ -53,15 +60,17 @@ contract EnsoMint is Ownable {
5360
token.mint(validator, validatorShare);
5461
uint256 stakersShare = amount - validatorShare;
5562
token.mint(address(this), stakersShare);
56-
token.approve(address(staking), stakersShare);
57-
staking.issueRewards(validator, stakersShare);
63+
// TODO: unfinished implementation
64+
// @audit undeclared identifier `staking`
65+
// token.approve(address(staking), stakersShare);
66+
// staking.issueRewards(validator, stakersShare);
5867
}
59-
if (totalShares += shareCount) revert IncorrectTotalShares(totalShares, shareCount);
60-
emit FundsIssued(totalFunds, period);
68+
// if (totalShares += shareCount) revert IncorrectTotalShares(totalShares, shareCount);
69+
// emit FundsIssued(totalFunds, period);
6170
}
6271

6372
function updateTokensPerSecond(uint256 amount) external onlyOwner {
6473
tokensPerSecond = amount;
6574
emit TokensPerSecondUpdated(amount);
6675
}
67-
}
76+
}
File renamed without changes.

src/EnsoVestingWallet.sol

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ pragma solidity ^0.8.20;
44
// Based on OpenZeppelin's VestingWallet & VestingWalletCliff contracts:
55
// https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/finance
66

7-
import { SafeERC20, IERC20 } from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol";
87
import { Ownable } from "openzeppelin-contracts/access/Ownable.sol";
8+
import { IERC20, SafeERC20 } from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol";
99

1010
contract EnsoVestingWallet is Ownable {
1111
using SafeERC20 for IERC20;
@@ -16,7 +16,7 @@ contract EnsoVestingWallet is Ownable {
1616
uint256 public released;
1717
bool public revoked;
1818
bool public immutable revocable;
19-
19+
2020
IERC20 private immutable _token;
2121
address private immutable _revoker;
2222
uint64 private immutable _start;
@@ -28,7 +28,16 @@ contract EnsoVestingWallet is Ownable {
2828
error NotRevocable();
2929
error NotRevoker(address sender, address revoker);
3030

31-
constructor(IERC20 token, address revoker, address beneficiary, uint64 startTimestamp, uint64 durationSeconds, uint64 cliffSeconds) Ownable(beneficiary) {
31+
constructor(
32+
IERC20 token,
33+
address revoker,
34+
address beneficiary,
35+
uint64 startTimestamp,
36+
uint64 durationSeconds,
37+
uint64 cliffSeconds
38+
)
39+
Ownable(beneficiary)
40+
{
3241
if (cliffSeconds > durationSeconds) {
3342
revert InvalidCliffDuration(cliffSeconds, durationSeconds);
3443
}
@@ -60,10 +69,11 @@ contract EnsoVestingWallet is Ownable {
6069
* @dev Getter for the end timestamp.
6170
*/
6271
function end() public view returns (uint256) {
72+
// @audit return _start + _duration?
6373
return start() + duration();
6474
}
6575

66-
/**
76+
/**
6777
* @dev Getter for the cliff timestamp.
6878
*/
6979
function cliff() public view virtual returns (uint256) {
@@ -87,7 +97,8 @@ contract EnsoVestingWallet is Ownable {
8797
/**
8898
* @dev Getter for the amount of releasable tokens.
8999
*/
90-
function releasable() public view returns (uint256) {
100+
function releasable() public view returns (uint256) {
101+
// @audit return `_vestingSchedule(...)`?
91102
return vestedAmount(uint64(block.timestamp)) - released;
92103
}
93104

@@ -122,6 +133,7 @@ contract EnsoVestingWallet is Ownable {
122133
if (!revocable) revert NotRevocable();
123134
if (msg.sender != _revoker) revert NotRevoker(msg.sender, _revoker);
124135
release(); // first, release funds beneficiary is entitled to up to this point
136+
// @audit CEI pattern violated, create a private `_relase()` that skips checking `revoked`
125137
revoked = true;
126138
uint256 amount = _token.balanceOf(address(this));
127139
_token.safeTransfer(receiver, amount);
@@ -132,10 +144,7 @@ contract EnsoVestingWallet is Ownable {
132144
* @dev Implementation of the vesting formula. This returns the amount vested, as a function of time, for
133145
* an asset given its total historical allocation. Returns 0 if the {cliff} timestamp is not met.
134146
*/
135-
function _vestingSchedule(
136-
uint256 totalAllocation,
137-
uint64 timestamp
138-
) internal view returns (uint256) {
147+
function _vestingSchedule(uint256 totalAllocation, uint64 timestamp) internal view returns (uint256) {
139148
if (revoked || timestamp < cliff()) {
140149
return 0;
141150
} else if (timestamp >= end()) {
@@ -144,4 +153,4 @@ contract EnsoVestingWallet is Ownable {
144153
return (totalAllocation * (timestamp - start())) / duration();
145154
}
146155
}
147-
}
156+
}

0 commit comments

Comments
 (0)