1pragma solidity ^0.8.24;
2
3abstract contract VTokenBase is
4 Initializable,
5 ERC4626Upgradeable,
6 OwnableUpgradeable,
7 PausableUpgradeable,
8 ERC165Upgradeable
9{
10 using Math for uint256;
11 using SafeERC20 for IERC20;
12
13 function setOracle(address _oracle) external onlyOwner {
14 address oldOracle = address(oracle);
15 oracle = Oracle(_oracle);
16 emit OracleChanged(oldOracle, _oracle);
17 }
18
19 function setDispatcher(address _dispatcher) external onlyOwner {
20 address oldDispatcher = address(dispatcher);
21 dispatcher = IDispatcher(_dispatcher);
22 emit DispatcherChanged(oldDispatcher, _dispatcher);
23 }
24
25 function setMaxRedeemRequestsPerUser(uint256 _maxRedeemRequestsPerUser) external onlyOwner {
26 uint256 oldLimit = maxRedeemRequestsPerUser;
27 maxRedeemRequestsPerUser = _maxRedeemRequestsPerUser;
28 emit MaxRedeemRequestsPerUserChanged(oldLimit, _maxRedeemRequestsPerUser);
29 }
30
31 function pause() external onlyOwner {
32 _pause();
33 }
34
35 function unpause() external onlyOwner {
36 _unpause();
37 }
38
39 function changeRoleAdmin(address _account, bool _isAdmin) external onlyOwner {
40 rolesAdmin[_account] = _isAdmin;
41 emit RoleAdminChanged(_account, _isAdmin);
42 }
43
44 function setTriggerAddress(address _triggerAddress) external onlyOwner {
45 address oldAddress = triggerAddress;
46 triggerAddress = _triggerAddress;
47 emit TriggerAddressChanged(oldAddress, _triggerAddress);
48 }
49
50 function asyncMint() external onlyTriggerAddress {
51 // burn currentCycleMintTokenAmount
52 _burn(address(this), currentCycleMintTokenAmount);
53
54 // Send mint request to Bifrost
55 DispatchPost memory post = DispatchPost({
56 body: abi.encode(block.chainid, AsyncOperation.MINT, currentCycleMintTokenAmount, currentCycleMintVTokenAmount),
57 dest: StateMachine.polkadot(BIFROST_CHAIN_ID),
58 timeout: 0,
59 to: BIFROST_SLPX,
60 fee: 0,
61 payer: _msgSender()
62 });
63 dispatcher.dispatch(post);
64
65 // reset currentCycleMintVTokenAmount and currentCycleMintTokenAmount
66 uint256 tokenAmount = currentCycleMintTokenAmount;
67 uint256 vTokenAmount = currentCycleMintVTokenAmount;
68 currentCycleMintVTokenAmount = 0;
69 currentCycleMintTokenAmount = 0;
70
71 emit AsyncMintCompleted(tokenAmount, vTokenAmount);
72 }
73
74 function asyncRedeem() external onlyTriggerAddress {
75 // Send redeem request to Bifrost
76 DispatchPost memory post = DispatchPost({
77 body: abi.encode(block.chainid, AsyncOperation.REDEEM, currentCycleRedeemVTokenAmount),
78 dest: StateMachine.polkadot(BIFROST_CHAIN_ID),
79 timeout: 0,
80 to: BIFROST_SLPX,
81 fee: 0,
82 payer: _msgSender()
83 });
84 dispatcher.dispatch(post);
85
86 // reset currentCycleRedeemVTokenAmount
87 uint256 vTokenAmount = currentCycleRedeemVTokenAmount;
88 currentCycleRedeemVTokenAmount = 0;
89
90 emit AsyncRedeemCompleted(vTokenAmount);
91 }
92
93 function batchClaim(uint256 batchSize) external onlyTriggerAddress {
94 if (redeemQueueIndex >= nextRequestId) {
95 revert InvalidBatchSize();
96 }
97
98 // Calculate actual batch size
99 uint256 actualBatchSize = nextRequestId - redeemQueueIndex;
100 if (batchSize > actualBatchSize) {
101 revert BatchSizeTooLarge(batchSize, actualBatchSize);
102 }
103
104 // Process redeem requests
105 for (uint256 i = redeemQueueIndex; i < redeemQueueIndex + batchSize; i++) {
106 RedeemRequest memory request = redeemQueue[i];
107 if (request.assets > 0) {
108 // Check if already processed
109 // Remove index from userRedeemRequests
110 uint256[] storage userRequests = userRedeemRequests[request.receiver];
111 bool found = false;
112 for (uint256 j = 0; j < userRequests.length; j++) {
113 if (userRequests[j] == i) {
114 // Move last element to current position and remove last element
115 userRequests[j] = userRequests[userRequests.length - 1];
116 userRequests.pop();
117 found = true;
118 break;
119 }
120 }
121 if (!found) {
122 revert RequestIdNotFound(request.receiver, i);
123 }
124 SafeERC20.safeTransfer(IERC20(asset()), request.receiver, request.assets);
125 // Mark as processed
126 delete redeemQueue[i];
127 // Emit success event
128 emit RedeemRequestSuccess(i, request.receiver, request.assets, request.startTime, block.timestamp);
129 }
130 }
131
132 // Update processing index
133 redeemQueueIndex += batchSize;
134
135 emit BatchClaimProcessed(redeemQueueIndex - batchSize, redeemQueueIndex, batchSize);
136 }
137
138 // =================== ERC4626 functions ===================
139 function totalAssets() public view virtual override returns (uint256) {
140 (uint256 tokenAmount,) = oracle.poolInfo(address(asset()));
141 return tokenAmount;
142 }
143
144 function _convertToShares(uint256 assets, Math.Rounding rounding)
145 internal
146 view
147 virtual
148 override
149 returns (uint256)
150 {
151 return oracle.getVTokenAmountByToken(address(asset()), assets, rounding);
152 }
153
154 function _convertToAssets(uint256 shares, Math.Rounding rounding)
155 internal
156 view
157 virtual
158 override
159 returns (uint256)
160 {
161 return oracle.getTokenAmountByVToken(address(asset()), shares, rounding);
162 }
163
164 function deposit(uint256 assets, address receiver) public virtual override whenNotPaused returns (uint256) {
165 currentCycleMintTokenAmount += assets;
166 uint256 vTokenAmount = super.deposit(assets, receiver);
167 currentCycleMintVTokenAmount += vTokenAmount;
168 return vTokenAmount;
169 }
170
171 function mint(uint256 shares, address receiver) public virtual override whenNotPaused returns (uint256) {
172 currentCycleMintVTokenAmount += shares;
173 uint256 tokenAmount = super.mint(shares, receiver);
174 currentCycleMintTokenAmount += tokenAmount;
175 return tokenAmount;
176 }
177
178 function withdraw(uint256 assets, address receiver, address owner)
179 public
180 virtual
181 override
182 whenNotPaused
183 returns (uint256)
184 {
185 return super.withdraw(assets, receiver, owner);
186 }
187
188 function redeem(uint256 shares, address receiver, address owner)
189 public
190 virtual
191 override
192 whenNotPaused
193 returns (uint256)
194 {
195 return super.redeem(shares, receiver, owner);
196 }
197
198 function _withdraw(address caller, address receiver, address owner, uint256 assets, uint256 shares)
199 internal
200 virtual
201 override
202 {
203 if (caller != owner) {
204 _spendAllowance(owner, caller, shares);
205 }
206
207 // Check if user has reached maximum redeem requests limit
208 if (maxRedeemRequestsPerUser > 0 && userRedeemRequests[owner].length >= maxRedeemRequestsPerUser) {
209 revert MaxRedeemRequestsReached(owner, userRedeemRequests[owner].length, maxRedeemRequestsPerUser);
210 }
211
212 _burn(owner, shares);
213
214 // Create redeem request
215 uint256 requestId = nextRequestId;
216 redeemQueue[requestId] = RedeemRequest({receiver: receiver, assets: assets, startTime: block.timestamp});
217 userRedeemRequests[owner].push(requestId);
218 nextRequestId++;
219
220 currentCycleRedeemVTokenAmount += shares;
221
222 emit Withdraw(caller, receiver, owner, assets, shares);
223 }
224
225 function getUserRedeemRequests(address user) public view returns (RedeemRequest[] memory) {
226 uint256[] memory requestIds = userRedeemRequests[user];
227 RedeemRequest[] memory requests = new RedeemRequest[](requestIds.length);
228 for (uint256 i = 0; i < requestIds.length; i++) {
229 requests[i] = redeemQueue[requestIds[i]];
230 }
231 return requests;
232 }
233
234 // =================== ERC6160 functions ===================
235 function mint(address _to, uint256 _amount) public onlyRoleAdmin {
236 super._mint(_to, _amount);
237 }
238
239 function burn(address _from, uint256 _amount) public onlyRoleAdmin {
240 super._burn(_from, _amount);
241 }
242
243 function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
244 return interfaceId == type(IERC4626).interfaceId || interfaceId == type(IERC20).interfaceId
245 || super.supportsInterface(interfaceId);
246 }
247}
248
249
1pragma solidity ^0.8.24;
2
3abstract contract VTokenBase is
4 Initializable,
5 ERC4626Upgradeable,
6 OwnableUpgradeable,
7 PausableUpgradeable,
8 ERC165Upgradeable
9{
10 using Math for uint256;
11 using SafeERC20 for IERC20;
12
13 // =================== Type declarations ===================
14 /// @notice Async operation type
15 enum AsyncOperation {
16 MINT,
17 REDEEM
18 }
19
20 /// @notice Redeem request structure
21 struct RedeemRequest {
22 address receiver;
23 uint256 assets;
24 uint256 startTime;
25 }
26
27 /// @notice Modifier: Only trigger address can call
28 modifier onlyTriggerAddress() {
29 if (_msgSender() != triggerAddress) {
30 revert NotTriggerAddress(_msgSender());
31 }
32 _;
33 }
34
35 /// @notice Modifier: Only role admin can call
36 modifier onlyRoleAdmin() {
37 if (!rolesAdmin[_msgSender()]) {
38 revert NotRoleAdmin(_msgSender());
39 }
40 _;
41 }
42
43 function __VTokenBase_init(IERC20 _asset, address _owner, string memory _name, string memory _symbol)
44 internal
45 onlyInitializing
46 {
47 __ERC20_init(_name, _symbol);
48 __ERC4626_init(_asset);
49 __Ownable_init(_owner);
50 __Pausable_init();
51 __ERC165_init();
52 _pause();
53 }
54
55 function setOracle(address _oracle) external onlyOwner {
56 address oldOracle = address(oracle);
57 oracle = Oracle(_oracle);
58 emit OracleChanged(oldOracle, _oracle);
59 }
60
61 function setDispatcher(address _dispatcher) external onlyOwner {
62 address oldDispatcher = address(dispatcher);
63 dispatcher = IDispatcher(_dispatcher);
64 emit DispatcherChanged(oldDispatcher, _dispatcher);
65 }
66
67 function setMaxRedeemRequestsPerUser(uint256 _maxRedeemRequestsPerUser) external onlyOwner {
68 uint256 oldLimit = maxRedeemRequestsPerUser;
69 maxRedeemRequestsPerUser = _maxRedeemRequestsPerUser;
70 emit MaxRedeemRequestsPerUserChanged(oldLimit, _maxRedeemRequestsPerUser);
71 }
72
73 function pause() external onlyOwner {
74 _pause();
75 }
76
77 function unpause() external onlyOwner {
78 _unpause();
79 }
80
81 function changeRoleAdmin(address _account, bool _isAdmin) external onlyOwner {
82 rolesAdmin[_account] = _isAdmin;
83 emit RoleAdminChanged(_account, _isAdmin);
84 }
85
86 function setTriggerAddress(address _triggerAddress) external onlyOwner {
87 address oldAddress = triggerAddress;
88 triggerAddress = _triggerAddress;
89 emit TriggerAddressChanged(oldAddress, _triggerAddress);
90 }
91
92 function asyncMint() external onlyTriggerAddress {
93 // burn currentCycleMintTokenAmount
94 _burn(address(this), currentCycleMintTokenAmount);
95
96 // Send mint request to Bifrost
97 DispatchPost memory post = DispatchPost({
98 body: abi.encode(block.chainid, AsyncOperation.MINT, currentCycleMintTokenAmount, currentCycleMintVTokenAmount),
99 dest: StateMachine.polkadot(BIFROST_CHAIN_ID),
100 timeout: 0,
101 to: BIFROST_SLPX,
102 fee: 0,
103 payer: _msgSender()
104 });
105 dispatcher.dispatch(post);
106
107 // reset currentCycleMintVTokenAmount and currentCycleMintTokenAmount
108 uint256 tokenAmount = currentCycleMintTokenAmount;
109 uint256 vTokenAmount = currentCycleMintVTokenAmount;
110 currentCycleMintVTokenAmount = 0;
111 currentCycleMintTokenAmount = 0;
112
113 emit AsyncMintCompleted(tokenAmount, vTokenAmount);
114 }
115
116 function asyncRedeem() external onlyTriggerAddress {
117 // Send redeem request to Bifrost
118 DispatchPost memory post = DispatchPost({
119 body: abi.encode(block.chainid, AsyncOperation.REDEEM, currentCycleRedeemVTokenAmount),
120 dest: StateMachine.polkadot(BIFROST_CHAIN_ID),
121 timeout: 0,
122 to: BIFROST_SLPX,
123 fee: 0,
124 payer: _msgSender()
125 });
126 dispatcher.dispatch(post);
127
128 // reset currentCycleRedeemVTokenAmount
129 uint256 vTokenAmount = currentCycleRedeemVTokenAmount;
130 currentCycleRedeemVTokenAmount = 0;
131
132 emit AsyncRedeemCompleted(vTokenAmount);
133 }
134
135 function batchClaim(uint256 batchSize) external onlyTriggerAddress {
136 if (redeemQueueIndex >= nextRequestId) {
137 revert InvalidBatchSize();
138 }
139
140 // Calculate actual batch size
141 uint256 actualBatchSize = nextRequestId - redeemQueueIndex;
142 if (batchSize > actualBatchSize) {
143 revert BatchSizeTooLarge(batchSize, actualBatchSize);
144 }
145
146 // Process redeem requests
147 for (uint256 i = redeemQueueIndex; i < redeemQueueIndex + batchSize; i++) {
148 RedeemRequest memory request = redeemQueue[i];
149 if (request.assets > 0) {
150 // Check if already processed
151 // Remove index from userRedeemRequests
152 uint256[] storage userRequests = userRedeemRequests[request.receiver];
153 bool found = false;
154 for (uint256 j = 0; j < userRequests.length; j++) {
155 if (userRequests[j] == i) {
156 // Move last element to current position and remove last element
157 userRequests[j] = userRequests[userRequests.length - 1];
158 userRequests.pop();
159 found = true;
160 break;
161 }
162 }
163 if (!found) {
164 revert RequestIdNotFound(request.receiver, i);
165 }
166 SafeERC20.safeTransfer(IERC20(asset()), request.receiver, request.assets);
167 // Mark as processed
168 delete redeemQueue[i];
169 // Emit success event
170 emit RedeemRequestSuccess(i, request.receiver, request.assets, request.startTime, block.timestamp);
171 }
172 }
173
174 // Update processing index
175 redeemQueueIndex += batchSize;
176
177 emit BatchClaimProcessed(redeemQueueIndex - batchSize, redeemQueueIndex, batchSize);
178 }
179
180 // =================== ERC4626 functions ===================
181 function totalAssets() public view virtual override returns (uint256) {
182 (uint256 tokenAmount,) = oracle.poolInfo(address(asset()));
183 return tokenAmount;
184 }
185
186 function _convertToShares(uint256 assets, Math.Rounding rounding)
187 internal
188 view
189 virtual
190 override
191 returns (uint256)
192 {
193 return oracle.getVTokenAmountByToken(address(asset()), assets, rounding);
194 }
195
196 function _convertToAssets(uint256 shares, Math.Rounding rounding)
197 internal
198 view
199 virtual
200 override
201 returns (uint256)
202 {
203 return oracle.getTokenAmountByVToken(address(asset()), shares, rounding);
204 }
205
206 function deposit(uint256 assets, address receiver) public virtual override whenNotPaused returns (uint256) {
207 currentCycleMintTokenAmount += assets;
208 uint256 vTokenAmount = super.deposit(assets, receiver);
209 currentCycleMintVTokenAmount += vTokenAmount;
210 return vTokenAmount;
211 }
212
213 function mint(uint256 shares, address receiver) public virtual override whenNotPaused returns (uint256) {
214 currentCycleMintVTokenAmount += shares;
215 uint256 tokenAmount = super.mint(shares, receiver);
216 currentCycleMintTokenAmount += tokenAmount;
217 return tokenAmount;
218 }
219
220 function withdraw(uint256 assets, address receiver, address owner)
221 public
222 virtual
223 override
224 whenNotPaused
225 returns (uint256)
226 {
227 return super.withdraw(assets, receiver, owner);
228 }
229
230 function redeem(uint256 shares, address receiver, address owner)
231 public
232 virtual
233 override
234 whenNotPaused
235 returns (uint256)
236 {
237 return super.redeem(shares, receiver, owner);
238 }
239
240 function _withdraw(address caller, address receiver, address owner, uint256 assets, uint256 shares)
241 internal
242 virtual
243 override
244 {
245 if (caller != owner) {
246 _spendAllowance(owner, caller, shares);
247 }
248
249 // Check if user has reached maximum redeem requests limit
250 if (maxRedeemRequestsPerUser > 0 && userRedeemRequests[owner].length >= maxRedeemRequestsPerUser) {
251 revert MaxRedeemRequestsReached(owner, userRedeemRequests[owner].length, maxRedeemRequestsPerUser);
252 }
253
254 _burn(owner, shares);
255
256 // Create redeem request
257 uint256 requestId = nextRequestId;
258 redeemQueue[requestId] = RedeemRequest({receiver: receiver, assets: assets, startTime: block.timestamp});
259 userRedeemRequests[owner].push(requestId);
260 nextRequestId++;
261
262 currentCycleRedeemVTokenAmount += shares;
263
264 emit Withdraw(caller, receiver, owner, assets, shares);
265 }
266
267 function getUserRedeemRequests(address user) public view returns (RedeemRequest[] memory) {
268 uint256[] memory requestIds = userRedeemRequests[user];
269 RedeemRequest[] memory requests = new RedeemRequest[](requestIds.length);
270 for (uint256 i = 0; i < requestIds.length; i++) {
271 requests[i] = redeemQueue[requestIds[i]];
272 }
273 return requests;
274 }
275
276 // =================== ERC6160 functions ===================
277 function mint(address _to, uint256 _amount) public onlyRoleAdmin {
278 super._mint(_to, _amount);
279 }
280
281 function burn(address _from, uint256 _amount) public onlyRoleAdmin {
282 super._burn(_from, _amount);
283 }
284
285 function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
286 return interfaceId == type(IERC4626).interfaceId || interfaceId == type(IERC20).interfaceId
287 || super.supportsInterface(interfaceId);
288 }
289}
290