-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathAppStakingVault.sol
More file actions
147 lines (125 loc) · 5.54 KB
/
Copy pathAppStakingVault.sol
File metadata and controls
147 lines (125 loc) · 5.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Errors} from "../utils/Errors.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import {ERC20Votes} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Nonces} from "@openzeppelin/contracts/utils/Nonces.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
/**
* @title AppStakingVault
* @author Elata Biosciences
* @custom:security-contact security@elata.bio
* @notice Per-app staking vault issuing non-transferable share tokens for feature gating and rewards.
* @dev Users deposit app tokens and receive ERC20Votes shares 1:1. Shares are soulbound and serve
* as voting power for app-level governance. Apps can gate features by checking balanceOf(user).
* AppRewardsDistributor uses getPastVotes() snapshots to compute pro-rata ELTA rewards. Users
* may unstake at any time without a lock period.
*/
contract AppStakingVault is ERC20, ERC20Permit, ERC20Votes, Ownable, ReentrancyGuard {
using SafeERC20 for IERC20;
error Insufficient();
/// @notice App token being staked
IERC20 public immutable APP;
event Staked(address indexed user, uint256 amount, uint256 newBalance);
event Unstaked(address indexed user, uint256 amount, uint256 newBalance);
event StakedFor(address indexed beneficiary, uint256 amount, address indexed staker);
/**
* @notice Initialize staking vault
* @param appName App name (e.g., "NeuroGame")
* @param appSymbol App symbol (e.g., "NGT")
* @param appToken Address of the app ERC20 token
* @param owner_ Contract owner (app creator or factory)
*/
constructor(string memory appName, string memory appSymbol, IERC20 appToken, address owner_)
ERC20(string.concat("Staked ", appName), string.concat("s", appSymbol))
ERC20Permit(string.concat("Staked ", appName))
Ownable(owner_)
{
if (address(appToken) == address(0)) revert Errors.ZeroAddress();
APP = appToken;
}
/**
* @notice Stake app tokens
* @dev User must approve this contract first
* @param amount Amount of tokens to stake
*/
function stake(uint256 amount) external nonReentrant {
if (amount == 0) revert Errors.InvalidAmount();
APP.safeTransferFrom(msg.sender, address(this), amount);
_mint(msg.sender, amount);
// Auto-delegate to self for voting power (ERC20Votes requirement)
if (delegates(msg.sender) == address(0)) _delegate(msg.sender, msg.sender);
emit Staked(msg.sender, amount, balanceOf(msg.sender));
}
/**
* @notice Stake on behalf of another address
* @dev Used by factory for auto-staking creator share at launch
* @param beneficiary Address to receive stake-shares
* @param amount Amount of tokens to stake
*/
function stakeFor(address beneficiary, uint256 amount) external onlyOwner nonReentrant {
if (amount == 0) revert Errors.InvalidAmount();
if (beneficiary == address(0)) revert Errors.ZeroAddress();
APP.safeTransferFrom(msg.sender, address(this), amount);
_mint(beneficiary, amount);
// Auto-delegate to self for voting power (ERC20Votes requirement)
if (delegates(beneficiary) == address(0)) _delegate(beneficiary, beneficiary);
emit StakedFor(beneficiary, amount, msg.sender);
}
/**
* @notice Unstake app tokens
* @param amount Amount of tokens to unstake
*/
function unstake(uint256 amount) external nonReentrant {
if (amount == 0) revert Errors.InvalidAmount();
if (balanceOf(msg.sender) < amount) revert Insufficient();
_burn(msg.sender, amount);
APP.safeTransfer(msg.sender, amount);
emit Unstaked(msg.sender, amount, balanceOf(msg.sender));
}
/**
* @notice Get user's staked balance
* @dev Convenience function, equivalent to balanceOf()
* @param user User address
* @return Staked balance
*/
function stakedOf(address user) external view returns (uint256) {
return balanceOf(user);
}
/**
* @notice Get user's staked balance (alias for stakedOf)
* @dev Matches Protocol Changes document naming convention
* @param user User address
* @return Staked balance
*/
function stakedBalanceOf(address user) external view returns (uint256) {
return balanceOf(user);
}
/**
* @notice Get total staked amount
* @dev Convenience function, equivalent to totalSupply()
* @return Total staked
*/
function totalStaked() external view returns (uint256) {
return totalSupply();
}
/**
* @dev Override to make tokens non-transferable
* @dev Allows minting/burning but blocks transfers between users
*/
function _update(address from, address to, uint256 amount) internal override(ERC20, ERC20Votes) {
// Allow minting (from == 0) and burning (to == 0)
// Block transfers between users
if (from != address(0) && to != address(0)) revert Errors.NonTransferable();
super._update(from, to, amount);
}
/**
* @dev Required override for Nonces
*/
function nonces(address owner) public view override(ERC20Permit, Nonces) returns (uint256) {
return super.nonces(owner);
}
}