Build Liquid Staking

Everywhere

arbitrumbasebifrostbnb-chainethereumhydrationsoneium
arbitrumbasebifrostbnb-chainethereumhydrationsoneium
slpx-1Secure, interoperable liquid staking stack for all chains
Pushing the boundaries of Liquid staking
boundary-01
Chain AbstractionBringing seamless native experience on any chain
boundary-01
ComposabilityComposable use cases across the DeFi ecosystem
boundary-01
Aggregated LiquidityMaximising liquidity efficiency
boundary-01
Trustless Cross-chainDecentralised, non-custodial cross chain protocols
What is SLPx?
A modular Liquid Staking SDK allows you to integrate on any chain
what
Get in Touch
SLPx Case Study
boundary-01
Omni LSThe first omni-chain liquid staking protocol developed based on Bifrost SLPx, facilitates users to mint vTokens on any chain
case-omni-01
Mint vDOT
Copy Code
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
Redeem vDOT
Copy Code
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
Learn More
Use Apps powered by SLPx
Omni LS
A true omnichain LST derivatives management platform
go
Hydration
Multi-Headed Liquidity Monster
go
More Coming...
We are closely collaborating with various partners to bring more use cases.
Build
Building Together
Discord
Discord
Telegram
Telegram
X
X
Forum
Forum
Subscribe
Subscribe