├── .env.example ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .gitmodules ├── .prettierignore ├── .prettierrc ├── .solhint.json ├── .solhintignore ├── .vscode └── settings.json ├── Makefile ├── README.md ├── agents ├── bot-operations-agent │ ├── .dockerignore │ ├── .gitignore │ ├── Dockerfile │ ├── README.md │ ├── jest.config.js │ ├── package-lock.json │ ├── package.json │ ├── publish.log │ ├── src │ │ ├── agent.ts │ │ ├── constants.ts │ │ └── utils.ts │ └── tsconfig.json └── suspicious-amount-agent │ ├── .dockerignore │ ├── .gitignore │ ├── Dockerfile │ ├── README.md │ ├── jest.config.js │ ├── package-lock.json │ ├── package.json │ ├── publish.log │ ├── src │ ├── abi │ │ ├── .gitkeep │ │ ├── BnbX.json │ │ ├── StakeManager.json │ │ └── index.ts │ ├── agent.ts │ ├── bnbx-supply.ts │ ├── constants.ts │ ├── er-drop.ts │ ├── suspicious-mint.ts │ ├── suspicious-rewards.ts │ ├── suspicious-withdraw.ts │ └── utils.ts │ └── tsconfig.json ├── certora ├── run.sh └── specs │ └── StakeManager.spec ├── contracts ├── BnbX.sol ├── OperatorRegistry.sol ├── ProtocolConstants.sol ├── StakeManager.sol ├── StakeManagerV2.sol ├── campaigns │ └── KOLReferral.sol ├── interfaces │ ├── IBnbX.sol │ ├── IOperatorRegistry.sol │ ├── IStakeCredit.sol │ ├── IStakeHub.sol │ ├── IStakeManager.sol │ ├── IStakeManagerV2.sol │ └── ITokenHub.sol └── mocks │ └── TokenHubMock.sol ├── foundry.toml ├── hardhat.config.ts ├── legacy ├── INTEGRATION.md ├── legacy-addresses │ ├── alpha-deployment-info.json │ ├── kol-referral-info.json │ ├── mainnet-deployment-info.json │ └── testnet-deployment-info.json └── test │ └── hardhat-tests │ ├── KOLReferral.spec.ts │ └── StakeManager.spec.ts ├── package-lock.json ├── package.json ├── remappings.txt ├── script ├── foundry-scripts │ └── migration │ │ └── Migration.s.sol └── hardhat-scripts │ ├── deploy.ts │ └── tasks.ts ├── test ├── fork-tests │ ├── OperatorRegistryTests.t.sol │ ├── StakeManagerV2BasicChecks.t.sol │ ├── StakeManagerV2Delegations.t.sol │ ├── StakeManagerV2EdgeCases.t.sol │ ├── StakeManagerV2Setup.t.sol │ └── StakeManagerV2Undelegations.t.sol └── migration │ └── Migration.t.sol └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | DEV_PUB_ADDR=xxx 2 | BSC_SCAN_API_KEY=xxx 3 | BSC_MAINNET_RPC_URL=xxx 4 | CHAIN_ID=xxx 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | artifacts 3 | cache 4 | coverage 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: false, 4 | es2021: true, 5 | mocha: true, 6 | node: true, 7 | }, 8 | plugins: ["@typescript-eslint"], 9 | extends: [ 10 | "standard", 11 | "plugin:prettier/recommended", 12 | "plugin:node/recommended", 13 | ], 14 | parser: "@typescript-eslint/parser", 15 | parserOptions: { 16 | ecmaVersion: 12, 17 | }, 18 | rules: { 19 | "node/no-unsupported-features/es-syntax": [ 20 | "error", 21 | { ignores: ["modules"] }, 22 | ], 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # directories 2 | cache 3 | cache_forge 4 | out 5 | node_modules 6 | .env 7 | coverage 8 | coverage.json 9 | typechain 10 | typechain-types 11 | tmp 12 | 13 | #Hardhat files 14 | cache 15 | artifacts 16 | 17 | #local env variables 18 | .env 19 | .env.test 20 | 21 | .openzeppelin 22 | 23 | # foundry files 24 | broadcast 25 | 26 | # MacOS 27 | .DS_Store 28 | 29 | # Certora 30 | .certora* 31 | resource_errors.json 32 | last_conf* 33 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/openzeppelin-contracts-upgradeable"] 5 | path = lib/openzeppelin-contracts-upgradeable 6 | url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable 7 | [submodule "lib/openzeppelin-contracts"] 8 | path = lib/openzeppelin-contracts 9 | url = https://github.com/OpenZeppelin/openzeppelin-contracts 10 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | artifacts 3 | cache 4 | coverage* 5 | gasReporterOutput.json 6 | lib 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "rules": { 4 | "compiler-version": ["error", "^0.8.0"], 5 | "func-visibility": ["warn", { "ignoreConstructors": true }] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.solhintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "solidity.compileUsingRemoteVersion": "v0.8.25+commit.b61c2a91", 3 | "[solidity]": { 4 | "editor.defaultFormatter": "JuanBlanco.solidity" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # include .env file and export its env vars 2 | # (-include to ignore error if it does not exist) 3 | # Note that any unset variables here will wipe the variables if they are set in 4 | # .zshrc or .bashrc. Make sure that the variables are set in .env, especially if 5 | # you're running into issues with fork tests 6 | include .env 7 | 8 | # add your private key using below command 9 | # cast wallet import devKey --interactive 10 | 11 | # deploy contracts for kelp june upgrade 12 | migrate-mainnet :; forge script script/foundry-scripts/migration/Migration.s.sol:Migration --rpc-url ${BSC_MAINNET_RPC_URL} --account devKey --sender ${DEV_PUB_ADDR} --broadcast --etherscan-api-key ${BSC_SCAN_API_KEY} --verify -vvv 13 | migrate-local-test :; forge script script/foundry-scripts/migration/Migration.s.sol:Migration --rpc-url http://127.0.0.1:8545 --account devKey --sender ${DEV_PUB_ADDR} -vvv 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BNBx 2 | 3 | ## Description 4 | 5 | Staderlabs Liquid Staking Product on BSC 6 | 7 | ## Contracts 8 | 9 | ### Mainnet 10 | 11 | | Name | Address | 12 | | ---------------- | ------------------------------------------ | 13 | | BNBx Token | 0x1bdd3cf7f79cfb8edbb955f20ad99211551ba275 | 14 | | StakeManagerV2 | 0x3b961e83400D51e6E1AF5c450d3C7d7b80588d28 | 15 | | OperatorRegistry | 0x9C1759359Aa7D32911c5bAD613E836aEd7c621a8 | 16 | 17 | #### Multisigs and Timelocks 18 | 19 | | Name | Address | 20 | | -------- | ------------------------------------------ | 21 | | ADMIN | 0xb866E12b414d9f975034C4BA51498E6E64559a4c | 22 | | MANAGER | 0x79A2Ae748AC8bE4118B7a8096681B30310c3adBE | 23 | | TIMELOCK | 0xD990A252E7e36700d47520e46cD2B3E446836488 | 24 | 25 | ### Testnet 26 | 27 | | Name | Address | 28 | | ---------------- | ------------------------------------------ | 29 | | BNBx Token | 0x6cd3f51A92d022030d6e75760200c051caA7152A | 30 | | StakeManagerV2 | 0x1632E7D92763e7E0A1ABE5b3e9c2A808aeCcbD57 | 31 | | OperatorRegistry | 0x0735aD824354A919Ef32D3157505B7C3bc05e3f6 | 32 | 33 | ## Development 34 | 35 | ### 1. setup 36 | 37 | import wallet with cast 38 | 39 | ```bash 40 | cast wallet import devKey --interactive 41 | ``` 42 | 43 | prepare env variables 44 | copy `.env.example` to `.env` and fill it 45 | 46 | ### 2. compile contracts 47 | 48 | ```bash 49 | forge build 50 | ``` 51 | 52 | ### 3. run tests 53 | 54 | ```bash 55 | forge test 56 | ``` 57 | 58 | ### 4. run script on local 59 | 60 | run mainnet fork using anvil 61 | 62 | ```bash 63 | source .env 64 | anvil --fork-url $BSC_MAINNET_RPC_URL 65 | ``` 66 | 67 | open a new terminal 68 | 69 | ```bash 70 | make migrate-local-test 71 | ``` 72 | 73 | ### 5. run script on mainnet 74 | 75 | ```bash 76 | make migrate-mainnet 77 | ``` 78 | -------------------------------------------------------------------------------- /agents/bot-operations-agent/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | forta.config.json -------------------------------------------------------------------------------- /agents/bot-operations-agent/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | forta.config.json -------------------------------------------------------------------------------- /agents/bot-operations-agent/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage: compile Typescript to Javascript 2 | FROM node:12-alpine AS builder 3 | WORKDIR /app 4 | COPY . . 5 | RUN npm ci 6 | RUN npm run build 7 | 8 | # Final stage: copy compiled Javascript from previous stage and install production dependencies 9 | FROM node:12-alpine 10 | ENV NODE_ENV=production 11 | # Uncomment the following line to enable agent logging 12 | LABEL "network.forta.settings.agent-logs.enable"="true" 13 | WORKDIR /app 14 | COPY --from=builder /app/dist ./src 15 | COPY package*.json ./ 16 | RUN npm ci --production 17 | CMD [ "npm", "run", "start:prod" ] -------------------------------------------------------------------------------- /agents/bot-operations-agent/README.md: -------------------------------------------------------------------------------- 1 | # BNBx Bot Operations Agent 2 | 3 | ## Supported Chains 4 | 5 | - BSC 6 | 7 | ## Alerts 8 | 9 | - BNBx-REWARD-CHANGE 10 | 11 | - Fired when Reward changes by more than 0.5 % 12 | - Severity is set to "Medium" 13 | - Type is set to "Info" 14 | - metadata: lastRewardAmount, curentRewardAmount 15 | 16 | - BNBx-DAILY-REWARDS 17 | 18 | - Fired when Daily Rewards is not executed 19 | - Severity is set to "Critical" 20 | - Type is set to "Info" 21 | - metadata: lastRewardsTime 22 | 23 | - BNBx-START-DELEGATION 24 | 25 | - Fired when StartDelegation is not executed for 36 hours 26 | - Severity is set to "Critical" 27 | - Type is set to "Info" 28 | - metadata: lastStartDelegationTime 29 | 30 | - BNBx-COMPLETE-DELEGATION 31 | 32 | - Fired when CompleteDelegation is not executed for 12 hours past StartDelegation 33 | - Severity is set to "Critical" 34 | - Type is set to "Info" 35 | - metadata: lastStartDelegationTime, lastCompleteDelegationTime 36 | 37 | - BNBx-START-UNDELEGATION 38 | 39 | - Fired when StartUndelegation is not executed for 7 days and 1 hours 40 | - Severity is set to "Critical" 41 | - Type is set to "Info" 42 | - metadata: lastStartUndelegationTime 43 | 44 | - BNBx-UNDELEGATION-UPDATE 45 | 46 | - Fired when undelegationStarted is not executed for 12 hours past StartUndelegation 47 | - Severity is set to "Critical" 48 | - Type is set to "Info" 49 | - metadata: lastStartDelegationTime, lastUndelegationUpdateTime 50 | 51 | - BNBx-COMPLETE-UNDELEGATION 52 | 53 | - Fired when CompleteUndelegation is not executed for 8 days and 12 hours past StartUndelegation 54 | - Severity is set to "Critical" 55 | - Type is set to "Info" 56 | - metadata: lastStartUndelegationTime, lastCompleteUndelegationTime 57 | -------------------------------------------------------------------------------- /agents/bot-operations-agent/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | testPathIgnorePatterns: ["dist"], 5 | }; 6 | -------------------------------------------------------------------------------- /agents/bot-operations-agent/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bnbx-bot-operations-agent", 3 | "version": "0.0.1", 4 | "description": "Alerts on Off Chain Bot Delays and Failures", 5 | "chainIds": [ 6 | 1 7 | ], 8 | "scripts": { 9 | "build": "tsc", 10 | "start": "npm run start:dev", 11 | "start:dev": "nodemon --watch src --watch forta.config.json -e js,ts,json --exec \"npm run build && forta-agent run\"", 12 | "start:prod": "forta-agent run --prod", 13 | "tx": "npm run build && forta-agent run --tx", 14 | "block": "npm run build && forta-agent run --block", 15 | "range": "npm run build && forta-agent run --range", 16 | "file": "npm run build && forta-agent run --file", 17 | "publish": "forta-agent publish", 18 | "info": "forta-agent info", 19 | "logs": "forta-agent logs", 20 | "push": "forta-agent push", 21 | "disable": "forta-agent disable", 22 | "enable": "forta-agent enable", 23 | "keyfile": "forta-agent keyfile", 24 | "test": "jest" 25 | }, 26 | "dependencies": { 27 | "forta-agent": "^0.1.9" 28 | }, 29 | "devDependencies": { 30 | "@types/jest": "^27.0.1", 31 | "@types/nodemon": "^1.19.0", 32 | "jest": "^27.0.6", 33 | "nodemon": "^2.0.8", 34 | "ts-jest": "^27.0.3", 35 | "typescript": "^4.3.4" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /agents/bot-operations-agent/publish.log: -------------------------------------------------------------------------------- 1 | Tue, 30 Aug 2022 19:58:56 GMT: successfully added agent id 0xb62f2ab23098094bf40db49a7379222d44f94bc656cd67906cf173b2eadb2e9c with manifest QmaoTCcQD4DoJ4okGDdu9gNRSRm8wSQbG4onhBppQpP6W4 2 | Wed, 31 Aug 2022 20:59:13 GMT: successfully updated agent id 0xb62f2ab23098094bf40db49a7379222d44f94bc656cd67906cf173b2eadb2e9c with manifest QmVKTNiRU9k4naWSwVNK54GovBfVzd7xTin77hFp31g7yK 3 | Mon, 05 Sep 2022 18:28:37 GMT: successfully updated agent id 0xb62f2ab23098094bf40db49a7379222d44f94bc656cd67906cf173b2eadb2e9c with manifest QmYKn7Ytmdi6BWCS1McXiYME3VoSwNaejHqicjnWX4axEs 4 | Wed, 07 Sep 2022 19:30:06 GMT: successfully updated agent id 0xb62f2ab23098094bf40db49a7379222d44f94bc656cd67906cf173b2eadb2e9c with manifest QmWDUsnKTu2FTq9UXpSeJcEu9FiFjBiyoqnoqWnsPc5gzq 5 | -------------------------------------------------------------------------------- /agents/bot-operations-agent/src/agent.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from "ethers"; 2 | import { 3 | Finding, 4 | FindingSeverity, 5 | FindingType, 6 | HandleTransaction, 7 | TransactionEvent, 8 | } from "forta-agent"; 9 | import { 10 | COMPLETE_DELEGATION_DELAY, 11 | COMPLETE_DELEGATION_FN, 12 | COMPLETE_UNDELEGATION_DELAY, 13 | COMPLETE_UNDELEGATION_FN, 14 | protocol, 15 | REWARD_CHANGE_BPS, 16 | REWARD_DELAY_HOURS, 17 | REWARD_EVENT, 18 | STAKE_MANAGER, 19 | START_DELEGATION_DELAY, 20 | START_DELEGATION_FN, 21 | START_UNDELEGATION_DELAY, 22 | START_UNDELEGATION_FN, 23 | TOTAL_BPS, 24 | UNDELEGATION_UPDATE_DELAY, 25 | UNDELEGATION_UPDATE_FN, 26 | } from "./constants"; 27 | 28 | import { getHours } from "./utils"; 29 | 30 | let dailyRewardsFailed: boolean, 31 | lastRewardsTime: Date, 32 | lastRewardAmount: BigNumber; 33 | 34 | let lastStartDelegationTime: Date, startDelegationFailed: boolean; 35 | let lastCompleteDelegationTime: Date, completeDelegationFailed: boolean; 36 | 37 | let lastStartUndelegationTime: Date, startUndelegationFailed: boolean; 38 | let lastUndelegationUpdateTime: Date, undelegationUpdateFailed: boolean; 39 | let lastCompleteUndelegationTime: Date, completeUndelegationFailed: boolean; 40 | 41 | const handleTransaction: HandleTransaction = async ( 42 | txEvent: TransactionEvent 43 | ) => { 44 | const findings: Finding[] = ( 45 | await Promise.all([ 46 | // handleRewardTransaction(txEvent), 47 | handleStartDelegationTransaction(txEvent), 48 | handleCompleteDelegationTransaction(txEvent), 49 | handleStartUndelegationTransaction(txEvent), 50 | handleUndelegationUpdateTransaction(txEvent), 51 | handleCompleteUndelegationTransaction(txEvent), 52 | ]) 53 | ).flat(); 54 | 55 | return findings; 56 | }; 57 | 58 | // const handleRewardTransaction: HandleTransaction = async ( 59 | // txEvent: TransactionEvent 60 | // ) => { 61 | // const findings: Finding[] = []; 62 | // const bnbxRewardEvents = txEvent.filterLog(REWARD_EVENT, STAKE_MANAGER); 63 | 64 | // if (bnbxRewardEvents.length) { 65 | // const { _amount } = bnbxRewardEvents[0].args; 66 | // if (lastRewardsTime) { 67 | // if ( 68 | // lastRewardAmount 69 | // .sub(_amount) 70 | // .abs() 71 | // .gt(lastRewardAmount.mul(REWARD_CHANGE_BPS).div(TOTAL_BPS)) 72 | // ) { 73 | // findings.push( 74 | // Finding.fromObject({ 75 | // name: "Significant Reward Change", 76 | // description: `Reward changed more than ${ 77 | // (REWARD_CHANGE_BPS * 100) / TOTAL_BPS 78 | // } %`, 79 | // alertId: "BNBx-REWARD-CHANGE", 80 | // protocol: protocol, 81 | // severity: FindingSeverity.Medium, 82 | // type: FindingType.Info, 83 | // metadata: { 84 | // lastRewardAmount: lastRewardAmount.toString(), 85 | // cuurentRewardAmount: _amount.toString(), 86 | // }, 87 | // }) 88 | // ); 89 | // } 90 | // } 91 | 92 | // lastRewardsTime = new Date(); 93 | // dailyRewardsFailed = false; 94 | // lastRewardAmount = _amount; 95 | 96 | // return findings; 97 | // } 98 | 99 | // if (!lastRewardsTime) return findings; 100 | 101 | // if (dailyRewardsFailed) return findings; 102 | 103 | // const currentTime = new Date(); 104 | // const diff = currentTime.getTime() - lastRewardsTime.getTime(); 105 | // const diffHours = getHours(diff); 106 | // if (diffHours > REWARD_DELAY_HOURS) { 107 | // findings.push( 108 | // Finding.fromObject({ 109 | // name: "Daily Rewards Failed", 110 | // description: `Daily Rewards Autocompund not invoked since ${REWARD_DELAY_HOURS} Hours`, 111 | // alertId: "BNBx-DAILY-REWARDS", 112 | // protocol: protocol, 113 | // severity: FindingSeverity.Critical, 114 | // type: FindingType.Info, 115 | // metadata: { 116 | // lastRewardsTime: lastRewardsTime.toUTCString(), 117 | // }, 118 | // }) 119 | // ); 120 | // dailyRewardsFailed = true; 121 | // } 122 | 123 | // return findings; 124 | // }; 125 | 126 | const handleStartDelegationTransaction: HandleTransaction = async ( 127 | txEvent: TransactionEvent 128 | ) => { 129 | const findings: Finding[] = []; 130 | const startDelegationInvocations = txEvent.filterFunction( 131 | START_DELEGATION_FN, 132 | STAKE_MANAGER 133 | ); 134 | 135 | if (startDelegationInvocations.length) { 136 | lastStartDelegationTime = new Date(); 137 | startDelegationFailed = false; 138 | return findings; 139 | } 140 | 141 | if (!lastStartDelegationTime) return findings; 142 | 143 | if (startDelegationFailed) return findings; 144 | 145 | const currentTime = new Date(); 146 | const diff = currentTime.getTime() - lastStartDelegationTime.getTime(); 147 | const diffHours = getHours(diff); 148 | if (diffHours > START_DELEGATION_DELAY) { 149 | findings.push( 150 | Finding.fromObject({ 151 | name: "Start Delegation Failed", 152 | description: `Start Delegation not invoked since ${START_DELEGATION_DELAY} Hours`, 153 | alertId: "BNBx-START-DELEGATION", 154 | protocol: protocol, 155 | severity: FindingSeverity.Critical, 156 | type: FindingType.Info, 157 | metadata: { 158 | lastStartDelegationTime: lastStartDelegationTime.toUTCString(), 159 | }, 160 | }) 161 | ); 162 | startDelegationFailed = true; 163 | } 164 | 165 | return findings; 166 | }; 167 | 168 | const handleCompleteDelegationTransaction: HandleTransaction = async ( 169 | txEvent: TransactionEvent 170 | ) => { 171 | const findings: Finding[] = []; 172 | const completeDelegationInvocations = txEvent.filterFunction( 173 | COMPLETE_DELEGATION_FN, 174 | STAKE_MANAGER 175 | ); 176 | 177 | if (completeDelegationInvocations.length) { 178 | lastCompleteDelegationTime = new Date(); 179 | completeDelegationFailed = false; 180 | return findings; 181 | } 182 | 183 | if (!lastStartDelegationTime || !lastCompleteDelegationTime) { 184 | return findings; 185 | } 186 | 187 | if (startDelegationFailed || completeDelegationFailed) return findings; 188 | 189 | const currentTime = new Date(); 190 | const diff = currentTime.getTime() - lastStartDelegationTime.getTime(); 191 | const diffHours = getHours(diff); 192 | 193 | if ( 194 | diffHours > COMPLETE_DELEGATION_DELAY && 195 | lastStartDelegationTime.getTime() > lastCompleteDelegationTime.getTime() 196 | ) { 197 | findings.push( 198 | Finding.fromObject({ 199 | name: "Complete Delegation Failed", 200 | description: `Complete Delegation not invoked since ${COMPLETE_DELEGATION_DELAY} Hours past last Start Delegation`, 201 | alertId: "BNBx-COMPLETE-DELEGATION", 202 | protocol: protocol, 203 | severity: FindingSeverity.Critical, 204 | type: FindingType.Info, 205 | metadata: { 206 | lastStartDelegationTime: lastStartDelegationTime.toUTCString(), 207 | lastCompleteDelegationTime: lastCompleteDelegationTime.toUTCString(), 208 | }, 209 | }) 210 | ); 211 | completeDelegationFailed = true; 212 | } 213 | 214 | return findings; 215 | }; 216 | 217 | const handleStartUndelegationTransaction: HandleTransaction = async ( 218 | txEvent: TransactionEvent 219 | ) => { 220 | const findings: Finding[] = []; 221 | const startUndelegationInvocations = txEvent.filterFunction( 222 | START_UNDELEGATION_FN, 223 | STAKE_MANAGER 224 | ); 225 | 226 | if (startUndelegationInvocations.length) { 227 | lastStartUndelegationTime = new Date(); 228 | startUndelegationFailed = false; 229 | return findings; 230 | } 231 | 232 | if (!lastStartUndelegationTime) return findings; 233 | 234 | if (startUndelegationFailed) return findings; 235 | 236 | const currentTime = new Date(); 237 | const diff = currentTime.getTime() - lastStartUndelegationTime.getTime(); 238 | const diffHours = getHours(diff); 239 | if (diffHours > START_UNDELEGATION_DELAY) { 240 | findings.push( 241 | Finding.fromObject({ 242 | name: "Start Undelegation Failed", 243 | description: `Start Undelegation not invoked since ${START_UNDELEGATION_DELAY} Hours`, 244 | alertId: "BNBx-START-UNDELEGATION", 245 | protocol: protocol, 246 | severity: FindingSeverity.Critical, 247 | type: FindingType.Info, 248 | metadata: { 249 | lastStartUndelegationTime: lastStartUndelegationTime.toUTCString(), 250 | }, 251 | }) 252 | ); 253 | startUndelegationFailed = true; 254 | } 255 | 256 | return findings; 257 | }; 258 | 259 | const handleUndelegationUpdateTransaction: HandleTransaction = async ( 260 | txEvent: TransactionEvent 261 | ) => { 262 | const findings: Finding[] = []; 263 | const UndelegationUpdateInvocations = txEvent.filterFunction( 264 | UNDELEGATION_UPDATE_FN, 265 | STAKE_MANAGER 266 | ); 267 | 268 | if (UndelegationUpdateInvocations.length) { 269 | lastUndelegationUpdateTime = new Date(); 270 | undelegationUpdateFailed = false; 271 | return findings; 272 | } 273 | 274 | if (!lastStartUndelegationTime || !lastUndelegationUpdateTime) { 275 | return findings; 276 | } 277 | 278 | if (startUndelegationFailed || undelegationUpdateFailed) return findings; 279 | 280 | const currentTime = new Date(); 281 | const diff = currentTime.getTime() - lastStartUndelegationTime.getTime(); 282 | const diffHours = getHours(diff); 283 | 284 | if ( 285 | diffHours > UNDELEGATION_UPDATE_DELAY && 286 | lastStartUndelegationTime.getTime() > lastUndelegationUpdateTime.getTime() 287 | ) { 288 | findings.push( 289 | Finding.fromObject({ 290 | name: "Undelegation Update Failed", 291 | description: `Undelegation not invoked at Beacon Chain since ${UNDELEGATION_UPDATE_DELAY} Hours past last Start UnDelegation`, 292 | alertId: "BNBx-UNDELEGATION-UPDATE", 293 | protocol: protocol, 294 | severity: FindingSeverity.Critical, 295 | type: FindingType.Info, 296 | metadata: { 297 | lastStartUndelegationTime: lastStartUndelegationTime.toUTCString(), 298 | lastUndelegationUpdateTime: lastUndelegationUpdateTime.toUTCString(), 299 | }, 300 | }) 301 | ); 302 | undelegationUpdateFailed = true; 303 | } 304 | 305 | return findings; 306 | }; 307 | 308 | const handleCompleteUndelegationTransaction: HandleTransaction = async ( 309 | txEvent: TransactionEvent 310 | ) => { 311 | const findings: Finding[] = []; 312 | const completeUndelegationInvocations = txEvent.filterFunction( 313 | COMPLETE_UNDELEGATION_FN, 314 | STAKE_MANAGER 315 | ); 316 | 317 | if (completeUndelegationInvocations.length) { 318 | lastCompleteUndelegationTime = new Date(); 319 | completeUndelegationFailed = false; 320 | return findings; 321 | } 322 | 323 | if (!lastStartUndelegationTime || !lastCompleteUndelegationTime) { 324 | return findings; 325 | } 326 | 327 | if (startUndelegationFailed || completeUndelegationFailed) return findings; 328 | 329 | const currentTime = new Date(); 330 | const diff = currentTime.getTime() - lastStartUndelegationTime.getTime(); 331 | const diffHours = getHours(diff); 332 | 333 | if ( 334 | diffHours > COMPLETE_UNDELEGATION_DELAY && 335 | lastStartUndelegationTime.getTime() > lastCompleteUndelegationTime.getTime() 336 | ) { 337 | findings.push( 338 | Finding.fromObject({ 339 | name: "Complete Undelegation Failed", 340 | description: `Complete Undelegation not invoked since ${COMPLETE_UNDELEGATION_DELAY} Hours past last Start Undelegation`, 341 | alertId: "BNBx-COMPLETE-UNDELEGATION", 342 | protocol: protocol, 343 | severity: FindingSeverity.Critical, 344 | type: FindingType.Info, 345 | metadata: { 346 | lastStartUndelegationTime: lastStartUndelegationTime.toUTCString(), 347 | lastCompleteUndelegationTime: 348 | lastCompleteUndelegationTime.toUTCString(), 349 | }, 350 | }) 351 | ); 352 | completeUndelegationFailed = true; 353 | } 354 | 355 | return findings; 356 | }; 357 | 358 | export default { 359 | handleTransaction, 360 | }; 361 | -------------------------------------------------------------------------------- /agents/bot-operations-agent/src/constants.ts: -------------------------------------------------------------------------------- 1 | const protocol = "BNBx Stader"; 2 | const STAKE_MANAGER = "0x7276241a669489E4BBB76f63d2A43Bfe63080F2F"; 3 | const REWARD_EVENT = "event Redelegate(uint256 _rewardsId, uint256 _amount)"; 4 | const REWARD_DELAY_HOURS = 24; 5 | const REWARD_CHANGE_BPS = 50; // 0 - 10_000 6 | const TOTAL_BPS = 10000; 7 | 8 | const START_DELEGATION_FN = "function startDelegation()"; 9 | const START_DELEGATION_DELAY = 36; 10 | 11 | const COMPLETE_DELEGATION_FN = "function completeDelegation(uint256 _uuid)"; 12 | const COMPLETE_DELEGATION_DELAY = 12; 13 | 14 | const START_UNDELEGATION_FN = "function startUndelegation()"; 15 | const START_UNDELEGATION_DELAY = 169; 16 | 17 | const UNDELEGATION_UPDATE_FN = "function undelegationStarted(uint256 _uuid)"; 18 | const UNDELEGATION_UPDATE_DELAY = 12; 19 | 20 | const COMPLETE_UNDELEGATION_FN = "function completeUndelegation(uint256 _uuid)"; 21 | const COMPLETE_UNDELEGATION_DELAY = 193; 22 | 23 | export { 24 | protocol, 25 | REWARD_EVENT, 26 | REWARD_DELAY_HOURS, 27 | STAKE_MANAGER, 28 | REWARD_CHANGE_BPS, 29 | TOTAL_BPS, 30 | START_DELEGATION_FN, 31 | START_DELEGATION_DELAY, 32 | COMPLETE_DELEGATION_FN, 33 | COMPLETE_DELEGATION_DELAY, 34 | START_UNDELEGATION_FN, 35 | START_UNDELEGATION_DELAY, 36 | UNDELEGATION_UPDATE_FN, 37 | UNDELEGATION_UPDATE_DELAY, 38 | COMPLETE_UNDELEGATION_FN, 39 | COMPLETE_UNDELEGATION_DELAY, 40 | }; 41 | -------------------------------------------------------------------------------- /agents/bot-operations-agent/src/utils.ts: -------------------------------------------------------------------------------- 1 | const getHours = (miliSecs: number) => { 2 | return miliSecs / (1000 * 60 * 60); 3 | }; 4 | 5 | export { getHours }; 6 | -------------------------------------------------------------------------------- /agents/bot-operations-agent/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */, 8 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 9 | "lib": [ 10 | "es2019" 11 | ] /* Specify library files to be included in the compilation. */, 12 | // "allowJs": true, /* Allow javascript files to be compiled. */ 13 | // "checkJs": true, /* Report errors in .js files. */ 14 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ 15 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 16 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 17 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 18 | // "outFile": "./", /* Concatenate and emit output to single file. */ 19 | "outDir": "dist" /* Redirect output structure to the directory. */, 20 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 21 | // "composite": true, /* Enable project compilation */ 22 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 23 | // "removeComments": true, /* Do not emit comments to output. */ 24 | // "noEmit": true, /* Do not emit outputs. */ 25 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 26 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 27 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 28 | 29 | /* Strict Type-Checking Options */ 30 | "strict": true /* Enable all strict type-checking options. */, 31 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 32 | // "strictNullChecks": true, /* Enable strict null checks. */ 33 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 34 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 35 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 36 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 37 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 38 | 39 | /* Additional Checks */ 40 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 41 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 42 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 43 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 44 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 45 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */ 46 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ 47 | 48 | /* Module Resolution Options */ 49 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 50 | "baseUrl": "." /* Base directory to resolve non-absolute module names. */, 51 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 52 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 53 | // "typeRoots": [], /* List of folders to include type definitions from. */ 54 | // "types": [], /* Type declaration files to be included in compilation. */ 55 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 56 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 57 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 58 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 59 | 60 | /* Source Map Options */ 61 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 62 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 63 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 64 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 65 | 66 | /* Experimental Options */ 67 | "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, 68 | "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */, 69 | 70 | /* Advanced Options */ 71 | "skipLibCheck": true /* Skip type checking of declaration files. */, 72 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /agents/suspicious-amount-agent/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | forta.config.json -------------------------------------------------------------------------------- /agents/suspicious-amount-agent/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | forta.config.json -------------------------------------------------------------------------------- /agents/suspicious-amount-agent/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage: compile Typescript to Javascript 2 | FROM node:12-alpine AS builder 3 | WORKDIR /app 4 | COPY . . 5 | RUN npm ci 6 | RUN npm run build 7 | 8 | # Final stage: copy compiled Javascript from previous stage and install production dependencies 9 | FROM node:12-alpine 10 | ENV NODE_ENV=production 11 | # Uncomment the following line to enable agent logging 12 | LABEL "network.forta.settings.agent-logs.enable"="true" 13 | WORKDIR /app 14 | COPY --from=builder /app/dist ./src 15 | COPY package*.json ./ 16 | RUN npm ci --production 17 | CMD [ "npm", "run", "start:prod" ] -------------------------------------------------------------------------------- /agents/suspicious-amount-agent/README.md: -------------------------------------------------------------------------------- 1 | # BNBx Suspicious Amount Agent 2 | 3 | ## Supported Chains 4 | 5 | - BSC 6 | 7 | ## Alerts 8 | 9 | - BNBx-LARGE-MINT 10 | 11 | - Fired when BNBx is minted in large amount (500 BNBx) 12 | - Severity is set to "High" 13 | - Type is set to "Info" 14 | - metadata: to, value 15 | 16 | - BNBx-LARGE-UNSTAKE 17 | 18 | - Fired when User unstakes large amount of BNBx (500 BNBx) 19 | - Severity is set to "High" 20 | - Type is set to "Info" 21 | - metadata: account, amountInBnbX, 22 | 23 | - BNBx-ER-DROP 24 | 25 | - Fired when Exchange Rate Drops 26 | - Severity is set to "Critical" 27 | - Type is set to "Exploit" 28 | - metadata: lastER, currentER 29 | 30 | - BNBx-SUPPLY-MISMATCH 31 | 32 | - Fired when ER \* BNBX_SUPPLY != TOTAL_POOLED_BNB 33 | - Severity is set to "Critical" 34 | - Type is set to "Exploit" 35 | - metadata: currentER, totalPooledBnb, currentSupply 36 | 37 | - BNBx-SUPPLY-CHANGE 38 | 39 | - Fired when BNBx Supply Changes by 10% 40 | - Severity is set to "High" 41 | - Type is set to "Suspicious" 42 | - metadata: lastSupply, currentSupply 43 | -------------------------------------------------------------------------------- /agents/suspicious-amount-agent/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | testPathIgnorePatterns: ["dist"], 5 | }; 6 | -------------------------------------------------------------------------------- /agents/suspicious-amount-agent/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bnbx-suspicious-amount-agent", 3 | "version": "0.0.1", 4 | "description": "Alerts on suspicious amount of BNBx minted, unstaked, Rewards", 5 | "chainIds": [ 6 | 1 7 | ], 8 | "scripts": { 9 | "build": "tsc", 10 | "start": "npm run start:dev", 11 | "start:dev": "nodemon --watch src --watch forta.config.json -e js,ts,json --exec \"npm run build && forta-agent run\"", 12 | "start:prod": "forta-agent run --prod", 13 | "tx": "npm run build && forta-agent run --tx", 14 | "block": "npm run build && forta-agent run --block", 15 | "range": "npm run build && forta-agent run --range", 16 | "file": "npm run build && forta-agent run --file", 17 | "publish": "forta-agent publish", 18 | "info": "forta-agent info", 19 | "logs": "forta-agent logs", 20 | "push": "forta-agent push", 21 | "disable": "forta-agent disable", 22 | "enable": "forta-agent enable", 23 | "keyfile": "forta-agent keyfile", 24 | "test": "jest" 25 | }, 26 | "dependencies": { 27 | "forta-agent": "^0.1.9" 28 | }, 29 | "devDependencies": { 30 | "@types/jest": "^27.0.1", 31 | "@types/nodemon": "^1.19.0", 32 | "jest": "^27.0.6", 33 | "nodemon": "^2.0.8", 34 | "ts-jest": "^27.0.3", 35 | "typescript": "^4.3.4" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /agents/suspicious-amount-agent/publish.log: -------------------------------------------------------------------------------- 1 | Thu, 25 Aug 2022 22:23:39 GMT: successfully added agent id 0x1b10e9f5cd672d1b759031ac829aaac2d4466ee01724254a62e479259ac03d93 with manifest QmV5rx5wpFvwXCPDeaWvDAeMdmNJ6tpQjbEef3SYQDdfKg 2 | Thu, 25 Aug 2022 22:46:08 GMT: successfully updated agent id 0x1b10e9f5cd672d1b759031ac829aaac2d4466ee01724254a62e479259ac03d93 with manifest QmWmHYveEW2Wf6EYmu16Ggc2nF8paxcmxX2nh7FyMUPUNv 3 | Tue, 30 Aug 2022 19:55:30 GMT: successfully updated agent id 0x1b10e9f5cd672d1b759031ac829aaac2d4466ee01724254a62e479259ac03d93 with manifest QmV73JtmKQtDpZcLxKb7Fvy3rozk5eNSqNqSTmvKCooPHF 4 | Wed, 31 Aug 2022 21:02:06 GMT: successfully updated agent id 0x1b10e9f5cd672d1b759031ac829aaac2d4466ee01724254a62e479259ac03d93 with manifest QmNwrZbbxJwkNGuwiRNQ8nECnXMPoTmqKwVAYiqCSDKh3Z 5 | Mon, 05 Sep 2022 18:21:48 GMT: successfully updated agent id 0x1b10e9f5cd672d1b759031ac829aaac2d4466ee01724254a62e479259ac03d93 with manifest QmVXQMsM3hWxmKxoWWEtZvLHZDemJaowjoPMDsmLMTcxz6 6 | Tue, 06 Sep 2022 10:56:42 GMT: successfully updated agent id 0x1b10e9f5cd672d1b759031ac829aaac2d4466ee01724254a62e479259ac03d93 with manifest QmZ46oxayDxyC5khFQ9M2Xv7C5Rsid4wcgSSYHk74Z3PD9 7 | Wed, 07 Sep 2022 06:05:40 GMT: successfully updated agent id 0x1b10e9f5cd672d1b759031ac829aaac2d4466ee01724254a62e479259ac03d93 with manifest QmP6WuQbcAWfxMN3nS9jd8c19biy3iE5idd8n9kSgt2Kec 8 | Mon, 03 Oct 2022 19:15:16 GMT: successfully updated agent id 0x1b10e9f5cd672d1b759031ac829aaac2d4466ee01724254a62e479259ac03d93 with manifest QmQ1JbT3SiDyi7g2u5xhnWDwJvU7coMXnwDoaoWNrSGBQN 9 | -------------------------------------------------------------------------------- /agents/suspicious-amount-agent/src/abi/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stader-labs/bnbX/a77cf295f4d169f955cd0393118f2efd85ba7a3e/agents/suspicious-amount-agent/src/abi/.gitkeep -------------------------------------------------------------------------------- /agents/suspicious-amount-agent/src/abi/index.ts: -------------------------------------------------------------------------------- 1 | import StakeManager from "./StakeManager.json"; 2 | import BnbX from "./BnbX.json"; 3 | const abis = { 4 | StakeManager, 5 | BnbX, 6 | }; 7 | 8 | export default abis; 9 | -------------------------------------------------------------------------------- /agents/suspicious-amount-agent/src/agent.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BlockEvent, 3 | Finding, 4 | HandleBlock, 5 | HandleTransaction, 6 | TransactionEvent, 7 | } from "forta-agent"; 8 | 9 | import * as suspiciousMint from "./suspicious-mint"; 10 | import * as suspiciousWithdraw from "./suspicious-withdraw"; 11 | import * as erDrop from "./er-drop"; 12 | import * as supplyChange from "./bnbx-supply"; 13 | 14 | const handleTransaction: HandleTransaction = async ( 15 | txEvent: TransactionEvent 16 | ) => { 17 | const findings: Finding[] = ( 18 | await Promise.all([ 19 | suspiciousMint.handleTransaction(txEvent), 20 | suspiciousWithdraw.handleTransaction(txEvent), 21 | ]) 22 | ).flat(); 23 | 24 | return findings; 25 | }; 26 | 27 | const handleBlock: HandleBlock = async (blockEvent: BlockEvent) => { 28 | const findings: Finding[] = ( 29 | await Promise.all([ 30 | erDrop.handleBlock(blockEvent), 31 | supplyChange.handleBlock(blockEvent), 32 | ]) 33 | ).flat(); 34 | 35 | return findings; 36 | }; 37 | 38 | export default { 39 | handleTransaction, 40 | handleBlock, 41 | }; 42 | -------------------------------------------------------------------------------- /agents/suspicious-amount-agent/src/bnbx-supply.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BlockEvent, 3 | ethers, 4 | EventType, 5 | Finding, 6 | FindingSeverity, 7 | FindingType, 8 | getEthersProvider, 9 | HandleBlock, 10 | } from "forta-agent"; 11 | import { 12 | BNBx, 13 | BNBX_SUPPLY_CHANGE_HOURS, 14 | BNBX_SUPPLY_CHANGE_PCT, 15 | protocol, 16 | STAKE_MANAGER, 17 | } from "./constants"; 18 | 19 | import abis from "./abi"; 20 | import { BigNumber } from "ethers"; 21 | import { getHours } from "./utils"; 22 | 23 | let lastSupply: BigNumber, lastSupplyTime: Date; 24 | let supplyMismatch: boolean; 25 | 26 | const handleBlock: HandleBlock = async (blockEvent: BlockEvent) => { 27 | const findings: Finding[] = []; 28 | 29 | if (blockEvent.type === EventType.REORG) return findings; 30 | 31 | const bnbX = new ethers.Contract(BNBx, abis.BnbX.abi, getEthersProvider()); 32 | const stakeManager = new ethers.Contract( 33 | STAKE_MANAGER, 34 | abis.StakeManager.abi, 35 | getEthersProvider() 36 | ); 37 | 38 | const oneEther = ethers.utils.parseEther("1"); 39 | const currentER: BigNumber = await stakeManager.convertBnbXToBnb(oneEther, { 40 | blockTag: blockEvent.blockNumber, 41 | }); 42 | const totalPooledBnb: BigNumber = await stakeManager.getTotalPooledBnb({ 43 | blockTag: blockEvent.blockNumber, 44 | }); 45 | const currentSupply: BigNumber = await bnbX.totalSupply({ 46 | blockTag: blockEvent.blockNumber, 47 | }); 48 | const currentSupplyTime: Date = new Date(); 49 | 50 | if ( 51 | currentER 52 | .mul(currentSupply) 53 | .div(oneEther) 54 | .sub(totalPooledBnb) 55 | .abs() 56 | .div(oneEther) 57 | .gt(1) 58 | ) { 59 | if (!supplyMismatch) { 60 | findings.push( 61 | Finding.fromObject({ 62 | name: "BNBx Supply Mis-Match", 63 | description: `BNBx, ER and TotalPooledBnb doesn't match`, 64 | alertId: "BNBx-SUPPLY-MISMATCH", 65 | protocol: protocol, 66 | severity: FindingSeverity.Critical, 67 | type: FindingType.Exploit, 68 | metadata: { 69 | currentER: currentER.toString(), 70 | totalPooledBnb: totalPooledBnb.toString(), 71 | currentSupply: currentSupply.toString(), 72 | }, 73 | }) 74 | ); 75 | supplyMismatch = true; 76 | } 77 | } else { 78 | supplyMismatch = false; 79 | } 80 | 81 | if (!lastSupply) { 82 | lastSupply = currentSupply; 83 | lastSupplyTime = currentSupplyTime; 84 | return findings; 85 | } 86 | const diffHours = getHours( 87 | currentSupplyTime.getTime() - lastSupplyTime.getTime() 88 | ); 89 | if (diffHours > BNBX_SUPPLY_CHANGE_HOURS) { 90 | if ( 91 | currentSupply 92 | .sub(lastSupply) 93 | .abs() 94 | .gt(lastSupply.mul(BNBX_SUPPLY_CHANGE_PCT).div(100)) 95 | ) { 96 | findings.push( 97 | Finding.fromObject({ 98 | name: "BNBx Supply Change", 99 | description: `BNBx Total Supply changed more than ${BNBX_SUPPLY_CHANGE_PCT} %`, 100 | alertId: "BNBx-SUPPLY-CHANGE", 101 | protocol: protocol, 102 | severity: FindingSeverity.High, 103 | type: FindingType.Suspicious, 104 | metadata: { 105 | lastSupply: lastSupply.toString(), 106 | currentSupply: currentSupply.toString(), 107 | }, 108 | }) 109 | ); 110 | } 111 | 112 | lastSupplyTime = currentSupplyTime; 113 | lastSupply = currentSupply; 114 | } 115 | 116 | return findings; 117 | }; 118 | 119 | export { handleBlock }; 120 | -------------------------------------------------------------------------------- /agents/suspicious-amount-agent/src/constants.ts: -------------------------------------------------------------------------------- 1 | const protocol = "BNBx Stader"; 2 | const BNBx = "0x1bdd3Cf7F79cfB8EdbB955f20ad99211551BA275"; 3 | const STAKE_MANAGER = "0x7276241a669489E4BBB76f63d2A43Bfe63080F2F"; 4 | 5 | const BEP20_TRANSFER_EVENT = 6 | "event Transfer(address indexed from, address indexed to, uint256 value)"; 7 | const REQUEST_WITHDRAW_EVENT = 8 | "event RequestWithdraw(address indexed _account, uint256 _amountInBnbX)"; 9 | const REWARD_EVENT = "event Redelegate(uint256 _rewardsId, uint256 _amount)"; 10 | 11 | const BNBX_MINT_THRESHOLD = "500"; 12 | const BNBX_UNSTAKE_THRESHOLD = "500"; 13 | const MIN_REWARD_THRESHOLD = "1"; 14 | const MAX_REWARD_THRESHOLD = "20"; 15 | 16 | const BNBX_SUPPLY_CHANGE_PCT = 10; 17 | const BNBX_SUPPLY_CHANGE_HOURS = 1; 18 | 19 | export { 20 | protocol, 21 | BNBx, 22 | STAKE_MANAGER, 23 | BEP20_TRANSFER_EVENT, 24 | REQUEST_WITHDRAW_EVENT, 25 | REWARD_EVENT, 26 | BNBX_MINT_THRESHOLD, 27 | BNBX_UNSTAKE_THRESHOLD, 28 | MIN_REWARD_THRESHOLD, 29 | MAX_REWARD_THRESHOLD, 30 | BNBX_SUPPLY_CHANGE_PCT, 31 | BNBX_SUPPLY_CHANGE_HOURS, 32 | }; 33 | -------------------------------------------------------------------------------- /agents/suspicious-amount-agent/src/er-drop.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BlockEvent, 3 | ethers, 4 | Finding, 5 | FindingSeverity, 6 | FindingType, 7 | getEthersProvider, 8 | HandleBlock, 9 | } from "forta-agent"; 10 | import { protocol, STAKE_MANAGER } from "./constants"; 11 | 12 | import abis from "./abi"; 13 | import { BigNumber } from "ethers"; 14 | 15 | let lastER: BigNumber; 16 | 17 | const handleBlock: HandleBlock = async (blockEvent: BlockEvent) => { 18 | const findings: Finding[] = []; 19 | 20 | const stakeManager = new ethers.Contract( 21 | STAKE_MANAGER, 22 | abis.StakeManager.abi, 23 | getEthersProvider() 24 | ); 25 | const oneEther = ethers.utils.parseEther("1"); 26 | const currentER: BigNumber = await stakeManager.convertBnbXToBnb(oneEther, { 27 | blockTag: blockEvent.blockNumber, 28 | }); 29 | if (lastER && currentER.lt(lastER)) { 30 | findings.push( 31 | Finding.fromObject({ 32 | name: "ER Drop", 33 | description: `Exchange Rate Dropped`, 34 | alertId: "BNBx-ER-DROP", 35 | protocol: protocol, 36 | severity: FindingSeverity.Critical, 37 | type: FindingType.Exploit, 38 | metadata: { 39 | lastER: lastER.toString(), 40 | currentER: currentER.toString(), 41 | }, 42 | }) 43 | ); 44 | } 45 | 46 | lastER = currentER; 47 | return findings; 48 | }; 49 | 50 | export { handleBlock }; 51 | -------------------------------------------------------------------------------- /agents/suspicious-amount-agent/src/suspicious-mint.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ethers, 3 | Finding, 4 | FindingSeverity, 5 | FindingType, 6 | HandleTransaction, 7 | TransactionEvent, 8 | } from "forta-agent"; 9 | import { 10 | BEP20_TRANSFER_EVENT, 11 | BNBx, 12 | BNBX_MINT_THRESHOLD, 13 | protocol, 14 | } from "./constants"; 15 | 16 | const handleTransaction: HandleTransaction = async ( 17 | txEvent: TransactionEvent 18 | ) => { 19 | const findings: Finding[] = []; 20 | 21 | // filter the transaction logs for BNBx mint events 22 | const bnbxMintEvents = txEvent 23 | .filterLog(BEP20_TRANSFER_EVENT, BNBx) 24 | .filter((transferEvent) => { 25 | const { from } = transferEvent.args; 26 | return from === "0x0000000000000000000000000000000000000000"; 27 | }); 28 | 29 | bnbxMintEvents.forEach((mintEvent) => { 30 | const { to, value } = mintEvent.args; 31 | 32 | const normalizedValue = ethers.utils.formatEther(value); 33 | const minThreshold = ethers.utils.parseEther(BNBX_MINT_THRESHOLD); 34 | 35 | if (value.gt(minThreshold)) { 36 | findings.push( 37 | Finding.fromObject({ 38 | name: "Large BNBx Mint", 39 | description: `Large amount of BNBx minted: ${normalizedValue}`, 40 | alertId: "BNBx-LARGE-MINT", 41 | protocol: protocol, 42 | severity: FindingSeverity.High, 43 | type: FindingType.Info, 44 | metadata: { 45 | to, 46 | value: value.toString(), 47 | }, 48 | }) 49 | ); 50 | } 51 | }); 52 | 53 | return findings; 54 | }; 55 | 56 | export { handleTransaction }; 57 | -------------------------------------------------------------------------------- /agents/suspicious-amount-agent/src/suspicious-rewards.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ethers, 3 | Finding, 4 | FindingSeverity, 5 | FindingType, 6 | HandleTransaction, 7 | TransactionEvent, 8 | } from "forta-agent"; 9 | import { 10 | MAX_REWARD_THRESHOLD, 11 | MIN_REWARD_THRESHOLD, 12 | protocol, 13 | REWARD_EVENT, 14 | STAKE_MANAGER, 15 | } from "./constants"; 16 | 17 | const handleTransaction: HandleTransaction = async ( 18 | txEvent: TransactionEvent 19 | ) => { 20 | const findings: Finding[] = []; 21 | 22 | const bnbxRewardEvents = txEvent.filterLog(REWARD_EVENT, STAKE_MANAGER); 23 | 24 | bnbxRewardEvents.forEach((rewardEvents) => { 25 | const { _rewardsId, _amount } = rewardEvents.args; 26 | 27 | const normalizedValue = ethers.utils.formatEther(_amount); 28 | const minThreshold = ethers.utils.parseEther(MIN_REWARD_THRESHOLD); 29 | const maxThreshold = ethers.utils.parseEther(MAX_REWARD_THRESHOLD); 30 | 31 | if (_amount.lt(minThreshold)) { 32 | findings.push( 33 | Finding.fromObject({ 34 | name: "Low BNBx Reward", 35 | description: `Low amount of BNBx Reward Received: ${normalizedValue}`, 36 | alertId: "BNBx-3", 37 | protocol: protocol, 38 | severity: FindingSeverity.High, 39 | type: FindingType.Info, 40 | metadata: { 41 | rewardsId: _rewardsId.toString(), 42 | amount: _amount.toString(), 43 | }, 44 | }) 45 | ); 46 | } 47 | 48 | if (_amount.gt(maxThreshold)) { 49 | findings.push( 50 | Finding.fromObject({ 51 | name: "High BNBx Reward", 52 | description: `High amount of BNBx Reward Received: ${normalizedValue}`, 53 | alertId: "BNBx-4", 54 | protocol: "BNBx Stader", 55 | severity: FindingSeverity.High, 56 | type: FindingType.Info, 57 | metadata: { 58 | _rewardsId, 59 | _amount, 60 | }, 61 | }) 62 | ); 63 | } 64 | }); 65 | 66 | return findings; 67 | }; 68 | 69 | export { handleTransaction }; 70 | -------------------------------------------------------------------------------- /agents/suspicious-amount-agent/src/suspicious-withdraw.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ethers, 3 | Finding, 4 | FindingSeverity, 5 | FindingType, 6 | HandleTransaction, 7 | TransactionEvent, 8 | } from "forta-agent"; 9 | 10 | import { 11 | BNBX_UNSTAKE_THRESHOLD, 12 | protocol, 13 | REQUEST_WITHDRAW_EVENT, 14 | STAKE_MANAGER, 15 | } from "./constants"; 16 | 17 | const handleTransaction: HandleTransaction = async ( 18 | txEvent: TransactionEvent 19 | ) => { 20 | const findings: Finding[] = []; 21 | 22 | // filter the transaction logs for BNBx unstake events 23 | const bnbxUnstakeEvents = txEvent.filterLog( 24 | REQUEST_WITHDRAW_EVENT, 25 | STAKE_MANAGER 26 | ); 27 | 28 | bnbxUnstakeEvents.forEach((unstakeEvents) => { 29 | const { _account, _amountInBnbX } = unstakeEvents.args; 30 | 31 | const normalizedValue = ethers.utils.formatEther(_amountInBnbX); 32 | const minThreshold = ethers.utils.parseEther(BNBX_UNSTAKE_THRESHOLD); 33 | 34 | if (_amountInBnbX.gt(minThreshold)) { 35 | findings.push( 36 | Finding.fromObject({ 37 | name: "Large BNBx Unstake", 38 | description: `Large amount of BNBx unstaked: ${normalizedValue}`, 39 | alertId: "BNBx-LARGE-UNSTAKE", 40 | protocol: protocol, 41 | severity: FindingSeverity.High, 42 | type: FindingType.Info, 43 | metadata: { 44 | account: _account, 45 | amountInBNBx: _amountInBnbX.toString(), 46 | }, 47 | }) 48 | ); 49 | } 50 | }); 51 | 52 | return findings; 53 | }; 54 | 55 | export { handleTransaction }; 56 | -------------------------------------------------------------------------------- /agents/suspicious-amount-agent/src/utils.ts: -------------------------------------------------------------------------------- 1 | const getHours = (miliSecs: number) => { 2 | return miliSecs / (1000 * 60 * 60); 3 | }; 4 | 5 | export { getHours }; 6 | -------------------------------------------------------------------------------- /agents/suspicious-amount-agent/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | "resolveJsonModule": true, 5 | 6 | /* Basic Options */ 7 | // "incremental": true, /* Enable incremental compilation */ 8 | "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */, 9 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 10 | "lib": [ 11 | "es2019" 12 | ] /* Specify library files to be included in the compilation. */, 13 | // "allowJs": true, /* Allow javascript files to be compiled. */ 14 | // "checkJs": true, /* Report errors in .js files. */ 15 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ 16 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 17 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 18 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 19 | // "outFile": "./", /* Concatenate and emit output to single file. */ 20 | "outDir": "dist" /* Redirect output structure to the directory. */, 21 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 22 | // "composite": true, /* Enable project compilation */ 23 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 24 | // "removeComments": true, /* Do not emit comments to output. */ 25 | // "noEmit": true, /* Do not emit outputs. */ 26 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 27 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 28 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 29 | 30 | /* Strict Type-Checking Options */ 31 | "strict": true /* Enable all strict type-checking options. */, 32 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 33 | // "strictNullChecks": true, /* Enable strict null checks. */ 34 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 35 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 36 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 37 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 38 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 39 | 40 | /* Additional Checks */ 41 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 42 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 43 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 44 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 45 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 46 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */ 47 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ 48 | 49 | /* Module Resolution Options */ 50 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 51 | "baseUrl": "." /* Base directory to resolve non-absolute module names. */, 52 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 53 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 54 | // "typeRoots": [], /* List of folders to include type definitions from. */ 55 | // "types": [], /* Type declaration files to be included in compilation. */ 56 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 57 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 58 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 59 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 60 | 61 | /* Source Map Options */ 62 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 63 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 64 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 65 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 66 | 67 | /* Experimental Options */ 68 | "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, 69 | "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */, 70 | 71 | /* Advanced Options */ 72 | "skipLibCheck": true /* Skip type checking of declaration files. */, 73 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /certora/run.sh: -------------------------------------------------------------------------------- 1 | certoraRun contracts/StakeManager.sol \ 2 | contracts/BnbX.sol \ 3 | --link StakeManager:bnbX=BnbX \ 4 | --verify StakeManager:certora/specs/StakeManager.spec \ 5 | --packages @openzeppelin=node_modules/@openzeppelin \ 6 | --path . \ 7 | --loop_iter 3 \ 8 | --settings -optimisticFallback=true --optimistic_loop \ 9 | --staging \ 10 | --msg "bnbx" 11 | -------------------------------------------------------------------------------- /certora/specs/StakeManager.spec: -------------------------------------------------------------------------------- 1 | using BnbX as BnbX 2 | 3 | methods{ 4 | 5 | convertBnbToBnbX(uint256) returns (uint256) envfree; 6 | convertBnbXToBnb(uint256) returns (uint256) envfree; 7 | hasRole(bytes32, address) returns (bool) envfree; 8 | 9 | //variables 10 | BOT() returns (bytes32) envfree; 11 | 12 | // getters 13 | totalBnbXToBurn() returns (uint256) envfree; 14 | totalClaimableBnb() returns (uint256) envfree; 15 | getBnbXWithdrawLimit() returns (uint256) envfree; 16 | getTotalPooledBnb() returns (uint256) envfree; 17 | getUserRequestStatus(address, uint256) returns (bool, uint256) envfree; 18 | getContracts() returns ( 19 | address _manager, 20 | address _bnbX, 21 | address _tokenHub, 22 | address _bcDepositWallet 23 | ) envfree; 24 | 25 | // BnbX.sol 26 | BnbX.totalSupply() returns (uint256) envfree; 27 | BnbX.balanceOf(address) returns (uint256) envfree; 28 | 29 | 30 | // ERC20Upgradable summarization 31 | transfer(address to, uint256 amount) returns (bool) => DISPATCHER(true); 32 | } 33 | 34 | rule userDepositsAndGetsCorrectAmountOfBnbX(address user, uint256 amount) { 35 | env e; 36 | require e.msg.sender == user; 37 | require e.msg.value == amount; 38 | 39 | uint256 bnbXAmount = convertBnbToBnbX(amount); 40 | uint256 userBnbXBalanceBefore = BnbX.balanceOf(user); 41 | 42 | deposit(e); 43 | 44 | uint256 userBnbXBalanceAfter = BnbX.balanceOf(user); 45 | 46 | assert userBnbXBalanceAfter == userBnbXBalanceBefore + bnbXAmount; 47 | } 48 | 49 | rule depositIncreasesTotalPooledBnb() { 50 | env e; 51 | 52 | uint256 pooledBnbBefore = getTotalPooledBnb(); 53 | 54 | deposit(e); 55 | 56 | uint256 pooledBnbAfter = getTotalPooledBnb(); 57 | 58 | assert pooledBnbAfter == pooledBnbBefore + e.msg.value; 59 | } 60 | 61 | rule totalSupplyIsCorrectAfterDeposit(address user, uint256 amount){ 62 | env e; 63 | 64 | require e.msg.sender == user; 65 | require e.msg.value == amount; 66 | 67 | uint256 totalSupplyBefore = BnbX.totalSupply(); 68 | 69 | require totalSupplyBefore + amount <= max_uint256; 70 | 71 | uint256 bnbXAmount = convertBnbToBnbX(amount); 72 | deposit(e); 73 | 74 | uint256 totalSupplyAfter = BnbX.totalSupply(); 75 | 76 | assert amount != 0 => totalSupplyBefore + bnbXAmount == totalSupplyAfter; 77 | } 78 | 79 | 80 | rule totalSupplyDoesNotChangeAfterRequestWithdraw(uint256 unstakeBnbXAmount){ 81 | env e; 82 | 83 | uint256 totalSupplyBefore = BnbX.totalSupply(); 84 | 85 | requestWithdraw(e, unstakeBnbXAmount); 86 | 87 | uint256 totalSupplyAfter = BnbX.totalSupply(); 88 | 89 | assert totalSupplyBefore == totalSupplyAfter; 90 | } 91 | 92 | rule totalSupplyDoesNotChangeAfterClaimWithdraw(uint256 idx){ 93 | env e; 94 | 95 | uint256 totalSupplyBefore = BnbX.totalSupply(); 96 | 97 | claimWithdraw(e, idx); 98 | 99 | uint256 totalSupplyAfter = BnbX.totalSupply(); 100 | 101 | assert totalSupplyBefore == totalSupplyAfter; 102 | } 103 | 104 | rule erDoesNotChangeOnTransfer() { 105 | env e; 106 | uint256 oneEther = 10^18; 107 | uint256 erBefore = convertBnbXToBnb(oneEther); 108 | 109 | address otherUser; 110 | uint256 amount; 111 | 112 | BnbX.transfer(e, otherUser, amount); 113 | 114 | uint256 erAfter = convertBnbXToBnb(oneEther); 115 | 116 | assert erBefore == erAfter; 117 | 118 | } 119 | 120 | // generic function `f` invoked with its specific `args` 121 | rule userDoesNotChangeOtherUserBalance(method f, address otherUser){ 122 | env e; 123 | calldataarg args; 124 | 125 | address manager; 126 | address _; 127 | manager, _, _, _ = getContracts(); 128 | bytes32 BOT_ROLE = BOT(); 129 | 130 | require !hasRole( BOT_ROLE, e.msg.sender); 131 | require e.msg.sender != manager; 132 | 133 | 134 | uint256 otherUserBnbXBalanceBefore = BnbX.balanceOf(otherUser); 135 | f(e,args); 136 | uint256 otherUserBnbXBalanceAfter = BnbX.balanceOf(otherUser); 137 | assert ((otherUser != e.msg.sender) => otherUserBnbXBalanceBefore == otherUserBnbXBalanceAfter); 138 | } 139 | 140 | rule bankRunSituation(){ 141 | env e1; 142 | env e2; 143 | env e3; 144 | 145 | uint256 bnbxAmt1 = convertBnbToBnbX(e1.msg.value); 146 | deposit(e1); 147 | 148 | uint256 bnbxAmt2 = convertBnbToBnbX(e2.msg.value); 149 | deposit(e2); 150 | 151 | uint256 bnbxAmt3 = convertBnbToBnbX(e3.msg.value); 152 | deposit(e3); 153 | 154 | // All user unstakes 155 | // user1 unstakes 156 | require (BnbX.balanceOf(e1.msg.sender) == bnbxAmt1); 157 | requestWithdraw(e1, bnbxAmt1); 158 | 159 | bool isClaimable1; 160 | uint256 _amount1; 161 | isClaimable1, _amount1 = getUserRequestStatus(e1.msg.sender, 0); 162 | require isClaimable1 == true; 163 | 164 | claimWithdraw(e1, 0); 165 | 166 | // user2 unstakes 167 | require (BnbX.balanceOf(e2.msg.sender) == bnbxAmt2); 168 | requestWithdraw(e2, bnbxAmt2); 169 | 170 | bool isClaimable2; 171 | uint256 _amount2; 172 | isClaimable2, _amount2 = getUserRequestStatus(e2.msg.sender, 0); 173 | require isClaimable2 == true; 174 | 175 | claimWithdraw(e2, 0); 176 | 177 | // user3 unstakes 178 | require (BnbX.balanceOf(e3.msg.sender) == bnbxAmt3); 179 | requestWithdraw(e3, bnbxAmt3); 180 | 181 | bool isClaimable3; 182 | uint256 _amount3; 183 | isClaimable3, _amount3 = getUserRequestStatus(e3.msg.sender, 0); 184 | require isClaimable3 == true; 185 | 186 | claimWithdraw(e3, 0); 187 | 188 | assert (getTotalPooledBnb()==0 && totalClaimableBnb()==0) => (totalBnbXToBurn()==0 && getBnbXWithdrawLimit() == 0); 189 | assert(BnbX.balanceOf(e1.msg.sender) == 0); 190 | assert(BnbX.balanceOf(e2.msg.sender) == 0); 191 | assert(BnbX.balanceOf(e3.msg.sender) == 0); 192 | } 193 | -------------------------------------------------------------------------------- /contracts/BnbX.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; 5 | import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; 6 | 7 | import "./interfaces/IBnbX.sol"; 8 | 9 | contract BnbX is IBnbX, ERC20Upgradeable, AccessControlUpgradeable { 10 | address private stakeManager; 11 | 12 | /// @custom:oz-upgrades-unsafe-allow constructor 13 | constructor() { 14 | _disableInitializers(); 15 | } 16 | 17 | function initialize(address _admin) external override initializer { 18 | __AccessControl_init(); 19 | __ERC20_init("Liquid Staking BNB", "BNBx"); 20 | 21 | require(_admin != address(0), "zero address provided"); 22 | 23 | _setupRole(DEFAULT_ADMIN_ROLE, _admin); 24 | } 25 | 26 | function mint(address _account, uint256 _amount) external override onlyStakeManager { 27 | _mint(_account, _amount); 28 | } 29 | 30 | function burn(address _account, uint256 _amount) external override onlyStakeManager { 31 | _burn(_account, _amount); 32 | } 33 | 34 | function setStakeManager(address _address) external override onlyRole(DEFAULT_ADMIN_ROLE) { 35 | require(stakeManager != _address, "Old address == new address"); 36 | require(_address != address(0), "zero address provided"); 37 | 38 | stakeManager = _address; 39 | 40 | emit SetStakeManager(_address); 41 | } 42 | 43 | modifier onlyStakeManager() { 44 | require(msg.sender == stakeManager, "Accessible only by StakeManager Contract"); 45 | _; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /contracts/OperatorRegistry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.25; 3 | 4 | import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; 5 | import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; 6 | import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; 7 | import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; 8 | 9 | import "./interfaces/IOperatorRegistry.sol"; 10 | import "./interfaces/IStakeHub.sol"; 11 | import "./interfaces/IStakeCredit.sol"; 12 | import "./ProtocolConstants.sol"; 13 | 14 | /// @title OperatorRegistry 15 | /// @notice OperatorRegistry is the main contract that manages operators. 16 | contract OperatorRegistry is 17 | IOperatorRegistry, 18 | PausableUpgradeable, 19 | AccessControlUpgradeable, 20 | ReentrancyGuardUpgradeable 21 | { 22 | using EnumerableSet for EnumerableSet.AddressSet; 23 | 24 | bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); 25 | bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE"); 26 | 27 | IStakeHub public constant STAKE_HUB = IStakeHub(ProtocolConstants.STAKE_HUB_ADDR); 28 | address public override preferredDepositOperator; 29 | address public override preferredWithdrawalOperator; 30 | uint256 public negligibleAmount; 31 | 32 | address public stakeManager; 33 | EnumerableSet.AddressSet private operatorSet; 34 | 35 | // @custom:oz-upgrades-unsafe-allow constructor 36 | constructor() { 37 | _disableInitializers(); 38 | } 39 | 40 | /// @notice Initialize the OperatorRegistry contract. 41 | function initialize(address _admin) external initializer { 42 | if (_admin == address(0)) revert ZeroAddress(); 43 | 44 | __AccessControl_init(); 45 | __Pausable_init(); 46 | __ReentrancyGuard_init(); 47 | 48 | negligibleAmount = 1e10; 49 | 50 | _setupRole(DEFAULT_ADMIN_ROLE, _admin); 51 | } 52 | 53 | /// @notice initialize stakeManager 54 | function initialize2(address _stakeManager) external reinitializer(2) { 55 | if (_stakeManager == address(0)) revert ZeroAddress(); 56 | stakeManager = _stakeManager; 57 | } 58 | 59 | /// @notice Allows an operator that was already staked on the BNB stake manager 60 | /// to join the BNBX protocol. 61 | /// @param _operator Address of the operator. 62 | function addOperator(address _operator) 63 | external 64 | override 65 | nonReentrant 66 | whenOperatorDoesNotExist(_operator) 67 | onlyRole(MANAGER_ROLE) 68 | { 69 | if (_operator == address(0)) revert ZeroAddress(); 70 | 71 | (uint256 createdTime, bool jailed,) = STAKE_HUB.getValidatorBasicInfo(_operator); 72 | if (createdTime == 0) revert OperatorNotExisted(); 73 | if (jailed) revert OperatorJailed(); 74 | 75 | operatorSet.add(_operator); 76 | 77 | emit AddedOperator(_operator); 78 | } 79 | 80 | /// @notice Allows to remove an operator from the registry. 81 | /// @param _operator Address of the operator. 82 | function removeOperator(address _operator) 83 | external 84 | override 85 | nonReentrant 86 | whenOperatorDoesExist(_operator) 87 | onlyRole(MANAGER_ROLE) 88 | { 89 | if (preferredDepositOperator == _operator) { 90 | revert OperatorIsPreferredDeposit(); 91 | } 92 | if (preferredWithdrawalOperator == _operator) { 93 | revert OperatorIsPreferredWithdrawal(); 94 | } 95 | 96 | if (IStakeCredit(STAKE_HUB.getValidatorCreditContract(_operator)).getPooledBNB(stakeManager) > negligibleAmount) 97 | { 98 | revert DelegationExists(); 99 | } 100 | 101 | operatorSet.remove(_operator); 102 | 103 | emit RemovedOperator(_operator); 104 | } 105 | 106 | /// @notice Allows to set the preferred operator for deposits. 107 | /// @param _operator Address of the operator. 108 | function setPreferredDepositOperator(address _operator) 109 | external 110 | override 111 | nonReentrant 112 | whenNotPaused 113 | whenOperatorDoesExist(_operator) 114 | onlyRole(OPERATOR_ROLE) 115 | { 116 | preferredDepositOperator = _operator; 117 | 118 | emit SetPreferredDepositOperator(preferredDepositOperator); 119 | } 120 | 121 | /// @notice Allows to set the preferred operator for withdrawals. 122 | /// @param _operator Address of the operator. 123 | function setPreferredWithdrawalOperator(address _operator) 124 | external 125 | override 126 | nonReentrant 127 | whenNotPaused 128 | whenOperatorDoesExist(_operator) 129 | onlyRole(OPERATOR_ROLE) 130 | { 131 | preferredWithdrawalOperator = _operator; 132 | 133 | emit SetPreferredWithdrawalOperator(preferredWithdrawalOperator); 134 | } 135 | 136 | /// @notice Allows to set the negligible amount. 137 | /// @param _negligibleAmount The negligible amount. 138 | function setNegligibleAmount(uint256 _negligibleAmount) external nonReentrant onlyRole(MANAGER_ROLE) { 139 | if (_negligibleAmount > ProtocolConstants.MAX_NEGLIGIBLE_AMOUNT) revert NegligibleAmountTooHigh(); 140 | negligibleAmount = _negligibleAmount; 141 | } 142 | 143 | /** 144 | * @dev Triggers stopped state. 145 | * Contract must not be paused 146 | */ 147 | function pause() external override onlyRole(MANAGER_ROLE) { 148 | _pause(); 149 | } 150 | 151 | /** 152 | * @dev Returns to normal state. 153 | * Contract must be paused 154 | */ 155 | function unpause() external override onlyRole(DEFAULT_ADMIN_ROLE) { 156 | _unpause(); 157 | } 158 | 159 | /// -------------------------------Getters----------------------------------- 160 | 161 | /// @notice Get operator address by its index. 162 | /// @param _index Operator index. 163 | /// @return _operator The operator address. 164 | function getOperatorAt(uint256 _index) external view override returns (address) { 165 | return operatorSet.at(_index); 166 | } 167 | 168 | /// @notice Get the total number of operators. 169 | /// @return The number of operators. 170 | function getOperatorsLength() external view override returns (uint256) { 171 | return operatorSet.length(); 172 | } 173 | 174 | /// @notice Check if an operator exists in the registry. 175 | /// @param _operator Address of the operator. 176 | /// @return True if the operator exists, false otherwise. 177 | function operatorExists(address _operator) external view override returns (bool) { 178 | return operatorSet.contains(_operator); 179 | } 180 | 181 | /// @notice Return the entire set in an array 182 | function getOperators() external view override returns (address[] memory) { 183 | return operatorSet.values(); 184 | } 185 | 186 | /// -------------------------------Modifiers----------------------------------- 187 | 188 | /** 189 | * @dev Modifier to make a function callable only when the operator exists in the registry. 190 | * @param _operator The operator address. 191 | * Requirements: 192 | * 193 | * - The operator must exist in the registry. 194 | */ 195 | modifier whenOperatorDoesExist(address _operator) { 196 | if (!operatorSet.contains(_operator)) revert OperatorNotExisted(); 197 | _; 198 | } 199 | 200 | /** 201 | * @dev Modifier to make a function callable only when the operator doesn't exist in the registry. 202 | * @param _operator The operator address. 203 | * 204 | * Requirements: 205 | * 206 | * - The operator must not exist in the registry. 207 | */ 208 | modifier whenOperatorDoesNotExist(address _operator) { 209 | if (operatorSet.contains(_operator)) revert OperatorExisted(); 210 | _; 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /contracts/ProtocolConstants.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.25; 3 | 4 | library ProtocolConstants { 5 | // addresses 6 | address public constant STAKE_HUB_ADDR = 0x0000000000000000000000000000000000002002; 7 | 8 | // values 9 | uint256 public constant MAX_NEGLIGIBLE_AMOUNT = 1e15; 10 | uint256 public constant MAX_ALLOWED_FEE_BPS = 5000; 11 | uint256 public constant BPS_DENOM = 10_000; 12 | } 13 | -------------------------------------------------------------------------------- /contracts/StakeManager.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.25; 3 | 4 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 5 | import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; 6 | import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; 7 | import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; 8 | import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; 9 | import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; 10 | 11 | import { IStakeManager } from "./interfaces/IStakeManager.sol"; 12 | import { IBnbX } from "./interfaces/IBnbX.sol"; 13 | import { ITokenHub } from "./interfaces/ITokenHub.sol"; 14 | 15 | /** 16 | * @title Stake Manager Contract 17 | * @dev Handles Staking of BNB on BSC 18 | */ 19 | contract StakeManager is IStakeManager, Initializable, PausableUpgradeable, AccessControlUpgradeable { 20 | using SafeERC20Upgradeable for IERC20Upgradeable; 21 | 22 | uint256 public depositsDelegated; // total BNB delegated to validators on Beacon Chain 23 | uint256 public depositsInContract; // total BNB deposited in contract but not yet transferred to relayer for moving to BC. 24 | uint256 public depositsBridgingOut; // total BNB in relayer while transfering BSC -> BC 25 | uint256 public totalBnbXToBurn; 26 | uint256 public totalClaimableBnb; // total BNB available to be claimed and resides in contract 27 | 28 | uint256 public nextDelegateUUID; 29 | uint256 public nextUndelegateUUID; 30 | uint256 public minDelegateThreshold; 31 | uint256 public minUndelegateThreshold; 32 | 33 | address private bnbX; 34 | address private bcDepositWallet; 35 | address private tokenHub; 36 | 37 | bool private isDelegationPending; // initial default value false 38 | 39 | mapping(uint256 => BotDelegateRequest) private uuidToBotDelegateRequestMap; 40 | mapping(uint256 => BotUndelegateRequest) private uuidToBotUndelegateRequestMap; 41 | mapping(address => WithdrawalRequest[]) private userWithdrawalRequests; 42 | 43 | uint256 public constant TEN_DECIMALS = 1e10; 44 | bytes32 public constant BOT = keccak256("BOT"); 45 | 46 | address private manager; 47 | address private proposedManager; 48 | uint256 public feeBps; // range {0-10_000} 49 | mapping(uint256 => bool) public rewardsIdUsed; 50 | 51 | address public redirectAddress; 52 | 53 | /// @custom:oz-upgrades-unsafe-allow constructor 54 | constructor() { 55 | _disableInitializers(); 56 | } 57 | 58 | /** 59 | * @param _bnbX - Address of BnbX Token on Binance Smart Chain 60 | * @param _admin - Address of the admin 61 | * @param _manager - Address of the manager 62 | * @param _tokenHub - Address of the manager 63 | * @param _bcDepositWallet - Beck32 decoding of Address of deposit Bot Wallet on Beacon Chain with `0x` prefix 64 | * @param _bot - Address of the Bot 65 | * @param _feeBps - Fee Basis Points 66 | */ 67 | function initialize( 68 | address _bnbX, 69 | address _admin, 70 | address _manager, 71 | address _tokenHub, 72 | address _bcDepositWallet, 73 | address _bot, 74 | uint256 _feeBps 75 | ) 76 | external 77 | override 78 | initializer 79 | { 80 | __AccessControl_init(); 81 | __Pausable_init(); 82 | 83 | require( 84 | ( 85 | (_bnbX != address(0)) && (_admin != address(0)) && (_manager != address(0)) && (_tokenHub != address(0)) 86 | && (_bcDepositWallet != address(0)) && (_bot != address(0)) 87 | ), 88 | "zero address provided" 89 | ); 90 | require(_feeBps <= 10_000, "_feeBps must not exceed 10000 (100%)"); 91 | 92 | _setRoleAdmin(BOT, DEFAULT_ADMIN_ROLE); 93 | _setupRole(DEFAULT_ADMIN_ROLE, _admin); 94 | _setupRole(BOT, _bot); 95 | 96 | manager = _manager; 97 | bnbX = _bnbX; 98 | tokenHub = _tokenHub; 99 | bcDepositWallet = _bcDepositWallet; 100 | minDelegateThreshold = 1e18; 101 | minUndelegateThreshold = 1e18; 102 | feeBps = _feeBps; 103 | 104 | emit SetManager(_manager); 105 | emit SetBotRole(_bot); 106 | emit SetBCDepositWallet(bcDepositWallet); 107 | emit SetMinDelegateThreshold(minDelegateThreshold); 108 | emit SetMinUndelegateThreshold(minUndelegateThreshold); 109 | emit SetFeeBps(_feeBps); 110 | } 111 | 112 | //////////////////////////////////////////////////////////// 113 | ///// /// 114 | ///// ***Deposit Flow*** /// 115 | ///// /// 116 | //////////////////////////////////////////////////////////// 117 | 118 | /** 119 | * @dev Allows user to deposit Bnb at BSC and mints BnbX for the user 120 | */ 121 | function deposit() external payable override whenNotPaused { 122 | uint256 amount = msg.value; 123 | require(amount > 0, "Invalid Amount"); 124 | 125 | uint256 bnbXToMint = convertBnbToBnbX(amount); 126 | 127 | depositsInContract += amount; 128 | 129 | IBnbX(bnbX).mint(msg.sender, bnbXToMint); 130 | } 131 | 132 | /** 133 | * @dev Allows bot to transfer users' funds from this contract to botDepositWallet at Beacon Chain 134 | * @return _uuid - unique id against which this transfer event was logged 135 | * @return _amount - Amount of funds transferred for staking 136 | * @notice Use `getBotDelegateRequest` function to get more details of the logged data 137 | */ 138 | function startDelegation() 139 | external 140 | payable 141 | override 142 | whenNotPaused 143 | onlyRole(BOT) 144 | returns (uint256 _uuid, uint256 _amount) 145 | { 146 | require(!isDelegationPending, "Previous Delegation Pending"); 147 | 148 | uint256 tokenHubRelayFee = getTokenHubRelayFee(); 149 | uint256 relayFeeReceived = msg.value; 150 | _amount = depositsInContract - (depositsInContract % TEN_DECIMALS); 151 | 152 | require(relayFeeReceived >= tokenHubRelayFee, "Insufficient RelayFee"); 153 | require(_amount >= minDelegateThreshold, "Insufficient Deposit Amount"); 154 | 155 | _uuid = nextDelegateUUID++; // post-increment : assigns the current value first and then increments 156 | uuidToBotDelegateRequestMap[_uuid] = 157 | BotDelegateRequest({ startTime: block.timestamp, endTime: 0, amount: _amount }); 158 | depositsBridgingOut += _amount; 159 | depositsInContract -= _amount; 160 | 161 | isDelegationPending = true; 162 | 163 | // sends funds to BC 164 | _tokenHubTransferOut(_amount, relayFeeReceived); 165 | } 166 | 167 | // function retryTransferOut(uint256 _uuid) external payable override whenNotPaused onlyManager { 168 | // uint256 tokenHubRelayFee = getTokenHubRelayFee(); 169 | // uint256 relayFeeReceived = msg.value; 170 | // require(relayFeeReceived >= tokenHubRelayFee, "Insufficient RelayFee"); 171 | 172 | // BotDelegateRequest storage botDelegateRequest = uuidToBotDelegateRequestMap[_uuid]; 173 | 174 | // require( 175 | // isDelegationPending && (botDelegateRequest.startTime != 0) && (botDelegateRequest.endTime == 0), 176 | // "Invalid UUID" 177 | // ); 178 | 179 | // uint256 extraBNB = getExtraBnbInContract(); 180 | // require( 181 | // (botDelegateRequest.amount == depositsBridgingOut) && (depositsBridgingOut <= extraBNB), 182 | // "Invalid BridgingOut Amount" 183 | // ); 184 | // _tokenHubTransferOut(depositsBridgingOut, relayFeeReceived); 185 | // } 186 | 187 | /** 188 | * @dev Allows bot to mark the delegateRequest as complete and update the state variables 189 | * @param _uuid - unique id for which the delgation was completed 190 | * @notice Use `getBotDelegateRequest` function to get more details of the logged data 191 | */ 192 | function completeDelegation(uint256 _uuid) external override whenNotPaused onlyRole(BOT) { 193 | require( 194 | (uuidToBotDelegateRequestMap[_uuid].amount > 0) && (uuidToBotDelegateRequestMap[_uuid].endTime == 0), 195 | "Invalid UUID" 196 | ); 197 | 198 | uuidToBotDelegateRequestMap[_uuid].endTime = block.timestamp; 199 | uint256 amount = uuidToBotDelegateRequestMap[_uuid].amount; 200 | depositsBridgingOut -= amount; 201 | depositsDelegated += amount; 202 | 203 | isDelegationPending = false; 204 | emit Delegate(_uuid, amount); 205 | } 206 | 207 | /** 208 | * @dev Allows bot to update the contract regarding the rewards 209 | * @param _amount - Amount of reward 210 | */ 211 | function addRestakingRewards(uint256 _id, uint256 _amount) external override whenNotPaused onlyRole(BOT) { 212 | require(_amount > 0, "No reward"); 213 | require(depositsDelegated > 0, "No funds delegated"); 214 | require(!rewardsIdUsed[_id], "Rewards ID already Used"); 215 | 216 | depositsDelegated += _amount; 217 | rewardsIdUsed[_id] = true; 218 | 219 | emit Redelegate(_id, _amount); 220 | } 221 | 222 | //////////////////////////////////////////////////////////// 223 | ///// /// 224 | ///// ***Withdraw Flow*** /// 225 | ///// /// 226 | //////////////////////////////////////////////////////////// 227 | 228 | /** 229 | * @dev Allows user to request for unstake/withdraw funds 230 | * @param _amountInBnbX - Amount of BnbX to swap for withdraw 231 | * @notice User must have approved this contract to spend BnbX 232 | */ 233 | function requestWithdraw(uint256 _amountInBnbX) external override whenNotPaused { 234 | require(_amountInBnbX > 0, "Invalid Amount"); 235 | 236 | totalBnbXToBurn += _amountInBnbX; 237 | uint256 totalBnbToWithdraw = convertBnbXToBnb(totalBnbXToBurn); 238 | require(totalBnbToWithdraw <= depositsDelegated, "Not enough BNB to withdraw"); 239 | 240 | userWithdrawalRequests[msg.sender].push( 241 | WithdrawalRequest({ uuid: nextUndelegateUUID, amountInBnbX: _amountInBnbX, startTime: block.timestamp }) 242 | ); 243 | 244 | IERC20Upgradeable(bnbX).safeTransferFrom(msg.sender, address(this), _amountInBnbX); 245 | emit RequestWithdraw(msg.sender, _amountInBnbX); 246 | } 247 | 248 | function claimWithdraw(uint256 _idx) external override { 249 | address user = msg.sender; 250 | WithdrawalRequest[] storage userRequests = userWithdrawalRequests[user]; 251 | 252 | require(_idx < userRequests.length, "Invalid index"); 253 | 254 | WithdrawalRequest storage withdrawRequest = userRequests[_idx]; 255 | uint256 uuid = withdrawRequest.uuid; 256 | uint256 amountInBnbX = withdrawRequest.amountInBnbX; 257 | 258 | BotUndelegateRequest storage botUndelegateRequest = uuidToBotUndelegateRequestMap[uuid]; 259 | require(botUndelegateRequest.endTime != 0, "Not able to claim yet"); 260 | userRequests[_idx] = userRequests[userRequests.length - 1]; 261 | userRequests.pop(); 262 | 263 | uint256 totalBnbToWithdraw_ = botUndelegateRequest.amount; 264 | uint256 totalBnbXToBurn_ = botUndelegateRequest.amountInBnbX; 265 | uint256 amount = (totalBnbToWithdraw_ * amountInBnbX) / totalBnbXToBurn_; 266 | 267 | totalClaimableBnb -= amount; 268 | AddressUpgradeable.sendValue(payable(user), amount); 269 | 270 | emit ClaimWithdrawal(user, _idx, amount); 271 | } 272 | 273 | /** 274 | * @dev Bot uses this function to get amount of BNB to withdraw 275 | * @return _uuid - unique id against which this Undelegation event was logged 276 | * @return _amount - Amount of funds required to Unstake 277 | * @notice Use `getBotUndelegateRequest` function to get more details of the logged data 278 | */ 279 | function startUndelegation() 280 | external 281 | override 282 | whenNotPaused 283 | onlyRole(BOT) 284 | returns (uint256 _uuid, uint256 _amount) 285 | { 286 | _uuid = nextUndelegateUUID++; // post-increment : assigns the current value first and then increments 287 | uint256 totalBnbXToBurn_ = totalBnbXToBurn; // To avoid Reentrancy attack 288 | _amount = convertBnbXToBnb(totalBnbXToBurn_); 289 | _amount -= _amount % TEN_DECIMALS; 290 | 291 | require(_amount >= minUndelegateThreshold, "Insufficient Withdraw Amount"); 292 | 293 | uuidToBotUndelegateRequestMap[_uuid] = 294 | BotUndelegateRequest({ startTime: 0, endTime: 0, amount: _amount, amountInBnbX: totalBnbXToBurn_ }); 295 | 296 | depositsDelegated -= _amount; 297 | totalBnbXToBurn = 0; 298 | 299 | IBnbX(bnbX).burn(address(this), totalBnbXToBurn_); 300 | } 301 | 302 | /** 303 | * @dev Allows Bot to communicate regarding start of Undelegation Event at Beacon Chain 304 | * @param _uuid - unique id against which this Undelegation event was logged 305 | */ 306 | function undelegationStarted(uint256 _uuid) external override whenNotPaused onlyRole(BOT) { 307 | BotUndelegateRequest storage botUndelegateRequest = uuidToBotUndelegateRequestMap[_uuid]; 308 | require((botUndelegateRequest.amount > 0) && (botUndelegateRequest.startTime == 0), "Invalid UUID"); 309 | 310 | botUndelegateRequest.startTime = block.timestamp; 311 | } 312 | 313 | /** 314 | * @dev Bot uses this function to send unstaked funds to this contract and 315 | * communicate regarding completion of Undelegation Event 316 | * @param _uuid - unique id against which this Undelegation event was logged 317 | * @notice Use `getBotUndelegateRequest` function to get more details of the logged data 318 | * @notice send exact amount of BNB 319 | */ 320 | function completeUndelegation(uint256 _uuid) external payable override whenNotPaused onlyRole(BOT) { 321 | BotUndelegateRequest storage botUndelegateRequest = uuidToBotUndelegateRequestMap[_uuid]; 322 | require((botUndelegateRequest.startTime != 0) && (botUndelegateRequest.endTime == 0), "Invalid UUID"); 323 | 324 | uint256 amount = msg.value; 325 | require(amount == botUndelegateRequest.amount, "Send Exact Amount of Fund"); 326 | botUndelegateRequest.endTime = block.timestamp; 327 | totalClaimableBnb += botUndelegateRequest.amount; 328 | 329 | emit Undelegate(_uuid, amount); 330 | } 331 | 332 | /** 333 | * @notice extract funds to migrate 334 | * @dev migrates to manager 335 | */ 336 | function migrateFunds() external whenPaused onlyRole(DEFAULT_ADMIN_ROLE) { 337 | (bool success,) = payable(manager).call{ value: depositsInContract }(""); 338 | require(success, "Transfer Failed"); 339 | } 340 | 341 | //////////////////////////////////////////////////////////// 342 | ///// /// 343 | ///// ***Setters*** /// 344 | ///// /// 345 | //////////////////////////////////////////////////////////// 346 | 347 | function proposeNewManager(address _address) external override onlyManager { 348 | require(manager != _address, "Old address == new address"); 349 | require(_address != address(0), "zero address provided"); 350 | 351 | proposedManager = _address; 352 | 353 | emit ProposeManager(_address); 354 | } 355 | 356 | function acceptNewManager() external override { 357 | require(msg.sender == proposedManager, "Accessible only by Proposed Manager"); 358 | 359 | manager = proposedManager; 360 | proposedManager = address(0); 361 | 362 | emit SetManager(manager); 363 | } 364 | 365 | function setBotRole(address _address) external override onlyManager { 366 | require(_address != address(0), "zero address provided"); 367 | 368 | _setupRole(BOT, _address); 369 | 370 | emit SetBotRole(_address); 371 | } 372 | 373 | function revokeBotRole(address _address) external override onlyManager { 374 | require(_address != address(0), "zero address provided"); 375 | 376 | _revokeRole(BOT, _address); 377 | 378 | emit RevokeBotRole(_address); 379 | } 380 | 381 | /// @param _address - Beck32 decoding of Address of deposit Bot Wallet on Beacon Chain with `0x` prefix 382 | function setBCDepositWallet(address _address) external override onlyManager { 383 | require(bcDepositWallet != _address, "Old address == new address"); 384 | require(_address != address(0), "zero address provided"); 385 | 386 | bcDepositWallet = _address; 387 | 388 | emit SetBCDepositWallet(_address); 389 | } 390 | 391 | function setMinDelegateThreshold(uint256 _minDelegateThreshold) external override onlyManager { 392 | require(_minDelegateThreshold > 0, "Invalid Threshold"); 393 | minDelegateThreshold = _minDelegateThreshold; 394 | 395 | emit SetMinDelegateThreshold(_minDelegateThreshold); 396 | } 397 | 398 | function setMinUndelegateThreshold(uint256 _minUndelegateThreshold) external override onlyManager { 399 | require(_minUndelegateThreshold > 0, "Invalid Threshold"); 400 | minUndelegateThreshold = _minUndelegateThreshold; 401 | 402 | emit SetMinUndelegateThreshold(_minUndelegateThreshold); 403 | } 404 | 405 | function setFeeBps(uint256 _feeBps) external override onlyRole(DEFAULT_ADMIN_ROLE) { 406 | require(_feeBps <= 10_000, "_feeBps must not exceed 10000 (100%)"); 407 | 408 | feeBps = _feeBps; 409 | 410 | emit SetFeeBps(_feeBps); 411 | } 412 | 413 | function setRedirectAddress(address _address) external override onlyRole(DEFAULT_ADMIN_ROLE) { 414 | require(redirectAddress != _address, "Old address == new address"); 415 | require(_address != address(0), "zero address provided"); 416 | 417 | redirectAddress = _address; 418 | 419 | emit SetRedirectAddress(_address); 420 | } 421 | 422 | //////////////////////////////////////////////////////////// 423 | ///// /// 424 | ///// ***Getters*** /// 425 | ///// /// 426 | //////////////////////////////////////////////////////////// 427 | 428 | function getTotalPooledBnb() public view override returns (uint256) { 429 | return (depositsDelegated + depositsBridgingOut + depositsInContract); 430 | } 431 | 432 | function getContracts() 433 | external 434 | view 435 | override 436 | returns (address _manager, address _bnbX, address _tokenHub, address _bcDepositWallet) 437 | { 438 | _manager = manager; 439 | _bnbX = bnbX; 440 | _tokenHub = tokenHub; 441 | _bcDepositWallet = bcDepositWallet; 442 | } 443 | 444 | /** 445 | * @return relayFee required by TokenHub contract to transfer funds from BSC -> BC 446 | */ 447 | function getTokenHubRelayFee() public view override returns (uint256) { 448 | return ITokenHub(tokenHub).relayFee(); 449 | } 450 | 451 | function getBotDelegateRequest(uint256 _uuid) external view override returns (BotDelegateRequest memory) { 452 | return uuidToBotDelegateRequestMap[_uuid]; 453 | } 454 | 455 | function getBotUndelegateRequest(uint256 _uuid) external view override returns (BotUndelegateRequest memory) { 456 | return uuidToBotUndelegateRequestMap[_uuid]; 457 | } 458 | 459 | /** 460 | * @dev Retrieves all withdrawal requests initiated by the given address 461 | * @param _address - Address of an user 462 | * @return userWithdrawalRequests array of user withdrawal requests 463 | */ 464 | function getUserWithdrawalRequests(address _address) external view override returns (WithdrawalRequest[] memory) { 465 | return userWithdrawalRequests[_address]; 466 | } 467 | 468 | /** 469 | * @dev Checks if the withdrawRequest is ready to claim 470 | * @param _user - Address of the user who raised WithdrawRequest 471 | * @param _idx - index of request in UserWithdrawls Array 472 | * @return _isClaimable - if the withdraw is ready to claim yet 473 | * @return _amount - Amount of BNB user would receive on withdraw claim 474 | * @notice Use `getUserWithdrawalRequests` to get the userWithdrawlRequests Array 475 | */ 476 | function getUserRequestStatus( 477 | address _user, 478 | uint256 _idx 479 | ) 480 | external 481 | view 482 | override 483 | returns (bool _isClaimable, uint256 _amount) 484 | { 485 | WithdrawalRequest[] storage userRequests = userWithdrawalRequests[_user]; 486 | 487 | require(_idx < userRequests.length, "Invalid index"); 488 | 489 | WithdrawalRequest storage withdrawRequest = userRequests[_idx]; 490 | uint256 uuid = withdrawRequest.uuid; 491 | uint256 amountInBnbX = withdrawRequest.amountInBnbX; 492 | 493 | BotUndelegateRequest storage botUndelegateRequest = uuidToBotUndelegateRequestMap[uuid]; 494 | 495 | // bot has triggered startUndelegation 496 | if (botUndelegateRequest.amount > 0) { 497 | uint256 totalBnbToWithdraw_ = botUndelegateRequest.amount; 498 | uint256 totalBnbXToBurn_ = botUndelegateRequest.amountInBnbX; 499 | _amount = (totalBnbToWithdraw_ * amountInBnbX) / totalBnbXToBurn_; 500 | } 501 | // bot has not triggered startUndelegation yet 502 | else { 503 | _amount = convertBnbXToBnb(amountInBnbX); 504 | } 505 | _isClaimable = (botUndelegateRequest.endTime != 0); 506 | } 507 | 508 | function getBnbXWithdrawLimit() external view override returns (uint256 _bnbXWithdrawLimit) { 509 | _bnbXWithdrawLimit = convertBnbToBnbX(depositsDelegated) - totalBnbXToBurn; 510 | } 511 | 512 | // function getExtraBnbInContract() public view override returns (uint256 _extraBnb) { 513 | // _extraBnb = address(this).balance - depositsInContract - totalClaimableBnb; 514 | // } 515 | 516 | //////////////////////////////////////////////////////////// 517 | ///// /// 518 | ///// ***Helpers & Utilities*** /// 519 | ///// /// 520 | //////////////////////////////////////////////////////////// 521 | 522 | function _tokenHubTransferOut(uint256 _amount, uint256 _relayFee) private { 523 | // have experimented with 13 hours and it worked 524 | uint64 expireTime = uint64(block.timestamp + 1 hours); 525 | 526 | bool isTransferred = ITokenHub(tokenHub).transferOut{ value: (_amount + _relayFee) }( 527 | address(0), bcDepositWallet, _amount, expireTime 528 | ); 529 | 530 | require(isTransferred, "TokenHub TransferOut Failed"); 531 | emit TransferOut(_amount); 532 | } 533 | 534 | /** 535 | * @dev Calculates amount of BnbX for `_amount` Bnb 536 | */ 537 | function convertBnbToBnbX(uint256 _amount) public view override returns (uint256) { 538 | uint256 totalShares = IBnbX(bnbX).totalSupply(); 539 | totalShares = totalShares == 0 ? 1 : totalShares; 540 | 541 | uint256 totalPooledBnb = getTotalPooledBnb(); 542 | totalPooledBnb = totalPooledBnb == 0 ? 1 : totalPooledBnb; 543 | 544 | uint256 amountInBnbX = (_amount * totalShares) / totalPooledBnb; 545 | 546 | return amountInBnbX; 547 | } 548 | 549 | /** 550 | * @dev Calculates amount of Bnb for `_amountInBnbX` BnbX 551 | */ 552 | function convertBnbXToBnb(uint256 _amountInBnbX) public view override returns (uint256) { 553 | uint256 totalShares = IBnbX(bnbX).totalSupply(); 554 | totalShares = totalShares == 0 ? 1 : totalShares; 555 | 556 | uint256 totalPooledBnb = getTotalPooledBnb(); 557 | totalPooledBnb = totalPooledBnb == 0 ? 1 : totalPooledBnb; 558 | 559 | uint256 amountInBnb = (_amountInBnbX * totalPooledBnb) / totalShares; 560 | 561 | return amountInBnb; 562 | } 563 | 564 | /** 565 | * @dev Flips the pause state 566 | */ 567 | function togglePause() external onlyRole(DEFAULT_ADMIN_ROLE) { 568 | paused() ? _unpause() : _pause(); 569 | } 570 | 571 | receive() external payable { 572 | if (msg.sender != redirectAddress) { 573 | AddressUpgradeable.sendValue(payable(redirectAddress), msg.value); 574 | } 575 | } 576 | 577 | modifier onlyManager() { 578 | require(msg.sender == manager, "Accessible only by Manager"); 579 | _; 580 | } 581 | } 582 | -------------------------------------------------------------------------------- /contracts/StakeManagerV2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.25; 3 | 4 | import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; 5 | import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; 6 | import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; 7 | import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; 8 | 9 | import "./interfaces/IStakeHub.sol"; 10 | import "./interfaces/IBnbX.sol"; 11 | import "./interfaces/IOperatorRegistry.sol"; 12 | import "./interfaces/IStakeManagerV2.sol"; 13 | import "./interfaces/IStakeCredit.sol"; 14 | import "./ProtocolConstants.sol"; 15 | 16 | /// @title StakeManagerV2 17 | /// @notice This contract manages staking, withdrawal, and re-delegation of BNB through a stake hub and operator registry. 18 | contract StakeManagerV2 is 19 | IStakeManagerV2, 20 | AccessControlUpgradeable, 21 | PausableUpgradeable, 22 | ReentrancyGuardUpgradeable 23 | { 24 | using SafeERC20Upgradeable for IBnbX; 25 | 26 | bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); 27 | bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE"); 28 | 29 | IStakeHub public constant STAKE_HUB = IStakeHub(ProtocolConstants.STAKE_HUB_ADDR); 30 | IOperatorRegistry public OPERATOR_REGISTRY; 31 | IBnbX public BNBX; 32 | 33 | address public staderTreasury; 34 | uint256 public totalDelegated; 35 | uint256 public feeBps; 36 | uint256 public maxExchangeRateSlippageBps; 37 | uint256 public maxActiveRequestsPerUser; 38 | uint256 public firstUnprocessedUserIndex; 39 | uint256 public firstUnbondingBatchIndex; 40 | uint256 public minWithdrawableBnbx; 41 | 42 | WithdrawalRequest[] private withdrawalRequests; 43 | BatchWithdrawalRequest[] private batchWithdrawalRequests; 44 | mapping(address => uint256[]) private userRequests; 45 | 46 | // @custom:oz-upgrades-unsafe-allow constructor 47 | constructor() { 48 | _disableInitializers(); 49 | } 50 | 51 | receive() external payable { } 52 | 53 | /// @notice Initialize the StakeManagerV2 contract. 54 | /// @param _admin Address of the admin, which will be provided DEFAULT_ADMIN_ROLE 55 | /// @param _operatorRegistry Address of the operator registry contract. 56 | /// @param _bnbX Address of the BnbX contract. 57 | /// @param _staderTreasury stader treasury address 58 | function initialize( 59 | address _admin, 60 | address _operatorRegistry, 61 | address _bnbX, 62 | address _staderTreasury 63 | ) 64 | external 65 | initializer 66 | { 67 | if (_admin == address(0)) revert ZeroAddress(); 68 | if (_operatorRegistry == address(0)) revert ZeroAddress(); 69 | if (_bnbX == address(0)) revert ZeroAddress(); 70 | if (_staderTreasury == address(0)) revert ZeroAddress(); 71 | 72 | __AccessControl_init(); 73 | __Pausable_init(); 74 | __ReentrancyGuard_init(); 75 | 76 | _setupRole(DEFAULT_ADMIN_ROLE, _admin); 77 | 78 | OPERATOR_REGISTRY = IOperatorRegistry(_operatorRegistry); 79 | BNBX = IBnbX(_bnbX); 80 | staderTreasury = _staderTreasury; 81 | feeBps = 1000; // 10% 82 | maxExchangeRateSlippageBps = 1000; // 10% 83 | maxActiveRequestsPerUser = 10; 84 | minWithdrawableBnbx = 1e15; 85 | } 86 | 87 | /*////////////////////////////////////////////////////////////// 88 | user interactions 89 | //////////////////////////////////////////////////////////////*/ 90 | 91 | /// @notice Delegate BNB to the preferred operator. 92 | /// @param _referralId referral id of KOL 93 | /// @return The amount of BnbX minted. 94 | function delegate(string calldata _referralId) 95 | external 96 | payable 97 | override 98 | nonReentrant 99 | whenNotPaused 100 | returns (uint256) 101 | { 102 | uint256 amountToMint = convertBnbToBnbX(msg.value); 103 | _delegate(); 104 | BNBX.mint(msg.sender, amountToMint); 105 | 106 | emit DelegateReferral(msg.sender, msg.value, amountToMint, _referralId); 107 | return amountToMint; 108 | } 109 | 110 | /// @notice Request to withdraw BnbX and get BNB back. 111 | /// @param _amount The amount of BnbX to withdraw. 112 | /// @return The index of the withdrawal request. 113 | function requestWithdraw( 114 | uint256 _amount, 115 | string calldata _referralId 116 | ) 117 | external 118 | override 119 | nonReentrant 120 | whenNotPaused 121 | returns (uint256) 122 | { 123 | if (_amount < minWithdrawableBnbx) revert WithdrawalBelowMinimum(); 124 | if (userRequests[msg.sender].length >= maxActiveRequestsPerUser) revert MaxLimitReached(); 125 | 126 | withdrawalRequests.push( 127 | WithdrawalRequest({ 128 | user: msg.sender, 129 | amountInBnbX: _amount, 130 | claimed: false, 131 | batchId: type(uint256).max, 132 | processed: false 133 | }) 134 | ); 135 | 136 | uint256 requestId = withdrawalRequests.length - 1; 137 | userRequests[msg.sender].push(requestId); 138 | 139 | BNBX.safeTransferFrom(msg.sender, address(this), _amount); 140 | emit RequestedWithdrawal(msg.sender, _amount, _referralId); 141 | 142 | return requestId; 143 | } 144 | 145 | /// @notice Claim the BNB from a withdrawal request. 146 | /// @param _idx user withdraw request array index 147 | /// @return The amount of BNB claimed. 148 | function claimWithdrawal(uint256 _idx) external override nonReentrant whenNotPaused returns (uint256) { 149 | WithdrawalRequest storage request = _extractRequest(msg.sender, _idx); 150 | if (request.claimed) revert AlreadyClaimed(); 151 | if (!request.processed) revert NotProcessed(); 152 | 153 | BatchWithdrawalRequest memory batchRequest = batchWithdrawalRequests[request.batchId]; 154 | if (!batchRequest.isClaimable) revert Unbonding(); 155 | 156 | request.claimed = true; 157 | uint256 amountInBnb = (batchRequest.amountInBnb * request.amountInBnbX) / batchRequest.amountInBnbX; 158 | 159 | (bool success,) = payable(msg.sender).call{ value: amountInBnb }(""); 160 | if (!success) revert TransferFailed(); 161 | 162 | emit ClaimedWithdrawal(msg.sender, _idx, amountInBnb); 163 | return amountInBnb; 164 | } 165 | 166 | /*////////////////////////////////////////////////////////////// 167 | operational methods 168 | //////////////////////////////////////////////////////////////*/ 169 | 170 | /// @notice Start the batch undelegation process. 171 | /// @param _batchSize The size of the batch. 172 | /// @param _operator The address of the operator to undelegate from. 173 | /// @dev This function can only be called by an address with the OPERATOR_ROLE. 174 | function startBatchUndelegation( 175 | uint256 _batchSize, 176 | address _operator 177 | ) 178 | external 179 | override 180 | whenNotPaused 181 | onlyRole(OPERATOR_ROLE) 182 | { 183 | if (_operator == address(0)) { 184 | // if operator is not provided, use preferred operator 185 | _operator = OPERATOR_REGISTRY.preferredWithdrawalOperator(); 186 | } 187 | if (!OPERATOR_REGISTRY.operatorExists(_operator)) { 188 | revert OperatorNotExisted(); 189 | } 190 | 191 | updateER(); 192 | 193 | address creditContract = STAKE_HUB.getValidatorCreditContract(_operator); 194 | uint256 amountInBnbXToBurn = _computeBnbXToBurn(_batchSize, creditContract); 195 | uint256 shares = IStakeCredit(creditContract).getSharesByPooledBNB(convertBnbXToBnb(amountInBnbXToBurn)); 196 | uint256 amountToWithdrawFromOperator = IStakeCredit(creditContract).getPooledBNBByShares(shares); 197 | totalDelegated -= amountToWithdrawFromOperator; 198 | 199 | batchWithdrawalRequests.push( 200 | BatchWithdrawalRequest({ 201 | amountInBnb: amountToWithdrawFromOperator, 202 | amountInBnbX: amountInBnbXToBurn, 203 | unlockTime: block.timestamp + STAKE_HUB.unbondPeriod(), 204 | operator: _operator, 205 | isClaimable: false 206 | }) 207 | ); 208 | 209 | BNBX.burn(address(this), amountInBnbXToBurn); 210 | STAKE_HUB.undelegate(_operator, shares); 211 | 212 | emit StartedBatchUndelegation(_operator, amountToWithdrawFromOperator, amountInBnbXToBurn); 213 | } 214 | 215 | /// @notice Complete the undelegation process. 216 | function completeBatchUndelegation() external override nonReentrant whenNotPaused { 217 | BatchWithdrawalRequest storage batchRequest = batchWithdrawalRequests[firstUnbondingBatchIndex]; 218 | if (batchRequest.unlockTime > block.timestamp) revert Unbonding(); 219 | 220 | batchRequest.isClaimable = true; 221 | firstUnbondingBatchIndex++; 222 | STAKE_HUB.claim(batchRequest.operator, 1); // claims 1 request 223 | 224 | emit CompletedBatchUndelegation(batchRequest.operator, batchRequest.amountInBnb); 225 | } 226 | 227 | /// @notice Redelegate staked BNB from one operator to another. 228 | /// @param _fromOperator The address of the operator to redelegate from. 229 | /// @param _toOperator The address of the operator to redelegate to. 230 | /// @param _amount The amount of BNB to redelegate. 231 | /// @dev redelegate has a fee associated with it. This fee will be consumed from TVL. See fn:getRedelegationFee() 232 | /// @dev redelegate doesn't have a waiting period 233 | /// @dev This function can only be called by an address with the MANAGER_ROLE. 234 | function redelegate( 235 | address _fromOperator, 236 | address _toOperator, 237 | uint256 _amount 238 | ) 239 | external 240 | override 241 | onlyRole(MANAGER_ROLE) 242 | { 243 | if (!OPERATOR_REGISTRY.operatorExists(_fromOperator)) { 244 | revert OperatorNotExisted(); 245 | } 246 | if (!OPERATOR_REGISTRY.operatorExists(_toOperator)) { 247 | revert OperatorNotExisted(); 248 | } 249 | 250 | uint256 shares = IStakeCredit(STAKE_HUB.getValidatorCreditContract(_fromOperator)).getSharesByPooledBNB(_amount); 251 | STAKE_HUB.redelegate(_fromOperator, _toOperator, shares, true); 252 | 253 | emit Redelegated(_fromOperator, _toOperator, _amount); 254 | } 255 | 256 | /// @notice Update the Exchange Rate 257 | function updateER() public override nonReentrant whenNotPaused { 258 | uint256 currentER = convertBnbXToBnb(1 ether); 259 | _updateER(); 260 | _checkIfNewExchangeRateWithinLimits(currentER); 261 | } 262 | 263 | /// @notice force update the exchange rate 264 | /// @dev This function can only be called by an address with the DEFAULT_ADMIN_ROLE. 265 | function forceUpdateER() external onlyRole(DEFAULT_ADMIN_ROLE) { 266 | _updateER(); 267 | } 268 | 269 | /// @notice Delegate BNB to the preferred operator without minting BnbX. 270 | /// @dev This function is useful for boosting staking rewards and, 271 | /// for initial Fusion hardfork migration without affecting the token supply. 272 | /// @dev Can only be called by an address with the MANAGER_ROLE. 273 | function delegateWithoutMinting() external payable override onlyRole(MANAGER_ROLE) { 274 | if (BNBX.totalSupply() == 0) revert ZeroAmount(); 275 | 276 | _delegate(); 277 | } 278 | 279 | /** 280 | * @notice Triggers stopped state. 281 | * @dev Contract must not be paused 282 | * @dev Can only be called by an address with the MANAGER_ROLE. 283 | */ 284 | function pause() external override onlyRole(MANAGER_ROLE) { 285 | _pause(); 286 | } 287 | 288 | /** 289 | * @notice Returns to normal state. 290 | * @dev Contract must be paused 291 | * @dev Can only be called by an address with the DEFAULT_ADMIN_ROLE. 292 | */ 293 | function unpause() external override onlyRole(DEFAULT_ADMIN_ROLE) { 294 | _unpause(); 295 | } 296 | 297 | /*////////////////////////////////////////////////////////////// 298 | setters 299 | //////////////////////////////////////////////////////////////*/ 300 | 301 | /// @notice Sets the address of the Stader Treasury. 302 | /// @param _staderTreasury The new address of the Stader Treasury. 303 | /// @dev Can only be called by an address with the DEFAULT_ADMIN_ROLE. 304 | function setStaderTreasury(address _staderTreasury) external onlyRole(DEFAULT_ADMIN_ROLE) { 305 | if (_staderTreasury == address(0)) revert ZeroAddress(); 306 | staderTreasury = _staderTreasury; 307 | emit SetStaderTreasury(_staderTreasury); 308 | } 309 | 310 | /// @notice set feeBps 311 | /// @dev updates exchange rate before setting the new feeBps 312 | /// @dev Can only be called by an address with the DEFAULT_ADMIN_ROLE. 313 | function setFeeBps(uint256 _feeBps) external onlyRole(DEFAULT_ADMIN_ROLE) { 314 | if (_feeBps > ProtocolConstants.MAX_ALLOWED_FEE_BPS) revert MaxLimitReached(); 315 | updateER(); 316 | feeBps = _feeBps; 317 | emit SetFeeBps(_feeBps); 318 | } 319 | 320 | /// @notice set maxActiveRequestsPerUser 321 | /// @dev Can only be called by an address with the DEFAULT_ADMIN_ROLE. 322 | function setMaxActiveRequestsPerUser(uint256 _maxActiveRequestsPerUser) external onlyRole(DEFAULT_ADMIN_ROLE) { 323 | maxActiveRequestsPerUser = _maxActiveRequestsPerUser; 324 | emit SetMaxActiveRequestsPerUser(_maxActiveRequestsPerUser); 325 | } 326 | 327 | /// @notice set maxExchangeRateSlippageBps 328 | /// @dev Can only be called by an address with the DEFAULT_ADMIN_ROLE. 329 | function setMaxExchangeRateSlippageBps(uint256 _maxExchangeRateSlippageBps) external onlyRole(DEFAULT_ADMIN_ROLE) { 330 | maxExchangeRateSlippageBps = _maxExchangeRateSlippageBps; 331 | emit SetMaxExchangeRateSlippageBps(_maxExchangeRateSlippageBps); 332 | } 333 | 334 | /// @notice set minWithdrawableBnbx 335 | /// @dev Can only be called by an address with the DEFAULT_ADMIN_ROLE. 336 | function setMinWithdrawableBnbx(uint256 _minWithdrawableBnbx) external onlyRole(DEFAULT_ADMIN_ROLE) { 337 | minWithdrawableBnbx = _minWithdrawableBnbx; 338 | emit SetMinWithdrawableBnbx(_minWithdrawableBnbx); 339 | } 340 | 341 | /*////////////////////////////////////////////////////////////// 342 | internal functions 343 | //////////////////////////////////////////////////////////////*/ 344 | 345 | /// @notice Delegate BNB to the preferred operator. 346 | function _delegate() internal { 347 | address preferredOperatorAddress = OPERATOR_REGISTRY.preferredDepositOperator(); 348 | totalDelegated += msg.value; 349 | 350 | STAKE_HUB.delegate{ value: msg.value }(preferredOperatorAddress, true); 351 | emit Delegated(preferredOperatorAddress, msg.value); 352 | } 353 | 354 | /// @dev extracts the withdrawal request and removes from userRequests 355 | function _extractRequest(address _user, uint256 _idx) internal returns (WithdrawalRequest storage request) { 356 | uint256[] storage userRequestsStorage = userRequests[_user]; 357 | // any change to userRequestsStorage will also be reflected in userRequests[_user] 358 | // see ref: https://docs.soliditylang.org/en/v0.8.25/types.html#data-location-and-assignment-behavior 359 | uint256 numUserRequests = userRequestsStorage.length; 360 | if (numUserRequests == 0) revert NoWithdrawalRequests(); 361 | if (_idx >= numUserRequests) revert InvalidIndex(); 362 | 363 | uint256 requestId = userRequestsStorage[_idx]; 364 | userRequestsStorage[_idx] = userRequestsStorage[numUserRequests - 1]; 365 | userRequestsStorage.pop(); 366 | return withdrawalRequests[requestId]; 367 | } 368 | 369 | /// @dev internal fn to update the exchange rate 370 | function _updateER() internal { 371 | uint256 totalPooledBnb = getActualStakeAcrossAllOperators(); 372 | _mintFees(totalPooledBnb); 373 | totalDelegated = totalPooledBnb; 374 | } 375 | 376 | /// @dev internal fn to mint the fees to the stader treasury 377 | function _mintFees(uint256 _totalPooledBnb) internal { 378 | if (_totalPooledBnb <= totalDelegated) return; 379 | uint256 feeInBnb = ((_totalPooledBnb - totalDelegated) * feeBps) / ProtocolConstants.BPS_DENOM; 380 | uint256 amountToMint = convertBnbToBnbX(feeInBnb); 381 | BNBX.mint(staderTreasury, amountToMint); 382 | } 383 | 384 | /// @dev internal fn to check if new exchange rate is within limits 385 | function _checkIfNewExchangeRateWithinLimits(uint256 currentER) internal { 386 | uint256 maxAllowableDelta = maxExchangeRateSlippageBps * currentER / ProtocolConstants.BPS_DENOM; 387 | uint256 newER = convertBnbXToBnb(1 ether); 388 | if (newER > currentER + maxAllowableDelta) { 389 | revert ExchangeRateOutOfBounds(currentER, maxAllowableDelta, newER); 390 | } 391 | emit ExchangeRateUpdated(currentER, newER); 392 | } 393 | 394 | /// @dev internal fn to compute the amount of BNBX to burn for a batch 395 | function _computeBnbXToBurn( 396 | uint256 _batchSize, 397 | address _creditContract 398 | ) 399 | internal 400 | returns (uint256 amountInBnbXToBurn) 401 | { 402 | uint256 pooledBnb = IStakeCredit(_creditContract).getPooledBNB(address(this)); 403 | 404 | uint256 cummulativeBnbXToBurn; 405 | uint256 cummulativeBnbToWithdraw; 406 | uint256 processedCount; 407 | 408 | while ((processedCount < _batchSize) && (firstUnprocessedUserIndex < withdrawalRequests.length)) { 409 | cummulativeBnbXToBurn += withdrawalRequests[firstUnprocessedUserIndex].amountInBnbX; 410 | cummulativeBnbToWithdraw = convertBnbXToBnb(cummulativeBnbXToBurn); 411 | if (cummulativeBnbToWithdraw > pooledBnb) break; 412 | 413 | amountInBnbXToBurn = cummulativeBnbXToBurn; 414 | withdrawalRequests[firstUnprocessedUserIndex].processed = true; 415 | withdrawalRequests[firstUnprocessedUserIndex].batchId = batchWithdrawalRequests.length; 416 | unchecked { 417 | ++processedCount; 418 | ++firstUnprocessedUserIndex; 419 | } 420 | } 421 | 422 | if (amountInBnbXToBurn == 0) revert NoWithdrawalRequests(); 423 | } 424 | 425 | /*////////////////////////////////////////////////////////////// 426 | getters 427 | //////////////////////////////////////////////////////////////*/ 428 | 429 | /// @notice Get withdrawal requests of a user 430 | function getUserRequestIds(address _user) external view returns (uint256[] memory) { 431 | return userRequests[_user]; 432 | } 433 | 434 | /// @notice get user request info by request Id 435 | function getUserRequestInfo(uint256 _requestId) external view returns (WithdrawalRequest memory) { 436 | return withdrawalRequests[_requestId]; 437 | } 438 | 439 | /// @notice get active withdrawal requests count 440 | function getUnprocessedWithdrawalRequestCount() external view returns (uint256) { 441 | return withdrawalRequests.length - firstUnprocessedUserIndex; 442 | } 443 | 444 | /// @notice total number of user withdraw requests 445 | function getWithdrawalRequestCount() external view returns (uint256) { 446 | return withdrawalRequests.length; 447 | } 448 | 449 | /// @notice get batch withdrawal request info by batch Id 450 | function getBatchWithdrawalRequestInfo(uint256 _batchId) external view returns (BatchWithdrawalRequest memory) { 451 | return batchWithdrawalRequests[_batchId]; 452 | } 453 | 454 | /// @notice total number of batch withdrawal requests 455 | function getBatchWithdrawalRequestCount() external view returns (uint256) { 456 | return batchWithdrawalRequests.length; 457 | } 458 | 459 | /// @notice Get the fee associated with a redelegation. 460 | /// @param _amount The amount of BNB to redelegate. 461 | /// @return The fee associated with the redelegation. 462 | function getRedelegationFee(uint256 _amount) external view returns (uint256) { 463 | return (_amount * STAKE_HUB.redelegateFeeRate()) / STAKE_HUB.REDELEGATE_FEE_RATE_BASE(); 464 | } 465 | 466 | /// @notice Convert BNB to BnbX. 467 | /// @param _amount The amount of BNB to convert. 468 | /// @return The amount of BnbX equivalent. 469 | function convertBnbToBnbX(uint256 _amount) public view override returns (uint256) { 470 | uint256 totalShares = BNBX.totalSupply(); 471 | totalShares = totalShares == 0 ? 1 : totalShares; 472 | uint256 totalDelegated_ = totalDelegated == 0 ? 1 : totalDelegated; 473 | 474 | return (_amount * totalShares) / totalDelegated_; 475 | } 476 | 477 | /// @notice Convert BnbX to BNB. 478 | /// @param _amountInBnbX The amount of BnbX to convert. 479 | /// @return The amount of BNB equivalent. 480 | function convertBnbXToBnb(uint256 _amountInBnbX) public view override returns (uint256) { 481 | uint256 totalShares = BNBX.totalSupply(); 482 | totalShares = totalShares == 0 ? 1 : totalShares; 483 | 484 | return (_amountInBnbX * totalDelegated) / totalShares; 485 | } 486 | 487 | /// @notice Get the total stake across all operators. 488 | /// @dev This is not stale 489 | /// @dev gas expensive: use cautiously 490 | /// @return The total stake in BNB. 491 | function getActualStakeAcrossAllOperators() public view returns (uint256) { 492 | uint256 totalStake; 493 | address[] memory operators = OPERATOR_REGISTRY.getOperators(); 494 | uint256 operatorsLength = operators.length; 495 | 496 | for (uint256 i; i < operatorsLength;) { 497 | address creditContract = STAKE_HUB.getValidatorCreditContract(operators[i]); 498 | totalStake += IStakeCredit(creditContract).getPooledBNB(address(this)); 499 | unchecked { 500 | ++i; 501 | } 502 | } 503 | return totalStake; 504 | } 505 | 506 | /// @notice Auxillary function to calculate amount of BNBx to be burn given batch size 507 | /// @param _batchSize - number of requests to process in a batch 508 | /// @return bnbxToBurn - amount of BNBx to be burn given the batch size 509 | function getBnbxToBurnForBatchSize(uint256 _batchSize) external view returns (uint256 bnbxToBurn) { 510 | uint256 processedCount; 511 | uint256 tempFirstUnprocessedUserIndex = firstUnprocessedUserIndex; 512 | while ((processedCount < _batchSize) && (tempFirstUnprocessedUserIndex < withdrawalRequests.length)) { 513 | bnbxToBurn += withdrawalRequests[tempFirstUnprocessedUserIndex].amountInBnbX; 514 | unchecked { 515 | ++processedCount; 516 | ++tempFirstUnprocessedUserIndex; 517 | } 518 | } 519 | } 520 | } 521 | -------------------------------------------------------------------------------- /contracts/campaigns/KOLReferral.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 5 | import "@opengsn/contracts/src/ERC2771Recipient.sol"; 6 | 7 | contract KOLReferral is Initializable, ERC2771Recipient { 8 | address public admin; 9 | 10 | mapping(address => string) public walletToReferralId; 11 | mapping(string => address) public referralIdToWallet; 12 | mapping(address => string) public userReferredBy; 13 | mapping(address => address[]) private _kolToUsers; 14 | 15 | address[] private _users; 16 | address[] private _kols; 17 | 18 | modifier onlyAdmin() { 19 | require(_msgSender() == admin, "Only Admin"); 20 | _; 21 | } 22 | 23 | /// @custom:oz-upgrades-unsafe-allow constructor 24 | constructor() { 25 | _disableInitializers(); 26 | } 27 | 28 | function initialize(address admin_, address trustedForwarder_) external initializer { 29 | require(admin_ != address(0), "zero address"); 30 | require(trustedForwarder_ != address(0), "zero address"); 31 | admin = admin_; 32 | _setTrustedForwarder(trustedForwarder_); 33 | } 34 | 35 | function registerKOL(address wallet, string memory referralId) external onlyAdmin { 36 | require(referralIdToWallet[referralId] == address(0), "ReferralId is already taken"); 37 | require(bytes(walletToReferralId[wallet]).length == 0, "ReferralId is already assigned for this wallet"); 38 | walletToReferralId[wallet] = referralId; 39 | referralIdToWallet[referralId] = wallet; 40 | _kols.push(wallet); 41 | } 42 | 43 | function storeUserInfo(string memory referralId) external { 44 | require(referralIdToWallet[referralId] != address(0), "Invalid ReferralId"); 45 | require(bytes(userReferredBy[_msgSender()]).length == 0, "User is already referred before"); 46 | userReferredBy[_msgSender()] = referralId; 47 | _users.push(_msgSender()); 48 | 49 | address kolWallet = referralIdToWallet[referralId]; 50 | 51 | require(_msgSender() != kolWallet, "kol not allowed as user"); 52 | _kolToUsers[kolWallet].push(_msgSender()); 53 | } 54 | 55 | function queryUserReferrer(address user) external view returns (address _referrer) { 56 | string memory referralId = userReferredBy[user]; 57 | return referralIdToWallet[referralId]; 58 | } 59 | 60 | function getKOLUserList(address kol) external view returns (address[] memory) { 61 | return _kolToUsers[kol]; 62 | } 63 | 64 | function getKOLs() external view returns (address[] memory) { 65 | return _kols; 66 | } 67 | 68 | function getUsers( 69 | uint256 startIdx, 70 | uint256 maxNumUsers 71 | ) 72 | external 73 | view 74 | returns (uint256 numUsers, address[] memory userList) 75 | { 76 | require(startIdx < _users.length, "invalid startIdx"); 77 | 78 | if (startIdx + maxNumUsers > _users.length) { 79 | maxNumUsers = _users.length - startIdx; 80 | } 81 | 82 | userList = new address[](maxNumUsers); 83 | for (numUsers = 0; startIdx < _users.length && numUsers < maxNumUsers; numUsers++) { 84 | userList[numUsers] = _users[startIdx++]; 85 | } 86 | 87 | return (numUsers, userList); 88 | } 89 | 90 | function getKOLCount() external view returns (uint256) { 91 | return _kols.length; 92 | } 93 | 94 | function getUserCount() external view returns (uint256) { 95 | return _users.length; 96 | } 97 | 98 | function getKOLRefCount(address kol) external view returns (uint256) { 99 | return _kolToUsers[kol].length; 100 | } 101 | 102 | function setAdmin(address admin_) external onlyAdmin { 103 | require(admin_ != address(0), "zero address"); 104 | require(admin_ != admin, "old admin == new admin"); 105 | admin = admin_; 106 | } 107 | 108 | function setTrustedForwarder(address trustedForwarder_) external onlyAdmin { 109 | require(trustedForwarder_ != address(0), "zero address"); 110 | _setTrustedForwarder(trustedForwarder_); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /contracts/interfaces/IBnbX.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; 5 | 6 | /// @title BnbX interface 7 | interface IBnbX is IERC20Upgradeable { 8 | function initialize(address _manager) external; 9 | 10 | function mint(address _account, uint256 _amount) external; 11 | 12 | function burn(address _account, uint256 _amount) external; 13 | 14 | function setStakeManager(address _address) external; 15 | 16 | event SetStakeManager(address indexed _address); 17 | } 18 | -------------------------------------------------------------------------------- /contracts/interfaces/IOperatorRegistry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.25; 3 | 4 | /// @title IOperatorRegistry 5 | /// @notice Node Operator registry interface 6 | interface IOperatorRegistry { 7 | error OperatorExisted(); 8 | error OperatorNotExisted(); 9 | error OperatorJailed(); 10 | error OperatorIsPreferredDeposit(); 11 | error OperatorIsPreferredWithdrawal(); 12 | error DelegationExists(); 13 | error ZeroAddress(); 14 | error NegligibleAmountTooHigh(); 15 | 16 | event AddedOperator(address indexed _operator); 17 | event RemovedOperator(address indexed _operator); 18 | event SetPreferredDepositOperator(address indexed _operator); 19 | event SetPreferredWithdrawalOperator(address indexed _operator); 20 | 21 | function preferredDepositOperator() external view returns (address); 22 | function preferredWithdrawalOperator() external view returns (address); 23 | function getOperatorAt(uint256) external view returns (address); 24 | function getOperatorsLength() external view returns (uint256); 25 | function getOperators() external view returns (address[] memory); 26 | function addOperator(address _operator) external; 27 | function removeOperator(address _operator) external; 28 | function setPreferredDepositOperator(address _operator) external; 29 | function setPreferredWithdrawalOperator(address _operator) external; 30 | function pause() external; 31 | function unpause() external; 32 | function operatorExists(address _operator) external view returns (bool); 33 | } 34 | -------------------------------------------------------------------------------- /contracts/interfaces/IStakeCredit.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.25; 3 | 4 | interface IStakeCredit { 5 | /** 6 | * @param delegator the address of the delegator 7 | * @return shares the amount of shares minted 8 | */ 9 | function delegate(address delegator) external payable returns (uint256 shares); 10 | 11 | /** 12 | * @return the total amount of BNB staked and reward of the delegator. 13 | */ 14 | function getPooledBNB(address account) external view returns (uint256); 15 | 16 | /** 17 | * @return the amount of shares that corresponds to `_bnbAmount` protocol-controlled BNB. 18 | */ 19 | function getSharesByPooledBNB(uint256 bnbAmount) external view returns (uint256); 20 | 21 | /** 22 | * @return the amount of BNB that corresponds to `_sharesAmount` token shares. 23 | */ 24 | function getPooledBNBByShares(uint256 shares) external view returns (uint256); 25 | } 26 | -------------------------------------------------------------------------------- /contracts/interfaces/IStakeHub.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.25; 3 | 4 | interface IStakeHub { 5 | /** 6 | * @param operatorAddress the operator address of the validator to be delegated to 7 | * @param delegateVotePower whether to delegate vote power to the validator 8 | */ 9 | function delegate(address operatorAddress, bool delegateVotePower) external payable; 10 | 11 | /** 12 | * @dev Undelegate BNB from a validator, fund is only claimable few days later 13 | * @param operatorAddress the operator address of the validator to be undelegated from 14 | * @param shares the shares to be undelegated 15 | */ 16 | function undelegate(address operatorAddress, uint256 shares) external; 17 | 18 | /** 19 | * @param srcValidator the operator address of the validator to be redelegated from 20 | * @param dstValidator the operator address of the validator to be redelegated to 21 | * @param shares the shares to be redelegated 22 | * @param delegateVotePower whether to delegate vote power to the dstValidator 23 | */ 24 | function redelegate(address srcValidator, address dstValidator, uint256 shares, bool delegateVotePower) external; 25 | 26 | /** 27 | * @notice get the credit contract address of a validator 28 | * 29 | * @param operatorAddress the operator address of the validator 30 | * 31 | * @return creditContract the credit contract address of the validator 32 | */ 33 | function getValidatorCreditContract(address operatorAddress) external view returns (address creditContract); 34 | 35 | /** 36 | * @notice get the basic info of a validator 37 | * 38 | * @param operatorAddress the operator address of the validator 39 | * 40 | * @return createdTime the creation time of the validator 41 | * @return jailed whether the validator is jailed 42 | * @return jailUntil the jail time of the validator 43 | */ 44 | function getValidatorBasicInfo(address operatorAddress) 45 | external 46 | view 47 | returns (uint256 createdTime, bool jailed, uint256 jailUntil); 48 | 49 | /** 50 | * @dev Claim the undelegated BNB from the pool after unbondPeriod 51 | * @param operatorAddress the operator address of the validator 52 | * @param requestNumber the request number of the undelegation. 0 means claim all 53 | */ 54 | function claim(address operatorAddress, uint256 requestNumber) external; 55 | 56 | function unbondPeriod() external view returns (uint256); 57 | function minDelegationBNBChange() external view returns (uint256); 58 | 59 | function REDELEGATE_FEE_RATE_BASE() external view returns (uint256); 60 | function redelegateFeeRate() external view returns (uint256); 61 | } 62 | -------------------------------------------------------------------------------- /contracts/interfaces/IStakeManager.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.0; 3 | 4 | interface IStakeManager { 5 | struct BotDelegateRequest { 6 | uint256 startTime; 7 | uint256 endTime; 8 | uint256 amount; 9 | } 10 | 11 | struct BotUndelegateRequest { 12 | uint256 startTime; 13 | uint256 endTime; 14 | uint256 amount; 15 | uint256 amountInBnbX; 16 | } 17 | 18 | struct WithdrawalRequest { 19 | uint256 uuid; 20 | uint256 amountInBnbX; 21 | uint256 startTime; 22 | } 23 | 24 | function initialize( 25 | address _bnbX, 26 | address _admin, 27 | address _manager, 28 | address _tokenHub, 29 | address _bcDepositWallet, 30 | address _bot, 31 | uint256 _feeBps 32 | ) 33 | external; 34 | 35 | function deposit() external payable; 36 | 37 | function startDelegation() external payable returns (uint256 _uuid, uint256 _amount); 38 | 39 | function completeDelegation(uint256 _uuid) external; 40 | 41 | function addRestakingRewards(uint256 _id, uint256 _amount) external; 42 | 43 | function requestWithdraw(uint256 _amountInBnbX) external; 44 | 45 | function claimWithdraw(uint256 _idx) external; 46 | 47 | function startUndelegation() external returns (uint256 _uuid, uint256 _amount); 48 | 49 | function undelegationStarted(uint256 _uuid) external; 50 | 51 | function completeUndelegation(uint256 _uuid) external payable; 52 | 53 | function proposeNewManager(address _address) external; 54 | 55 | function acceptNewManager() external; 56 | 57 | function setBotRole(address _address) external; 58 | 59 | function revokeBotRole(address _address) external; 60 | 61 | function setBCDepositWallet(address _address) external; 62 | 63 | function setMinDelegateThreshold(uint256 _minDelegateThreshold) external; 64 | 65 | function setMinUndelegateThreshold(uint256 _minUndelegateThreshold) external; 66 | 67 | function setFeeBps(uint256 _feeBps) external; 68 | 69 | function setRedirectAddress(address _address) external; 70 | 71 | function getTotalPooledBnb() external view returns (uint256); 72 | 73 | function getContracts() 74 | external 75 | view 76 | returns (address _manager, address _bnbX, address _tokenHub, address _bcDepositWallet); 77 | 78 | function getTokenHubRelayFee() external view returns (uint256); 79 | 80 | function getBotDelegateRequest(uint256 _uuid) external view returns (BotDelegateRequest memory); 81 | 82 | function getBotUndelegateRequest(uint256 _uuid) external view returns (BotUndelegateRequest memory); 83 | 84 | function getUserWithdrawalRequests(address _address) external view returns (WithdrawalRequest[] memory); 85 | 86 | function getUserRequestStatus( 87 | address _user, 88 | uint256 _idx 89 | ) 90 | external 91 | view 92 | returns (bool _isClaimable, uint256 _amount); 93 | 94 | function getBnbXWithdrawLimit() external view returns (uint256 _bnbXWithdrawLimit); 95 | 96 | function convertBnbToBnbX(uint256 _amount) external view returns (uint256); 97 | 98 | function convertBnbXToBnb(uint256 _amountInBnbX) external view returns (uint256); 99 | 100 | event Delegate(uint256 _uuid, uint256 _amount); 101 | event TransferOut(uint256 _amount); 102 | event RequestWithdraw(address indexed _account, uint256 _amountInBnbX); 103 | event ClaimWithdrawal(address indexed _account, uint256 _idx, uint256 _amount); 104 | event Undelegate(uint256 _uuid, uint256 _amount); 105 | event Redelegate(uint256 _rewardsId, uint256 _amount); 106 | event SetManager(address indexed _address); 107 | event ProposeManager(address indexed _address); 108 | event SetBotRole(address indexed _address); 109 | event RevokeBotRole(address indexed _address); 110 | event SetBCDepositWallet(address indexed _address); 111 | event SetMinDelegateThreshold(uint256 _minDelegateThreshold); 112 | event SetMinUndelegateThreshold(uint256 _minUndelegateThreshold); 113 | event SetFeeBps(uint256 _feeBps); 114 | event SetRedirectAddress(address indexed _address); 115 | } 116 | -------------------------------------------------------------------------------- /contracts/interfaces/IStakeManagerV2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.25; 3 | 4 | struct WithdrawalRequest { 5 | address user; 6 | bool processed; 7 | bool claimed; 8 | uint256 amountInBnbX; 9 | uint256 batchId; 10 | } 11 | 12 | struct BatchWithdrawalRequest { 13 | uint256 amountInBnb; 14 | uint256 amountInBnbX; 15 | uint256 unlockTime; 16 | address operator; 17 | bool isClaimable; 18 | } 19 | 20 | interface IStakeManagerV2 { 21 | error TransferFailed(); 22 | error OperatorNotExisted(); 23 | error ZeroAmount(); 24 | error ZeroAddress(); 25 | error Unbonding(); 26 | error NoOperatorsAvailable(); 27 | error NoWithdrawalRequests(); 28 | error InvalidIndex(); 29 | error AlreadyClaimed(); 30 | error NotProcessed(); 31 | error MaxLimitReached(); 32 | error ExchangeRateOutOfBounds(uint256 _currentER, uint256 _maxAllowableDelta, uint256 _newER); 33 | error WithdrawalBelowMinimum(); 34 | 35 | function delegate(string calldata _referralId) external payable returns (uint256); 36 | function requestWithdraw(uint256 _amount, string calldata _referralId) external returns (uint256); 37 | function claimWithdrawal(uint256 _idx) external returns (uint256); 38 | 39 | function startBatchUndelegation(uint256 _batchSize, address _operator) external; 40 | function completeBatchUndelegation() external; 41 | function redelegate(address _fromOperator, address _toOperator, uint256 _amount) external; 42 | function delegateWithoutMinting() external payable; 43 | function updateER() external; 44 | function pause() external; 45 | function unpause() external; 46 | 47 | function convertBnbToBnbX(uint256 _amount) external view returns (uint256); 48 | function convertBnbXToBnb(uint256 _amountInBnbX) external view returns (uint256); 49 | function getUserRequestIds(address _user) external returns (uint256[] memory); 50 | function getRedelegationFee(uint256 _amount) external view returns (uint256); 51 | 52 | event Delegated(address indexed _account, uint256 _amount); 53 | event RequestedWithdrawal(address indexed _account, uint256 _amountInBnbX, string _referralId); 54 | event ClaimedWithdrawal(address indexed _account, uint256 _index, uint256 _amountInBnb); 55 | event Redelegated(address indexed _fromOperator, address indexed _toOperator, uint256 _amountInBnb); 56 | event DelegateReferral(address indexed _account, uint256 _amountInBnb, uint256 _amountInBnbX, string _referralId); 57 | event SetStaderTreasury(address _treasury); 58 | event SetFeeBps(uint256 _feeBps); 59 | event CompletedBatchUndelegation(address indexed _operator, uint256 _amountInBnb); 60 | event StartedBatchUndelegation(address indexed _operator, uint256 _amountInBnb, uint256 _amountInBnbX); 61 | event ExchangeRateUpdated(uint256 _currentER, uint256 _newER); 62 | event SetMaxActiveRequestsPerUser(uint256 _maxActiveRequestsPerUser); 63 | event SetMaxExchangeRateSlippageBps(uint256 _maxExchangeRateSlippageBps); 64 | event SetMinWithdrawableBnbx(uint256 _minWithdrawableBnbx); 65 | } 66 | -------------------------------------------------------------------------------- /contracts/interfaces/ITokenHub.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.0; 3 | 4 | /** 5 | * @title binance TokenHub interface 6 | * @dev Helps in cross-chain transfers (BSC -> BC) 7 | */ 8 | interface ITokenHub { 9 | function relayFee() external view returns (uint256); 10 | 11 | function transferOut( 12 | address contractAddr, 13 | address recipient, 14 | uint256 amount, 15 | uint64 expireTime 16 | ) 17 | external 18 | payable 19 | returns (bool); 20 | } 21 | -------------------------------------------------------------------------------- /contracts/mocks/TokenHubMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.0; 3 | 4 | import { ITokenHub } from "../interfaces/ITokenHub.sol"; 5 | 6 | contract TokenHubMock is ITokenHub { 7 | uint256 public constant TEN_DECIMALS = 1e10; 8 | 9 | function transferOut(address, address, uint256, uint64 expireTime) external payable override returns (bool) { 10 | require(expireTime >= block.timestamp + 120, "expireTime must be two minutes later"); 11 | require(msg.value % TEN_DECIMALS == 0, "invalid received BNB amount: precision loss in amount conversion"); 12 | return true; 13 | } 14 | 15 | function relayFee() external pure override returns (uint256) { 16 | uint256 fee = 10_000_000_000_000_000; // 0.01 BNB on testnet, 0.002 BNB on mainnet 17 | return fee; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | evm_version = "cancun" # See https://www.evmdiff.com/features?name=PUSH0&kind=opcode 3 | src = "contracts" 4 | out = "out" 5 | libs = ["node_modules", "lib"] 6 | test = "test" 7 | cache_path = "cache_forge" 8 | optimizer = true 9 | optimizer_runs = 10_000 10 | solc_version = "0.8.25" 11 | build_info = true 12 | extra_output = ["storageLayout"] 13 | fs_permissions = [{ access = "read", path = "./" }] 14 | 15 | [profile.ci] 16 | fuzz = { runs = 10_000 } 17 | verbosity = 3 18 | 19 | [fmt] 20 | bracket_spacing = true 21 | int_types = "long" 22 | line_length = 120 23 | multiline_func_header = "all" 24 | number_underscore = "thousands" 25 | quote_style = "double" 26 | tab_width = 4 27 | wrap_comments = false 28 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | 3 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 4 | import { HardhatUserConfig, task } from "hardhat/config"; 5 | import { 6 | deployDirect, 7 | deployProxy, 8 | upgradeProxy, 9 | } from "./script/hardhat-scripts/tasks"; 10 | 11 | import "@nomicfoundation/hardhat-toolbox"; 12 | import "@nomicfoundation/hardhat-foundry"; 13 | import "@openzeppelin/hardhat-upgrades"; 14 | 15 | task("deployBnbXProxy", "Deploy BnbX Proxy only") 16 | .addPositionalParam("admin") 17 | .setAction(async ({ admin }, hre: HardhatRuntimeEnvironment) => { 18 | await deployProxy(hre, "BnbX", admin); 19 | }); 20 | 21 | task("upgradeBnbXProxy", "Upgrade BnbX Proxy") 22 | .addPositionalParam("proxyAddress") 23 | .setAction(async ({ proxyAddress }, hre: HardhatRuntimeEnvironment) => { 24 | await upgradeProxy(hre, "BnbX", proxyAddress); 25 | }); 26 | 27 | task("deployBnbXImpl", "Deploy BnbX Implementation only").setAction( 28 | async (args, hre: HardhatRuntimeEnvironment) => { 29 | await deployDirect(hre, "BnbX"); 30 | } 31 | ); 32 | 33 | task("deployStakeManagerProxy", "Deploy StakeManager Proxy only") 34 | .addPositionalParam("bnbX") 35 | .addPositionalParam("admin") 36 | .addPositionalParam("manager") 37 | .addPositionalParam("tokenHub") 38 | .addPositionalParam("bcDepositWallet") 39 | .addPositionalParam("bot") 40 | .addPositionalParam("feeBps") 41 | .setAction( 42 | async ( 43 | { bnbX, admin, manager, tokenHub, bcDepositWallet, bot, feeBps }, 44 | hre: HardhatRuntimeEnvironment 45 | ) => { 46 | await deployProxy( 47 | hre, 48 | "StakeManager", 49 | bnbX, 50 | admin, 51 | manager, 52 | tokenHub, 53 | bcDepositWallet, 54 | bot, 55 | feeBps 56 | ); 57 | } 58 | ); 59 | 60 | task("upgradeStakeManagerProxy", "Upgrade StakeManager Proxy") 61 | .addPositionalParam("proxyAddress") 62 | .setAction(async ({ proxyAddress }, hre: HardhatRuntimeEnvironment) => { 63 | await upgradeProxy(hre, "StakeManager", proxyAddress); 64 | }); 65 | 66 | task( 67 | "deployStakeManagerImpl", 68 | "Deploy StakeManager Implementation only" 69 | ).setAction(async (args, hre: HardhatRuntimeEnvironment) => { 70 | await deployDirect(hre, "StakeManager"); 71 | }); 72 | 73 | task("deployReferralContract", "Deploy KOL Referral Contract") 74 | .addPositionalParam("admin") 75 | .addPositionalParam("trustedForwarder") 76 | .setAction( 77 | async ({ admin, trustedForwarder }, hre: HardhatRuntimeEnvironment) => { 78 | await deployProxy(hre, "KOLReferral", admin, trustedForwarder); 79 | } 80 | ); 81 | 82 | task("upgradeReferralContract", "Upgrade KOL Referral Contract") 83 | .addPositionalParam("proxyAddress") 84 | .setAction(async ({ proxyAddress }, hre: HardhatRuntimeEnvironment) => { 85 | await upgradeProxy(hre, "KOLReferral", proxyAddress); 86 | }); 87 | 88 | const config: HardhatUserConfig = { 89 | solidity: { 90 | compilers: [ 91 | { 92 | version: "0.8.25", 93 | settings: { 94 | optimizer: { 95 | enabled: true, 96 | runs: 200, 97 | }, 98 | evmVersion: "cancun", 99 | outputSelection: { 100 | "*": { 101 | "*": ["storageLayout"], 102 | }, 103 | }, 104 | }, 105 | }, 106 | ], 107 | }, 108 | defaultNetwork: "hardhat", 109 | networks: { 110 | mainnet: { 111 | url: `${process.env.BSC_MAINNET_RPC_URL}`, 112 | chainId: 56, 113 | accounts: [process.env.DEV_PRIVATE_KEY ?? ""], 114 | }, 115 | testnet: { 116 | url: `${process.env.BSC_TESTNET_RPC_URL}`, 117 | chainId: 97, 118 | accounts: [process.env.DEV_PRIVATE_KEY ?? ""], 119 | }, 120 | }, 121 | etherscan: { 122 | apiKey: process.env.BSC_SCAN_API_KEY, 123 | }, 124 | }; 125 | 126 | export default config; 127 | -------------------------------------------------------------------------------- /legacy/INTEGRATION.md: -------------------------------------------------------------------------------- 1 | # Integration guide 2 | 3 | Liquid staking is achieved through `StakeManager` contract and the yield-bearing ERC-20 token `BnbX` is given to the user. 4 | 5 | ## 1. Stake BNB 6 | 7 | Send BNB and receive liquid staking BnbX token. 8 | 9 | ```SOLIDITY 10 | IStakeManager stakeManager = IStakeManager(STAKE_MANAGER_ADDRESS); 11 | IStakeManager.deposit{value: msg.value}(); 12 | uint256 amountInBnbX = IBnbX(BNBX_ADDRESS).balanceOf(msg.sender); 13 | 14 | emit StakeEvent(msg.sender, msg.value, amountInBnbX); 15 | ``` 16 | 17 | ## 2. Unstake BNB 18 | 19 | Send BnbX and create a withdrawal request. 20 | _BnbX approval should be given._ 21 | 22 | ```SOLIDITY 23 | require( 24 | IBnbX(BNBX_ADDRESS).approve(STAKE_MANAGER_ADDRESS, amount), 25 | "Not approved" 26 | ); 27 | IStakeManager stakeManager = IStakeManager(STAKE_MANAGER_ADDRESS); 28 | IStakeManager.requestWithdraw(amount); 29 | 30 | emit UnstakeEvent(msg.sender, amount); 31 | ``` 32 | 33 | ## 3. Claim BNB 34 | 35 | After 7-15 days, BNB can be withdrawn. 36 | 37 | ```SOLIDITY 38 | IStakeManager stakeManager = IStakeManager(STAKE_MANAGER_ADDRESS); 39 | (bool isClaimable, uint256 amount) = getUserRequestStatus( 40 | msg.sender, 41 | _idx 42 | ); 43 | require(isClaimable, "Not ready yet"); 44 | 45 | IStakeManager.claimWithdraw(_idx); 46 | uint256 amount = address(msg.sender).balance; 47 | 48 | emit ClaimEvent(msg.sender, amount); 49 | ``` 50 | 51 | ## Full example: 52 | 53 | ```SOLIDITY 54 | pragma solidity ^0.8.0; 55 | 56 | import "IBnbX.sol"; 57 | import "IStakeManager.sol"; 58 | 59 | contract Example { 60 | event StakeEvent( 61 | address indexed _address, 62 | uint256 amountInBnb, 63 | uint256 amountInBnbX 64 | ); 65 | event UnstakeEvent(address indexed _address, uint256 amountInBnbX); 66 | event ClaimEvent(address indexed _address, uint256 amountInBnb); 67 | 68 | address private STAKE_MANAGER_ADDRESS = 69 | "0x7276241a669489E4BBB76f63d2A43Bfe63080F2"; //mainnet address 70 | address private BNBX_ADDRESS = "0x1bdd3Cf7F79cfB8EdbB955f20ad99211551BA275"; //mainnet address 71 | 72 | function stake() external payable { 73 | IStakeManager stakeManager = IStakeManager(STAKE_MANAGER_ADDRESS); 74 | IStakeManager.deposit{value: msg.value}(); 75 | uint256 amountInBnbX = IBnbX(BNBX_ADDRESS).balanceOf(msg.sender); 76 | 77 | emit StakeEvent(msg.sender, msg.value, amountInBnbX); 78 | } 79 | 80 | function unstake(uint256 _amount) external { 81 | require( 82 | IBnbX(BNBX_ADDRESS).approve(STAKE_MANAGER_ADDRESS, _amount), 83 | "Not approved" 84 | ); 85 | IStakeManager stakeManager = IStakeManager(STAKE_MANAGER_ADDRESS); 86 | IStakeManager.requestWithdraw(_amount); 87 | 88 | emit UnstakeEvent(msg.sender, _amount); 89 | } 90 | 91 | function claim(uint256 _idx) external { 92 | IStakeManager stakeManager = IStakeManager(STAKE_MANAGER_ADDRESS); 93 | (bool isClaimable, uint256 amount) = getUserRequestStatus( 94 | msg.sender, 95 | _idx 96 | ); 97 | require(isClaimable, "Not ready yet"); 98 | 99 | IStakeManager.claimWithdraw(_idx); 100 | uint256 amount = address(msg.sender).balance; 101 | 102 | emit ClaimEvent(msg.sender, amount); 103 | } 104 | } 105 | 106 | ``` 107 | -------------------------------------------------------------------------------- /legacy/legacy-addresses/alpha-deployment-info.json: -------------------------------------------------------------------------------- 1 | { 2 | "proxy_admin": "0xa4958E3AB09D9Ac364c6C9193e54D63ca583face", 3 | "bnbX_proxy": "0x996aD83BB72Ef0F9b925e2ba41F3e6516C0A0329", 4 | "bnbX_impl": "0xf8a85887907f804776Ea9764781bF3b8cd59Cf6F", 5 | "stake_manager_proxy": "0xcEB338ea4D23822416C70036698D2712e2BD6971", 6 | "stake_manager_impl": "0xea46028037638cB44cb4C17985Ba7B21AF0AFbD9", 7 | "admin": "0x79A2Ae748AC8bE4118B7a8096681B30310c3adBE", 8 | "manager": "0x79A2Ae748AC8bE4118B7a8096681B30310c3adBE", 9 | "bcDepositWallet": "0xb0b7dcc2f574d59b7a983e531a8caf5ecf1e207e", 10 | "bot": "0xEc60396BCBC207478AD65dEc4788cEbA5470C235", 11 | "BSC_CLAIM_WALLET": "0xcc8916C3091597cAb4cDc94e69c1f49ba864A96B" 12 | } 13 | -------------------------------------------------------------------------------- /legacy/legacy-addresses/kol-referral-info.json: -------------------------------------------------------------------------------- 1 | { 2 | "testnet": { 3 | "contract": "0x50C15cb832dcBFB9ee4b95d101608b664F06D7f7", 4 | "admin": "0x8e39FBBA48014E8a36b36dad183d2A00E9c750cC", 5 | "trustedForwarder": "0x61456BF1715C1415730076BB79ae118E806E74d2" 6 | }, 7 | "beta": { 8 | "contract": "0x3629787a59418732B388ED398928c5Bb9f4E74f1", 9 | "proxyAdmin": "0xcb507d421540f1ec1b8adcaf81995c7c89f4213e", 10 | "admin": "0x8e39FBBA48014E8a36b36dad183d2A00E9c750cC", 11 | "trustedForwarder": "0x86C80a8aa58e0A4fa09A69624c31Ab2a6CAD56b8" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /legacy/legacy-addresses/mainnet-deployment-info.json: -------------------------------------------------------------------------------- 1 | { 2 | "proxy_admin": "0xF90e293D34a42CB592Be6BE6CA19A9963655673C", 3 | "bnbX_proxy": "0x1bdd3Cf7F79cfB8EdbB955f20ad99211551BA275", 4 | "bnbX_impl": "0x7422bf8E583eBeFbe05664D1EB75f06160D9E43F", 5 | "stake_manager_proxy": "0x7276241a669489E4BBB76f63d2A43Bfe63080F2F", 6 | "stake_manager_impl": "0xbb394206d8f19942687EF9CDB162A785c24aAe0e", 7 | "admin": "0x79A2Ae748AC8bE4118B7a8096681B30310c3adBE", 8 | "manager": "0x79A2Ae748AC8bE4118B7a8096681B30310c3adBE", 9 | "bcDepositWallet": "0x539582EBD552A59d16ED2a7E1B126B6b3ff3772D", 10 | "bot": "0x90199e1Bb29b3097BFe22231c00045b4f14B8C44", 11 | "BSC_CLAIM_WALLET": "0x49aFcdEF47c024246Cbef2031aFE66AF1A880B2f" 12 | } 13 | -------------------------------------------------------------------------------- /legacy/legacy-addresses/testnet-deployment-info.json: -------------------------------------------------------------------------------- 1 | { 2 | "proxy_admin": "0x1CC9855DDB4dB4AC7160C5f1EF9529D884d008Fe", 3 | "bnbX_proxy": "0x3ECB02c703C815e9cFFd8d9437B7A2F93638d7Cb", 4 | "bnbX_impl": "0x0d5700c51d9fe18Fa70EddA42BBBB2bc2Bc511Ce", 5 | "stake_manager_proxy": "0xDAdcae6bF110c0e70E5624bCdcCBe206f92A2Df9", 6 | "stake_manager_impl": "0x2d130414B2a782EA5EA0f3789b3b8F3a67c39b31", 7 | "manager": "0x8e39FBBA48014E8a36b36dad183d2A00E9c750cC", 8 | "bcDepositWallet": "0x17e3dc58fbc0a0a858cf90751a8d2ca45d4ff92a", 9 | "BSC_WALLET": "0xEc60396BCBC207478AD65dEc4788cEbA5470C235", 10 | "BSC_CLAIM_WALLET": "0xcc8916C3091597cAb4cDc94e69c1f49ba864A96B" 11 | } 12 | -------------------------------------------------------------------------------- /legacy/test/hardhat-tests/KOLReferral.spec.ts: -------------------------------------------------------------------------------- 1 | // import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; 2 | // import { expect } from "chai"; 3 | // import { ethers, upgrades } from "hardhat"; 4 | // import { KOLReferral } from "../../typechain-types"; 5 | 6 | // describe("KOL referral Contract", () => { 7 | // let admin: SignerWithAddress; 8 | // let kol1: SignerWithAddress; 9 | // let trustedForwarder: SignerWithAddress; 10 | // let users: SignerWithAddress[]; 11 | // let kolContract: KOLReferral; 12 | 13 | // beforeEach(async () => { 14 | // [admin, kol1, trustedForwarder, ...users] = await ethers.getSigners(); 15 | 16 | // kolContract = await upgrades.deployProxy( 17 | // await ethers.getContractFactory("KOLReferral"), 18 | // [admin.address, trustedForwarder.address] 19 | // ); 20 | // await kolContract.waitForDeployment(); 21 | // }); 22 | 23 | // it("register a kol", async () => { 24 | // let referralId1: string = "kol_1_ref_id"; 25 | 26 | // expect(await kolContract.walletToReferralId(kol1.address)).be.eq(""); 27 | // expect(await kolContract.referralIdToWallet(referralId1)).be.eq( 28 | // ethers.ZeroAddress 29 | // ); 30 | // expect(await kolContract.getKOLCount()).be.eq(0); 31 | 32 | // await kolContract.registerKOL(kol1.address, referralId1); 33 | // expect(await kolContract.walletToReferralId(kol1.address)).be.eq( 34 | // referralId1 35 | // ); 36 | // expect(await kolContract.referralIdToWallet(referralId1)).be.eq( 37 | // kol1.address 38 | // ); 39 | // expect(await kolContract.getKOLCount()).be.eq(1); 40 | // }); 41 | 42 | // it("store user info", async () => { 43 | // let referralId1: string = "kol_1_ref_id"; 44 | // await kolContract.registerKOL(kol1.address, referralId1); 45 | 46 | // expect(await kolContract.getUserCount()).to.be.eq(0); 47 | // expect(await kolContract.getKOLRefCount(kol1.address)).be.eq(0); 48 | 49 | // let u1kolContract = kolContract.connect(users[0]); 50 | // await u1kolContract.storeUserInfo(referralId1); 51 | 52 | // expect(await kolContract.queryUserReferrer(users[0].address)).to.be.eq( 53 | // kol1.address 54 | // ); 55 | // expect(await kolContract.getKOLRefCount(kol1.address)).be.eq(1); 56 | 57 | // const totalUsers = await kolContract.getUserCount(); 58 | // const { numUsers, userList } = await kolContract.getUsers(0, totalUsers); 59 | // expect(userList[0]).to.be.eq(users[0].address); 60 | // expect(numUsers).to.be.eq(1); 61 | // expect(numUsers).to.be.eq(totalUsers); 62 | 63 | // await expect( 64 | // kolContract.connect(kol1).storeUserInfo(referralId1) 65 | // ).be.revertedWith("kol not allowed as user"); 66 | // }); 67 | // }); 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bnbx", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "clean": "rm -rf cache out", 8 | "build": "forge build", 9 | "lint": "npm run lint:sol && npm run prettier:check", 10 | "lint:sol": "forge fmt --check && solhint {script,src,test}/**/*.sol", 11 | "prettier:check": "prettier --check **/*.{json,md,yml} --ignore-path=.prettierignore", 12 | "prettier:write": "prettier --write **/*.{json,md,yml} --ignore-path=.prettierignore", 13 | "test": "forge test" 14 | }, 15 | "devDependencies": { 16 | "@nomicfoundation/hardhat-foundry": "^1.1.2", 17 | "@nomicfoundation/hardhat-toolbox": "^5.0.0", 18 | "@opengsn/contracts": "^3.0.0-beta.1", 19 | "@openzeppelin/hardhat-upgrades": "^3.1.1", 20 | "dotenv": "^16.4.5", 21 | "hardhat": "^2.22.5", 22 | "solhint": "^5.0.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | ds-test/=lib/forge-std/lib/ds-test/src/ 2 | forge-std/=lib/forge-std/src/ 3 | @openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ 4 | @openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ 5 | -------------------------------------------------------------------------------- /script/foundry-scripts/migration/Migration.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.25; 3 | 4 | import "forge-std/Script.sol"; 5 | 6 | import { TransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; 7 | import { Create2 } from "@openzeppelin/contracts/utils/Create2.sol"; 8 | import { StakeManager } from "contracts/StakeManager.sol"; 9 | import { StakeManagerV2 } from "contracts/StakeManagerV2.sol"; 10 | import { OperatorRegistry } from "contracts/OperatorRegistry.sol"; 11 | 12 | contract Migration is Script { 13 | bytes32 public constant GENERIC_SALT = keccak256(abi.encodePacked("BNBX-MIGRATION")); 14 | 15 | address private proxyAdmin; 16 | address private admin; 17 | address private manager; 18 | address private staderOperator; 19 | address private treasury; 20 | address private devAddr; 21 | 22 | // address public STAKE_HUB = 0x0000000000000000000000000000000000002002; 23 | address private BNBx = 0x1bdd3Cf7F79cfB8EdbB955f20ad99211551BA275; 24 | 25 | StakeManagerV2 public stakeManagerV2; 26 | OperatorRegistry public operatorRegistry; 27 | 28 | function _createProxy(address impl) private returns (address proxy) { 29 | proxy = address(new TransparentUpgradeableProxy{ salt: GENERIC_SALT }(impl, proxyAdmin, "")); 30 | console2.log("proxy address: ", proxy); 31 | console2.log("impl address: ", impl); 32 | } 33 | 34 | /// @dev Computes the address of a proxy for the given implementation 35 | /// @param implementation the implementation to proxy 36 | /// @return proxyAddr the address of the created proxy 37 | function _computeAddress(address implementation) private view returns (address) { 38 | bytes memory creationCode = type(TransparentUpgradeableProxy).creationCode; 39 | bytes memory contractBytecode = abi.encodePacked(creationCode, abi.encode(implementation, proxyAdmin, "")); 40 | 41 | return Create2.computeAddress(GENERIC_SALT, keccak256(contractBytecode)); 42 | } 43 | 44 | function _deployAndSetupContracts() private { 45 | // deploy operator registry 46 | console2.log("deploying operator registry..."); 47 | operatorRegistry = OperatorRegistry(_createProxy(address(new OperatorRegistry()))); 48 | operatorRegistry.initialize(devAddr); 49 | 50 | // grant manager and operator role for operator registry 51 | console2.log("granting manager and operator role for operator registry..."); 52 | operatorRegistry.grantRole(operatorRegistry.MANAGER_ROLE(), manager); 53 | operatorRegistry.grantRole(operatorRegistry.OPERATOR_ROLE(), staderOperator); 54 | 55 | // TODO: done manually 56 | // // add preferred operator 57 | // address bscOperator = 0x343dA7Ff0446247ca47AA41e2A25c5Bbb230ED0A; 58 | // vm.prank(manager); 59 | // operatorRegistry.addOperator(bscOperator); 60 | // vm.prank(staderOperator); 61 | // operatorRegistry.setPreferredDepositOperator(bscOperator); 62 | 63 | // deploy stake manager v2 64 | console2.log("deploying stake manager v2..."); 65 | // stakeManagerV2 impl 66 | address stakeManagerV2Impl = address(new StakeManagerV2()); 67 | 68 | stakeManagerV2 = StakeManagerV2(payable(_createProxy(stakeManagerV2Impl))); 69 | stakeManagerV2.initialize(devAddr, address(operatorRegistry), BNBx, treasury); 70 | operatorRegistry.initialize2(address(stakeManagerV2)); 71 | 72 | // grant manager role for stake manager v2 73 | console2.log("granting manager and operator role for stake manager v2..."); 74 | stakeManagerV2.grantRole(stakeManagerV2.MANAGER_ROLE(), manager); 75 | stakeManagerV2.grantRole(stakeManagerV2.OPERATOR_ROLE(), staderOperator); 76 | 77 | // grant default admin role to admin 78 | console2.log("granting default admin role to admin for both contracts..."); 79 | stakeManagerV2.grantRole(stakeManagerV2.DEFAULT_ADMIN_ROLE(), admin); 80 | operatorRegistry.grantRole(operatorRegistry.DEFAULT_ADMIN_ROLE(), admin); 81 | 82 | // renounce DEFAULT_ADMIN_ROLE from devAddr 83 | console2.log("renouncing default admin role from devAddr..."); 84 | stakeManagerV2.renounceRole(stakeManagerV2.DEFAULT_ADMIN_ROLE(), devAddr); 85 | operatorRegistry.renounceRole(operatorRegistry.DEFAULT_ADMIN_ROLE(), devAddr); 86 | 87 | console2.log("deploying stake manager v1 impl..."); 88 | address stakeManagerV1Impl = address(new StakeManager()); 89 | console2.log("stakeManagerV1Impl: ", stakeManagerV1Impl); 90 | } 91 | 92 | function run() public { 93 | proxyAdmin = 0xF90e293D34a42CB592Be6BE6CA19A9963655673C; 94 | admin = 0x79A2Ae748AC8bE4118B7a8096681B30310c3adBE; // internal multisig 95 | manager = 0x79A2Ae748AC8bE4118B7a8096681B30310c3adBE; // internal multisig 96 | staderOperator = 0xDfB508E262B683EC52D533B80242Ae74087BC7EB; // previous claim wallet 97 | treasury = 0x01422247a1d15BB4FcF91F5A077Cf25BA6460130; // treasury 98 | devAddr = msg.sender; 99 | 100 | vm.startBroadcast(); // the executer will become msg.sender 101 | console.log("deploying contracts by: ", msg.sender); 102 | _deployAndSetupContracts(); 103 | verify(); 104 | vm.stopBroadcast(); 105 | // TODO: assert manually 106 | // // assert only admin has DEFAULT_ADMIN_ROLE in both the contracts 107 | // assertTrue(stakeManagerV2.hasRole(stakeManagerV2.DEFAULT_ADMIN_ROLE(), admin)); 108 | // assertTrue(operatorRegistry.hasRole(operatorRegistry.DEFAULT_ADMIN_ROLE(), admin)); 109 | 110 | // assertFalse(stakeManagerV2.hasRole(stakeManagerV2.DEFAULT_ADMIN_ROLE(), devAddr)); 111 | // assertFalse(operatorRegistry.hasRole(operatorRegistry.DEFAULT_ADMIN_ROLE(), devAddr)); 112 | } 113 | 114 | function verify() public view { 115 | require( 116 | stakeManagerV2.hasRole(stakeManagerV2.DEFAULT_ADMIN_ROLE(), admin), 117 | "stakeManagerV2 has no DEFAULT_ADMIN_ROLE" 118 | ); 119 | require(stakeManagerV2.hasRole(stakeManagerV2.MANAGER_ROLE(), manager), "stakeManagerV2 has no MANAGER_ROLE"); 120 | require( 121 | stakeManagerV2.hasRole(stakeManagerV2.OPERATOR_ROLE(), staderOperator), 122 | "stakeManagerV2 has no OPERATOR_ROLE" 123 | ); 124 | 125 | require( 126 | operatorRegistry.hasRole(operatorRegistry.DEFAULT_ADMIN_ROLE(), admin), 127 | "operatorRegistry has no DEFAULT_ADMIN_ROLE" 128 | ); 129 | require( 130 | operatorRegistry.hasRole(operatorRegistry.MANAGER_ROLE(), manager), "operatorRegistry has no MANAGER_ROLE" 131 | ); 132 | require( 133 | operatorRegistry.hasRole(operatorRegistry.OPERATOR_ROLE(), staderOperator), 134 | "operatorRegistry has no OPERATOR_ROLE" 135 | ); 136 | 137 | require( 138 | !stakeManagerV2.hasRole(stakeManagerV2.DEFAULT_ADMIN_ROLE(), devAddr), 139 | "stakeManagerV2 has DEFAULT_ADMIN_ROLE from devAddr" 140 | ); 141 | require( 142 | !operatorRegistry.hasRole(operatorRegistry.DEFAULT_ADMIN_ROLE(), devAddr), 143 | "operatorRegistry has DEFAULT_ADMIN_ROLE from devAddr" 144 | ); 145 | 146 | require(operatorRegistry.stakeManager() == address(stakeManagerV2), "operatorRegistry has wrong stakeManager"); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /script/hardhat-scripts/deploy.ts: -------------------------------------------------------------------------------- 1 | import { ethers, upgrades } from "hardhat"; 2 | 3 | async function main() { 4 | const stakeManagerContractFactory = await ethers.getContractFactory( 5 | "StakeManager" 6 | ); 7 | const stakeManagerContract = await stakeManagerContractFactory.deploy(); 8 | 9 | await stakeManagerContract.waitForDeployment(); 10 | 11 | console.log( 12 | "StakeManager Contract impl deployed to:", 13 | await stakeManagerContract.getAddress() 14 | ); 15 | } 16 | 17 | // We recommend this pattern to be able to use async/await everywhere 18 | // and properly handle errors. 19 | main().catch((error) => { 20 | console.error(error); 21 | process.exitCode = 1; 22 | }); 23 | -------------------------------------------------------------------------------- /script/hardhat-scripts/tasks.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 2 | 3 | export async function deployDirect( 4 | hre: HardhatRuntimeEnvironment, 5 | contractName: string, 6 | ...args: any 7 | ) { 8 | const contractFactory = await hre.ethers.getContractFactory(contractName); 9 | 10 | console.log(`Deploying ${contractName}: ${args}, ${args.length}`); 11 | let contract = args.length 12 | ? await contractFactory.deploy(...args) 13 | : await contractFactory.deploy(); 14 | 15 | contract = await contract.waitForDeployment(); 16 | 17 | console.log(`${contractName} deployed to:`, await contract.getAddress()); 18 | } 19 | 20 | export async function deployProxy( 21 | hre: HardhatRuntimeEnvironment, 22 | contractName: string, 23 | ...args: any 24 | ) { 25 | const contractFactory = await hre.ethers.getContractFactory(contractName); 26 | 27 | console.log(`Deploying proxy ${contractName}: ${args}, ${args.length}`); 28 | let contract = args.length 29 | ? await hre.upgrades.deployProxy(contractFactory, args) 30 | : await hre.upgrades.deployProxy(contractFactory); 31 | 32 | contract = await contract.waitForDeployment(); 33 | 34 | const contractImplAddress = 35 | await hre.upgrades.erc1967.getImplementationAddress( 36 | await contract.getAddress() 37 | ); 38 | 39 | console.log( 40 | `Proxy ${contractName} deployed to:`, 41 | await contract.getAddress() 42 | ); 43 | console.log(`Impl ${contractName} deployed to:`, contractImplAddress); 44 | } 45 | 46 | export async function upgradeProxy( 47 | hre: HardhatRuntimeEnvironment, 48 | contractName: string, 49 | proxyAddress: string 50 | ) { 51 | const contractFactory = await hre.ethers.getContractFactory(contractName); 52 | 53 | console.log(`Upgrading ${contractName} with proxy at: ${proxyAddress}`); 54 | 55 | let contract = await hre.upgrades.upgradeProxy(proxyAddress, contractFactory); 56 | contract = await contract.waitForDeployment(); 57 | 58 | const contractImplAddress = 59 | await hre.upgrades.erc1967.getImplementationAddress(proxyAddress); 60 | 61 | console.log( 62 | `Proxy ${contractName} deployed to:`, 63 | await contract.getAddress() 64 | ); 65 | console.log(`Impl ${contractName} deployed to:`, contractImplAddress); 66 | } 67 | -------------------------------------------------------------------------------- /test/fork-tests/OperatorRegistryTests.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.25; 3 | 4 | import "./StakeManagerV2Setup.t.sol"; 5 | import "contracts/interfaces/IStakeCredit.sol"; 6 | 7 | contract OperatorRegistryTests is StakeManagerV2Setup { 8 | uint256 minDelegateAmount; 9 | 10 | function setUp() public override { 11 | super.setUp(); 12 | minDelegateAmount = STAKE_HUB.minDelegationBNBChange(); 13 | } 14 | 15 | function test_revertsWhenReAddSameOperator() public { 16 | address oldOperator = operatorRegistry.preferredDepositOperator(); 17 | 18 | vm.expectRevert(IOperatorRegistry.OperatorExisted.selector); 19 | vm.prank(manager); 20 | operatorRegistry.addOperator(oldOperator); 21 | } 22 | 23 | function test_addRandomAddressAsOperator() public { 24 | address newOperator = makeAddr("invalid-operator"); 25 | 26 | vm.expectRevert(IOperatorRegistry.OperatorNotExisted.selector); 27 | vm.prank(manager); 28 | operatorRegistry.addOperator(newOperator); 29 | } 30 | 31 | function test_setInvalidOperatorAsPreferred() public { 32 | // below operator is not yet added 33 | address operator2 = 0xd34403249B2d82AAdDB14e778422c966265e5Fb5; 34 | assertFalse(operatorRegistry.operatorExists(operator2)); 35 | 36 | vm.expectRevert(IOperatorRegistry.OperatorNotExisted.selector); 37 | vm.prank(manager); 38 | operatorRegistry.setPreferredDepositOperator(operator2); 39 | 40 | vm.expectRevert(IOperatorRegistry.OperatorNotExisted.selector); 41 | vm.prank(manager); 42 | operatorRegistry.setPreferredWithdrawalOperator(operator2); 43 | } 44 | 45 | function test_addOperator() public { 46 | uint256 numOperatorsBefore = operatorRegistry.getOperatorsLength(); 47 | 48 | address operator2 = 0xd34403249B2d82AAdDB14e778422c966265e5Fb5; 49 | vm.prank(manager); 50 | operatorRegistry.addOperator(operator2); 51 | 52 | assertTrue(operatorRegistry.operatorExists(operator2)); 53 | assertEq(operatorRegistry.getOperatorsLength(), numOperatorsBefore + 1); 54 | 55 | address[] memory operatorList = operatorRegistry.getOperators(); 56 | assertEq(operatorList.length, numOperatorsBefore + 1); 57 | } 58 | 59 | function test_removePreferredOperator() public { 60 | address preferredDepositOperator = operatorRegistry.preferredDepositOperator(); 61 | 62 | vm.expectRevert(IOperatorRegistry.OperatorIsPreferredDeposit.selector); 63 | vm.prank(manager); 64 | operatorRegistry.removeOperator(preferredDepositOperator); 65 | 66 | address operator2 = 0xd34403249B2d82AAdDB14e778422c966265e5Fb5; 67 | vm.prank(manager); 68 | operatorRegistry.addOperator(operator2); 69 | 70 | // set operator2 as preferred withdraw operator 71 | vm.prank(staderOperator); 72 | operatorRegistry.setPreferredWithdrawalOperator(operator2); 73 | 74 | vm.expectRevert(IOperatorRegistry.OperatorIsPreferredWithdrawal.selector); 75 | vm.prank(manager); 76 | operatorRegistry.removeOperator(operator2); 77 | } 78 | 79 | function test_removeOperatorWhenSomeDustRemains() public { 80 | address oldWithdrawOperator = operatorRegistry.preferredWithdrawalOperator(); 81 | 82 | // new validator 83 | address newOperator = 0xd34403249B2d82AAdDB14e778422c966265e5Fb5; 84 | assertFalse(operatorRegistry.operatorExists(newOperator)); 85 | 86 | // add a new validator 87 | vm.prank(manager); 88 | operatorRegistry.addOperator(newOperator); 89 | 90 | vm.startPrank(staderOperator); 91 | operatorRegistry.setPreferredDepositOperator(newOperator); 92 | operatorRegistry.setPreferredWithdrawalOperator(newOperator); 93 | vm.stopPrank(); 94 | 95 | uint256 amount = 2 ether; 96 | hoax(user1, amount); 97 | uint256 bnbxMinted = stakeManagerV2.delegate{ value: amount }("referral"); 98 | 99 | // set other operator as preferred 100 | vm.startPrank(staderOperator); 101 | operatorRegistry.setPreferredDepositOperator(oldWithdrawOperator); 102 | operatorRegistry.setPreferredWithdrawalOperator(oldWithdrawOperator); 103 | vm.stopPrank(); 104 | 105 | uint256 bnbStakedAtOperator = 106 | IStakeCredit(STAKE_HUB.getValidatorCreditContract(newOperator)).getPooledBNB(address(stakeManagerV2)); 107 | console2.log("bnbStakedAtOperator:", bnbStakedAtOperator); 108 | 109 | vm.expectRevert(IOperatorRegistry.DelegationExists.selector); 110 | vm.prank(manager); 111 | operatorRegistry.removeOperator(newOperator); 112 | 113 | // user requests withdraw 114 | vm.startPrank(user1); 115 | BnbX(bnbxAddr).approve(address(stakeManagerV2), bnbxMinted); 116 | uint256 negligibleAmount = operatorRegistry.negligibleAmount(); 117 | stakeManagerV2.requestWithdraw(bnbxMinted - negligibleAmount / 2, ""); 118 | vm.stopPrank(); 119 | 120 | // unstake from operator 121 | vm.prank(staderOperator); 122 | stakeManagerV2.startBatchUndelegation(2, newOperator); 123 | 124 | // check and remove operator now 125 | bnbStakedAtOperator = 126 | IStakeCredit(STAKE_HUB.getValidatorCreditContract(newOperator)).getPooledBNB(address(stakeManagerV2)); 127 | console2.log("bnbStakedAtOperator:", bnbStakedAtOperator); 128 | 129 | uint256 tvlBefore = stakeManagerV2.getActualStakeAcrossAllOperators(); 130 | 131 | vm.prank(manager); 132 | operatorRegistry.removeOperator(newOperator); 133 | 134 | uint256 tvlAfter = stakeManagerV2.getActualStakeAcrossAllOperators(); 135 | assertApproxEqAbs(tvlBefore, tvlAfter, negligibleAmount + 100); 136 | } 137 | 138 | function test_removeOperatorWhenSomeDustRemainsAfterRedelegation() public { 139 | address oldOperator = operatorRegistry.preferredDepositOperator(); 140 | 141 | // new validator 142 | address newOperator = 0xd34403249B2d82AAdDB14e778422c966265e5Fb5; 143 | 144 | // add a new validator 145 | vm.prank(manager); 146 | operatorRegistry.addOperator(newOperator); 147 | 148 | vm.startPrank(staderOperator); 149 | operatorRegistry.setPreferredDepositOperator(newOperator); 150 | operatorRegistry.setPreferredWithdrawalOperator(newOperator); 151 | vm.stopPrank(); 152 | 153 | uint256 amount = 2 ether; 154 | hoax(user1, amount); 155 | stakeManagerV2.delegate{ value: amount }("referral"); 156 | 157 | // set other operator as preferred 158 | vm.startPrank(staderOperator); 159 | operatorRegistry.setPreferredDepositOperator(oldOperator); 160 | operatorRegistry.setPreferredWithdrawalOperator(oldOperator); 161 | vm.stopPrank(); 162 | 163 | uint256 bnbStakedAtOperator = 164 | IStakeCredit(STAKE_HUB.getValidatorCreditContract(newOperator)).getPooledBNB(address(stakeManagerV2)); 165 | console2.log("bnbStakedAtOperator:", bnbStakedAtOperator); 166 | 167 | vm.expectRevert(IOperatorRegistry.DelegationExists.selector); 168 | vm.prank(manager); 169 | operatorRegistry.removeOperator(newOperator); 170 | 171 | uint256 negligibleAmount = operatorRegistry.negligibleAmount(); 172 | 173 | vm.prank(manager); 174 | stakeManagerV2.redelegate(newOperator, oldOperator, bnbStakedAtOperator - negligibleAmount + 1); 175 | 176 | // check and remove operator now 177 | bnbStakedAtOperator = 178 | IStakeCredit(STAKE_HUB.getValidatorCreditContract(newOperator)).getPooledBNB(address(stakeManagerV2)); 179 | console2.log("bnbStakedAtOperator:", bnbStakedAtOperator); 180 | 181 | uint256 tvlBefore = stakeManagerV2.getActualStakeAcrossAllOperators(); 182 | 183 | vm.prank(manager); 184 | operatorRegistry.removeOperator(newOperator); 185 | 186 | uint256 tvlAfter = stakeManagerV2.getActualStakeAcrossAllOperators(); 187 | assertApproxEqAbs(tvlBefore, tvlAfter, negligibleAmount + 100); 188 | } 189 | 190 | function test_setNegligibleAmount() public { 191 | vm.expectRevert(IOperatorRegistry.NegligibleAmountTooHigh.selector); 192 | vm.startPrank(manager); 193 | operatorRegistry.setNegligibleAmount(1e15 + 1); 194 | 195 | operatorRegistry.setNegligibleAmount(1e8); 196 | assertEq(operatorRegistry.negligibleAmount(), 1e8); 197 | vm.stopPrank(); 198 | } 199 | 200 | function test_pause_unpause() public { 201 | vm.prank(manager); 202 | operatorRegistry.pause(); 203 | assertTrue(operatorRegistry.paused()); 204 | 205 | vm.expectRevert(); 206 | vm.prank(staderOperator); 207 | operatorRegistry.unpause(); 208 | 209 | vm.prank(admin); 210 | operatorRegistry.unpause(); 211 | assertFalse(operatorRegistry.paused()); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /test/fork-tests/StakeManagerV2BasicChecks.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.25; 3 | 4 | import "./StakeManagerV2Setup.t.sol"; 5 | 6 | contract StakeManagerV2BasicChecks is StakeManagerV2Setup { 7 | function setUp() public override { 8 | super.setUp(); 9 | } 10 | 11 | function test_basicChecks() public view { 12 | assertNotEq(stakeManagerV2.staderTreasury(), address(0)); 13 | assertNotEq(stakeManagerV2.maxExchangeRateSlippageBps(), 0); 14 | assertEq(address(stakeManagerV2.BNBX()), bnbxAddr); 15 | assertGt(stakeManagerV2.convertBnbXToBnb(1 ether), 1 ether); 16 | assertGt(stakeManagerV2.maxActiveRequestsPerUser(), 0); 17 | } 18 | 19 | function test_updateER() public { 20 | uint256 totalDelegated1 = stakeManagerV2.totalDelegated(); 21 | uint256 treasuryBNBxBal1 = _bnbxBalance(treasury); 22 | stakeManagerV2.updateER(); 23 | uint256 totalDelegated2 = stakeManagerV2.totalDelegated(); 24 | uint256 treasuryBNBxBal2 = _bnbxBalance(treasury); 25 | 26 | if (totalDelegated2 <= totalDelegated1) { 27 | assertApproxEqAbs(totalDelegated1, totalDelegated2, 5); 28 | assertEq(treasuryBNBxBal1, treasuryBNBxBal2); 29 | } else { 30 | assertGt(treasuryBNBxBal2, treasuryBNBxBal1); 31 | } 32 | } 33 | 34 | function test_setStaderTreasury() public { 35 | vm.startPrank(admin); 36 | address newTreasury = makeAddr("new-treasury"); 37 | stakeManagerV2.setStaderTreasury(newTreasury); 38 | } 39 | 40 | function test_setFeeBps() public { 41 | vm.startPrank(admin); 42 | uint256 newFeeBps = 100; 43 | stakeManagerV2.setFeeBps(newFeeBps); 44 | assertEq(stakeManagerV2.feeBps(), newFeeBps); 45 | 46 | vm.expectRevert(IStakeManagerV2.MaxLimitReached.selector); 47 | stakeManagerV2.setFeeBps(5001); 48 | } 49 | 50 | function test_setMaxActiveRequestsPerUser() public { 51 | vm.startPrank(admin); 52 | uint256 newMaxActiveRequestsPerUser = 100; 53 | stakeManagerV2.setMaxActiveRequestsPerUser(newMaxActiveRequestsPerUser); 54 | assertEq(stakeManagerV2.maxActiveRequestsPerUser(), newMaxActiveRequestsPerUser); 55 | } 56 | 57 | function test_setMaxExchangeRateSlippageBps() public { 58 | vm.startPrank(admin); 59 | uint256 newMaxExchangeRateSlippageBps = 100; 60 | stakeManagerV2.setMaxExchangeRateSlippageBps(newMaxExchangeRateSlippageBps); 61 | assertEq(stakeManagerV2.maxExchangeRateSlippageBps(), newMaxExchangeRateSlippageBps); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /test/fork-tests/StakeManagerV2Delegations.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.25; 3 | 4 | import "./StakeManagerV2Setup.t.sol"; 5 | import { IStakeCredit } from "contracts/interfaces/IStakeCredit.sol"; 6 | 7 | contract StakeManagerV2Delegations is StakeManagerV2Setup { 8 | address preferredDepositOperator; 9 | uint256 minDelegateAmount; 10 | 11 | function setUp() public override { 12 | super.setUp(); 13 | minDelegateAmount = STAKE_HUB.minDelegationBNBChange(); 14 | preferredDepositOperator = operatorRegistry.preferredDepositOperator(); 15 | } 16 | 17 | function testFuzz_revertWhenUserAmountLessThanMinDelegation(uint256 amountInBnb) public { 18 | vm.assume(amountInBnb < minDelegateAmount); 19 | 20 | vm.expectRevert(); 21 | hoax(user1, amountInBnb); 22 | stakeManagerV2.delegate{ value: amountInBnb }("referral"); 23 | } 24 | 25 | function testFuzz_userDeposit(uint256 amountInBnb) public { 26 | vm.assume(amountInBnb >= minDelegateAmount); 27 | vm.assume(amountInBnb < 1e35); 28 | 29 | address creditContract = STAKE_HUB.getValidatorCreditContract(preferredDepositOperator); 30 | uint256 validatorBalanceBefore = IStakeCredit(creditContract).getPooledBNB(address(stakeManagerV2)); 31 | uint256 expectedBnbxAmount = stakeManagerV2.convertBnbToBnbX(amountInBnb); 32 | 33 | hoax(user1, amountInBnb); 34 | uint256 bnbxMinted = stakeManagerV2.delegate{ value: amountInBnb }("referral"); 35 | 36 | assertEq(BnbX(bnbxAddr).balanceOf(user1), expectedBnbxAmount); 37 | assertEq(bnbxMinted, expectedBnbxAmount); 38 | 39 | uint256 validatorBalanceAfter = IStakeCredit(creditContract).getPooledBNB(address(stakeManagerV2)); 40 | assertApproxEqAbs(validatorBalanceAfter, validatorBalanceBefore + amountInBnb, 2); 41 | } 42 | 43 | function testFuzz_redelegation(uint256 amount, uint256 redelegationAmount) public { 44 | vm.assume(amount < 1e35); 45 | vm.assume(amount >= minDelegateAmount); 46 | vm.assume(redelegationAmount >= minDelegateAmount); 47 | vm.assume(redelegationAmount <= amount); 48 | 49 | // new validator 50 | address toOperator = 0xd34403249B2d82AAdDB14e778422c966265e5Fb5; 51 | 52 | // add a new validator 53 | vm.prank(manager); 54 | operatorRegistry.addOperator(toOperator); 55 | 56 | hoax(user1, amount); 57 | stakeManagerV2.delegate{ value: amount }("referral"); 58 | 59 | uint256 totalStakedBnb = stakeManagerV2.getActualStakeAcrossAllOperators(); 60 | vm.prank(manager); 61 | stakeManagerV2.redelegate(preferredDepositOperator, toOperator, redelegationAmount); 62 | 63 | uint256 totalStakedBnbAfter = stakeManagerV2.getActualStakeAcrossAllOperators(); 64 | assertApproxEqAbs( 65 | totalStakedBnbAfter, totalStakedBnb - stakeManagerV2.getRedelegationFee(redelegationAmount), 100 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /test/fork-tests/StakeManagerV2EdgeCases.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.25; 3 | 4 | import "./StakeManagerV2Setup.t.sol"; 5 | import "contracts/interfaces/IStakeCredit.sol"; 6 | 7 | contract StakeManagerV2EdgeCases is StakeManagerV2Setup { 8 | uint256 minDelegateAmount; 9 | 10 | function setUp() public override { 11 | super.setUp(); 12 | minDelegateAmount = STAKE_HUB.minDelegationBNBChange(); 13 | } 14 | 15 | function test_redelegationAndUnstakeFromSameOperator(uint256 amount) public { 16 | vm.assume(amount < 1e35); 17 | vm.assume(amount >= minDelegateAmount + 100); 18 | 19 | address oldOperator = operatorRegistry.preferredDepositOperator(); 20 | 21 | // new validator 22 | address toOperator = 0xd34403249B2d82AAdDB14e778422c966265e5Fb5; 23 | 24 | // add a new validator 25 | vm.prank(manager); 26 | operatorRegistry.addOperator(toOperator); 27 | 28 | vm.prank(staderOperator); 29 | operatorRegistry.setPreferredDepositOperator(toOperator); 30 | 31 | hoax(user1, amount); 32 | stakeManagerV2.delegate{ value: amount }("referral"); 33 | 34 | // redelegate all amount 35 | vm.prank(manager); 36 | stakeManagerV2.redelegate(toOperator, oldOperator, amount); 37 | 38 | uint256 amountToWithdraw = amount / 2; 39 | 40 | // user requests withdraw 41 | vm.startPrank(user1); 42 | BnbX(bnbxAddr).approve(address(stakeManagerV2), amount); 43 | stakeManagerV2.requestWithdraw(amount / 2, ""); 44 | vm.stopPrank(); 45 | 46 | // lets try unstaking from the same toOperator (which does not have any funds delegated by us) 47 | vm.expectRevert(IStakeManagerV2.NoWithdrawalRequests.selector); 48 | vm.prank(staderOperator); 49 | stakeManagerV2.startBatchUndelegation(amountToWithdraw, toOperator); 50 | } 51 | 52 | function test_updateER_AfterExtraDelegation() public { 53 | // initial update ER 54 | stakeManagerV2.updateER(); 55 | 56 | // increase the exchange rate by a little bit 57 | hoax(manager, 10 ether); 58 | stakeManagerV2.delegateWithoutMinting{ value: 10 ether }(); 59 | 60 | uint256 totalDelegated2 = stakeManagerV2.totalDelegated(); 61 | uint256 treasuryBNBxBal2 = _bnbxBalance(treasury); 62 | stakeManagerV2.updateER(); 63 | uint256 totalDelegated3 = stakeManagerV2.totalDelegated(); 64 | uint256 treasuryBNBxBal3 = _bnbxBalance(treasury); 65 | 66 | assertApproxEqAbs(totalDelegated3, totalDelegated2, 5); 67 | // no bnbx fees is minted in this case 68 | assertEq(treasuryBNBxBal3, treasuryBNBxBal2); 69 | } 70 | 71 | function test_updateER_whenRewardsEnters() public { 72 | uint256 totalDelegated2 = stakeManagerV2.totalDelegated(); 73 | uint256 treasuryBNBxBal2 = _bnbxBalance(treasury); 74 | 75 | // add mock rewards 76 | address operator1 = operatorRegistry.preferredDepositOperator(); 77 | address creditContract = STAKE_HUB.getValidatorCreditContract(operator1); 78 | uint256 pooledBNBAtOperator = IStakeCredit(creditContract).getPooledBNB(address(stakeManagerV2)); 79 | vm.mockCall( 80 | creditContract, 81 | abi.encodeWithSelector(IStakeCredit.getPooledBNB.selector), 82 | abi.encode(pooledBNBAtOperator + 10 ether) 83 | ); 84 | 85 | stakeManagerV2.updateER(); 86 | uint256 totalDelegated3 = stakeManagerV2.totalDelegated(); 87 | uint256 treasuryBNBxBal3 = _bnbxBalance(treasury); 88 | 89 | // total delegated increases and treasury is minted bnbx 90 | assertGt(totalDelegated3, totalDelegated2); 91 | assertGt(treasuryBNBxBal3, treasuryBNBxBal2); 92 | } 93 | 94 | function test_forceUpdateER_whenVeryHighRewards() public { 95 | uint256 totalDelegated2 = stakeManagerV2.totalDelegated(); 96 | uint256 treasuryBNBxBal2 = _bnbxBalance(treasury); 97 | 98 | // add mock rewards 99 | address operator1 = operatorRegistry.preferredDepositOperator(); 100 | address creditContract = STAKE_HUB.getValidatorCreditContract(operator1); 101 | uint256 pooledBNBAtOperator = IStakeCredit(creditContract).getPooledBNB(address(stakeManagerV2)); 102 | vm.mockCall( 103 | creditContract, 104 | abi.encodeWithSelector(IStakeCredit.getPooledBNB.selector), 105 | abi.encode(pooledBNBAtOperator + 100_000 ether) // high rewards 106 | ); 107 | 108 | vm.expectRevert(); // ExchangeRateOutOfBounds 109 | stakeManagerV2.updateER(); 110 | 111 | vm.prank(admin); 112 | stakeManagerV2.forceUpdateER(); 113 | uint256 totalDelegated3 = stakeManagerV2.totalDelegated(); 114 | uint256 treasuryBNBxBal3 = _bnbxBalance(treasury); 115 | 116 | // total delegated increases and treasury is minted bnbx 117 | assertGt(totalDelegated3, totalDelegated2); 118 | assertGt(treasuryBNBxBal3, treasuryBNBxBal2); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /test/fork-tests/StakeManagerV2Setup.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.25; 3 | 4 | import "forge-std/Test.sol"; 5 | 6 | import { ProxyAdmin } from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; 7 | import { Create2 } from "@openzeppelin/contracts/utils/Create2.sol"; 8 | import { 9 | TransparentUpgradeableProxy, 10 | ITransparentUpgradeableProxy 11 | } from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; 12 | 13 | import { StakeManager } from "contracts/StakeManager.sol"; 14 | import { BnbX } from "contracts/BnbX.sol"; 15 | import "contracts/StakeManagerV2.sol"; 16 | import { OperatorRegistry, IOperatorRegistry } from "contracts/OperatorRegistry.sol"; 17 | import { IStakeHub } from "contracts/interfaces/IStakeHub.sol"; 18 | 19 | contract StakeManagerV2Setup is Test { 20 | bytes32 public constant GENERIC_SALT = keccak256(abi.encodePacked("BNBX-MIGRATION")); 21 | 22 | address public proxyAdmin; 23 | address public timelock; 24 | address public admin; 25 | address public manager; 26 | address public staderOperator; 27 | address public treasury; 28 | address public devAddr; 29 | address public user1; 30 | address public user2; 31 | 32 | IStakeHub public STAKE_HUB = IStakeHub(0x0000000000000000000000000000000000002002); 33 | address public bnbxAddr = 0x1bdd3Cf7F79cfB8EdbB955f20ad99211551BA275; 34 | 35 | StakeManager public stakeManagerV1; 36 | StakeManagerV2 public stakeManagerV2; 37 | OperatorRegistry public operatorRegistry; 38 | 39 | function setUp() public virtual { 40 | string memory rpcUrl = vm.envString("BSC_MAINNET_RPC_URL"); 41 | vm.createSelectFork(rpcUrl); 42 | 43 | _initialiseAddresses(); 44 | 45 | _clearCurrentPendingTransactions(); 46 | } 47 | 48 | // ----------------------------------HELPERS-------------------------------- // 49 | 50 | function _initialiseAddresses() private { 51 | // TODO: update below addresses with correct addresses once on mainnet 52 | proxyAdmin = 0xF90e293D34a42CB592Be6BE6CA19A9963655673C; 53 | timelock = 0xD990A252E7e36700d47520e46cD2B3E446836488; 54 | admin = 0x79A2Ae748AC8bE4118B7a8096681B30310c3adBE; // internal multisig 55 | manager = 0x79A2Ae748AC8bE4118B7a8096681B30310c3adBE; // internal multisig 56 | staderOperator = 0xDfB508E262B683EC52D533B80242Ae74087BC7EB; 57 | treasury = 0x01422247a1d15BB4FcF91F5A077Cf25BA6460130; 58 | 59 | devAddr = address(this); // may change it to your own address 60 | stakeManagerV1 = StakeManager(payable(0x7276241a669489E4BBB76f63d2A43Bfe63080F2F)); 61 | stakeManagerV2 = StakeManagerV2(payable(0x3b961e83400D51e6E1AF5c450d3C7d7b80588d28)); 62 | operatorRegistry = OperatorRegistry(0x9C1759359Aa7D32911c5bAD613E836aEd7c621a8); 63 | 64 | user1 = makeAddr("user1"); 65 | user2 = makeAddr("user2"); 66 | } 67 | 68 | function _clearCurrentPendingTransactions() private { 69 | // clear current withdrawals from preferred current withdrawal operator 70 | address oldWithdrawOperator = operatorRegistry.preferredWithdrawalOperator(); 71 | uint256 numWithdrawRequests = stakeManagerV2.getUnprocessedWithdrawalRequestCount(); 72 | vm.prank(staderOperator); 73 | stakeManagerV2.startBatchUndelegation(numWithdrawRequests, oldWithdrawOperator); 74 | 75 | skip(8 days); 76 | 77 | uint256 numUnbondingBatches = 78 | stakeManagerV2.getBatchWithdrawalRequestCount() - stakeManagerV2.firstUnbondingBatchIndex(); 79 | while (numUnbondingBatches > 0) { 80 | stakeManagerV2.completeBatchUndelegation(); 81 | numUnbondingBatches--; 82 | } 83 | } 84 | 85 | function _upgradeAndSetupContracts() private { 86 | address stakeManagerV1Impl = address(new StakeManager()); 87 | 88 | vm.prank(timelock); 89 | ProxyAdmin(proxyAdmin).upgrade(ITransparentUpgradeableProxy(address(stakeManagerV1)), stakeManagerV1Impl); 90 | } 91 | 92 | function _deployAndSetupContracts() private { 93 | // stakeManagerV2 impl 94 | address stakeManagerV2Impl = address(new StakeManagerV2()); 95 | 96 | // deploy operator registry 97 | operatorRegistry = OperatorRegistry(_createProxy(address(new OperatorRegistry()))); 98 | operatorRegistry.initialize(devAddr); 99 | 100 | // grant manager and operator role for operator registry 101 | vm.startPrank(devAddr); 102 | operatorRegistry.grantRole(operatorRegistry.MANAGER_ROLE(), manager); 103 | operatorRegistry.grantRole(operatorRegistry.OPERATOR_ROLE(), staderOperator); 104 | vm.stopPrank(); 105 | 106 | // add preferred operator 107 | address bscOperator = 0x343dA7Ff0446247ca47AA41e2A25c5Bbb230ED0A; 108 | vm.prank(manager); 109 | operatorRegistry.addOperator(bscOperator); 110 | vm.startPrank(staderOperator); 111 | operatorRegistry.setPreferredDepositOperator(bscOperator); 112 | operatorRegistry.setPreferredWithdrawalOperator(bscOperator); 113 | vm.stopPrank(); 114 | 115 | // deploy stake manager v2 116 | stakeManagerV2 = StakeManagerV2(payable(_createProxy(stakeManagerV2Impl))); 117 | stakeManagerV2.initialize(devAddr, address(operatorRegistry), bnbxAddr, treasury); 118 | operatorRegistry.initialize2(address(stakeManagerV2)); 119 | assertEq(address(stakeManagerV2), operatorRegistry.stakeManager()); 120 | 121 | vm.startPrank(devAddr); 122 | // grant manager role for stake manager v2 123 | stakeManagerV2.grantRole(stakeManagerV2.MANAGER_ROLE(), manager); 124 | stakeManagerV2.grantRole(stakeManagerV2.OPERATOR_ROLE(), staderOperator); 125 | 126 | // grant default admin role to admin 127 | stakeManagerV2.grantRole(stakeManagerV2.DEFAULT_ADMIN_ROLE(), admin); 128 | operatorRegistry.grantRole(operatorRegistry.DEFAULT_ADMIN_ROLE(), admin); 129 | 130 | // renounce DEFAULT_ADMIN_ROLE from devAddr 131 | stakeManagerV2.renounceRole(stakeManagerV2.DEFAULT_ADMIN_ROLE(), devAddr); 132 | operatorRegistry.renounceRole(operatorRegistry.DEFAULT_ADMIN_ROLE(), devAddr); 133 | 134 | vm.stopPrank(); 135 | 136 | // assert only admin has DEFAULT_ADMIN_ROLE in both the contracts 137 | assertTrue(stakeManagerV2.hasRole(stakeManagerV2.DEFAULT_ADMIN_ROLE(), admin)); 138 | assertTrue(operatorRegistry.hasRole(operatorRegistry.DEFAULT_ADMIN_ROLE(), admin)); 139 | 140 | assertFalse(stakeManagerV2.hasRole(stakeManagerV2.DEFAULT_ADMIN_ROLE(), devAddr)); 141 | assertFalse(operatorRegistry.hasRole(operatorRegistry.DEFAULT_ADMIN_ROLE(), devAddr)); 142 | } 143 | 144 | function _createProxy(address impl) private returns (address proxy) { 145 | proxy = address(new TransparentUpgradeableProxy{ salt: GENERIC_SALT }(impl, proxyAdmin, "")); 146 | } 147 | 148 | function _migrateFunds() private { 149 | uint256 prevTVL = stakeManagerV1.getTotalPooledBnb(); 150 | vm.deal(manager, prevTVL); 151 | 152 | vm.startPrank(manager); 153 | stakeManagerV1.togglePause(); 154 | stakeManagerV2.pause(); 155 | stakeManagerV2.delegateWithoutMinting{ value: prevTVL }(); 156 | vm.stopPrank(); 157 | 158 | // set stake Manager on BnbX 159 | vm.prank(timelock); 160 | BnbX(bnbxAddr).setStakeManager(address(stakeManagerV2)); 161 | 162 | vm.prank(admin); 163 | stakeManagerV2.unpause(); 164 | } 165 | 166 | /// @dev Computes the address of a proxy for the given implementation 167 | /// @param implementation the implementation to proxy 168 | /// @return proxyAddr the address of the created proxy 169 | function _computeAddress(address implementation) private view returns (address) { 170 | bytes memory creationCode = type(TransparentUpgradeableProxy).creationCode; 171 | bytes memory contractBytecode = abi.encodePacked(creationCode, abi.encode(implementation, proxyAdmin, "")); 172 | 173 | return Create2.computeAddress(GENERIC_SALT, keccak256(contractBytecode)); 174 | } 175 | 176 | function _bnbxBalance(address addr) internal view returns (uint256) { 177 | return BnbX(bnbxAddr).balanceOf(addr); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /test/fork-tests/StakeManagerV2Undelegations.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.25; 3 | 4 | import "./StakeManagerV2Setup.t.sol"; 5 | 6 | contract StakeManagerV2Undelegations is StakeManagerV2Setup { 7 | uint256 amountDeposited = 1e10 ether; 8 | 9 | function setUp() public override { 10 | super.setUp(); 11 | 12 | // set deposit and withdrawal operator same 13 | address withdrawalOperator = operatorRegistry.preferredWithdrawalOperator(); 14 | vm.prank(staderOperator); 15 | operatorRegistry.setPreferredDepositOperator(withdrawalOperator); 16 | 17 | startHoax(user1); 18 | stakeManagerV2.delegate{ value: amountDeposited }("referral"); 19 | BnbX(bnbxAddr).approve(address(stakeManagerV2), 2 * amountDeposited); 20 | vm.stopPrank(); 21 | 22 | startHoax(user2); 23 | stakeManagerV2.delegate{ value: amountDeposited }("referral"); 24 | BnbX(bnbxAddr).approve(address(stakeManagerV2), 2 * amountDeposited); 25 | vm.stopPrank(); 26 | } 27 | 28 | function test_getBnbxToBurnForBatchSize() public { 29 | _batchUndelegateSetup(3 ether, 5 ether); 30 | 31 | assertEq(stakeManagerV2.getBnbxToBurnForBatchSize(3), 11 ether); 32 | assertEq(stakeManagerV2.getBnbxToBurnForBatchSize(1), 3 ether); 33 | assertEq(stakeManagerV2.getBnbxToBurnForBatchSize(2), 8 ether); 34 | assertEq(stakeManagerV2.getBnbxToBurnForBatchSize(6), 24 ether); 35 | assertEq(stakeManagerV2.getBnbxToBurnForBatchSize(7), 24 ether); 36 | assertEq(stakeManagerV2.getBnbxToBurnForBatchSize(0), 0); 37 | } 38 | 39 | function test_revertWhenWithdrawAmountIsLow(uint256 bnbxAmount) public { 40 | vm.assume(bnbxAmount < stakeManagerV2.minWithdrawableBnbx()); 41 | 42 | vm.expectRevert(IStakeManagerV2.WithdrawalBelowMinimum.selector); 43 | vm.prank(user1); 44 | stakeManagerV2.requestWithdraw(bnbxAmount, ""); 45 | } 46 | 47 | function testFuzz_userWithdrawAndBatchCreation(uint256 bnbxToWithdraw) public { 48 | uint256 userBnbxBalance = BnbX(bnbxAddr).balanceOf(user1); 49 | vm.assume(bnbxToWithdraw >= stakeManagerV2.minWithdrawableBnbx()); 50 | vm.assume(bnbxToWithdraw <= userBnbxBalance); 51 | 52 | assertEq(stakeManagerV2.getUserRequestIds(user1).length, 0); 53 | 54 | vm.prank(user1); 55 | stakeManagerV2.requestWithdraw(bnbxToWithdraw, ""); 56 | 57 | assertEq(stakeManagerV2.getUnprocessedWithdrawalRequestCount(), 1); 58 | 59 | uint256[] memory userReqIds = stakeManagerV2.getUserRequestIds(user1); 60 | assertEq(userReqIds.length, 1); 61 | 62 | WithdrawalRequest memory request = stakeManagerV2.getUserRequestInfo(userReqIds[0]); 63 | assertEq(request.user, user1); 64 | assertEq(request.processed, false); 65 | assertEq(request.claimed, false); 66 | assertEq(request.amountInBnbX, bnbxToWithdraw); 67 | assertEq(request.batchId, type(uint256).max); 68 | 69 | uint256 batchId = stakeManagerV2.getBatchWithdrawalRequestCount(); 70 | vm.prank(staderOperator); 71 | stakeManagerV2.startBatchUndelegation(10, address(0)); 72 | assertEq(stakeManagerV2.getBatchWithdrawalRequestCount(), batchId + 1); 73 | 74 | BatchWithdrawalRequest memory batchRequest = stakeManagerV2.getBatchWithdrawalRequestInfo(batchId); 75 | assertApproxEqAbs(batchRequest.amountInBnb, stakeManagerV2.convertBnbXToBnb(bnbxToWithdraw), 5); 76 | assertEq(batchRequest.amountInBnbX, bnbxToWithdraw); 77 | assertEq(batchRequest.unlockTime, block.timestamp + STAKE_HUB.unbondPeriod()); 78 | assertEq(batchRequest.operator, operatorRegistry.preferredWithdrawalOperator()); 79 | assertEq(batchRequest.isClaimable, false); 80 | 81 | request = stakeManagerV2.getUserRequestInfo(userReqIds[0]); 82 | assertEq(request.user, user1); 83 | assertEq(request.processed, true); 84 | assertEq(request.claimed, false); 85 | assertEq(request.amountInBnbX, bnbxToWithdraw); 86 | assertEq(request.batchId, batchId); 87 | } 88 | 89 | function test_claimWithdrawal() public { 90 | vm.startPrank(user1); 91 | uint256 amountMinted = stakeManagerV2.delegate{ value: 1 ether }("referral"); 92 | amountMinted = 1e16; 93 | uint256 amountOfBnbExpected = stakeManagerV2.convertBnbXToBnb(amountMinted); 94 | /* --------------------------- withdrawal request --------------------------- */ 95 | // there has to be a withdrawalrequest 96 | vm.startPrank(user1); 97 | BnbX(bnbxAddr).approve(address(stakeManagerV2), type(uint256).max); 98 | stakeManagerV2.requestWithdraw(amountMinted, ""); 99 | assertEq(stakeManagerV2.getUnprocessedWithdrawalRequestCount(), 1); 100 | assertEq(BnbX(bnbxAddr).balanceOf(address(stakeManagerV2)), amountMinted); 101 | vm.stopPrank(); 102 | /* ---------------------------- startBatch undelegation first ------------------------ 103 | ---- */ 104 | vm.startPrank(staderOperator); 105 | stakeManagerV2.startBatchUndelegation(1, address(0)); 106 | uint256 batchWithdrawalRequestCount = stakeManagerV2.getBatchWithdrawalRequestCount(); 107 | console.log("batchWithdrawalRequestCount: ", batchWithdrawalRequestCount); 108 | vm.stopPrank(); 109 | /* ------------------- have to complete batch undelegation ------------------ */ 110 | // this is to change the state of the batch request and make it claimable 111 | vm.startPrank(staderOperator); 112 | skip(7 days); 113 | uint256 firstUnbondingBatchIndexBefore = stakeManagerV2.firstUnbondingBatchIndex(); 114 | 115 | stakeManagerV2.completeBatchUndelegation(); 116 | uint256 firstUnbondingBatchIndexAfter = stakeManagerV2.firstUnbondingBatchIndex(); 117 | assertEq(firstUnbondingBatchIndexAfter, firstUnbondingBatchIndexBefore + 1); 118 | vm.stopPrank(); 119 | 120 | uint256 userBalanceBefore = user1.balance; 121 | vm.startPrank(user1); 122 | stakeManagerV2.claimWithdrawal(0); 123 | vm.stopPrank(); 124 | 125 | uint256 userBalanceAfter = user1.balance; 126 | assertApproxEqAbs(userBalanceAfter, userBalanceBefore + amountOfBnbExpected, 2); 127 | } 128 | 129 | function test_E2E_MultipleUserWithdrawal() public { 130 | _batchUndelegateSetup(5 ether, 3 ether); 131 | 132 | assertEq(stakeManagerV2.getUserRequestIds(user1).length, 3); 133 | assertEq(stakeManagerV2.getUserRequestIds(user2).length, 3); 134 | assertEq(stakeManagerV2.getUnprocessedWithdrawalRequestCount(), 6); 135 | 136 | uint256 prevNumBatches = stakeManagerV2.getBatchWithdrawalRequestCount(); 137 | 138 | vm.startPrank(staderOperator); 139 | stakeManagerV2.startBatchUndelegation(2, address(0)); 140 | assertEq(stakeManagerV2.getUnprocessedWithdrawalRequestCount(), 4); 141 | 142 | stakeManagerV2.startBatchUndelegation(6, address(0)); 143 | assertEq(stakeManagerV2.getUnprocessedWithdrawalRequestCount(), 0); 144 | 145 | vm.stopPrank(); 146 | 147 | assertEq(stakeManagerV2.getBatchWithdrawalRequestCount(), prevNumBatches + 2); 148 | 149 | vm.warp(block.timestamp + STAKE_HUB.unbondPeriod() + 1); 150 | 151 | vm.prank(staderOperator); 152 | stakeManagerV2.completeBatchUndelegation(); 153 | 154 | vm.prank(user1); 155 | stakeManagerV2.claimWithdrawal(0); 156 | 157 | vm.prank(user2); 158 | stakeManagerV2.claimWithdrawal(0); 159 | } 160 | 161 | function testFuzz_userClaimRestrictedBeforeCompleteBatchUndelegation(uint256 amount1, uint256 amount2) public { 162 | uint256 minWithdrawableBnbx = stakeManagerV2.minWithdrawableBnbx(); 163 | uint256 userBnbxBalance = BnbX(bnbxAddr).balanceOf(user1); 164 | vm.assume(amount1 >= minWithdrawableBnbx && amount1 < userBnbxBalance / 2); 165 | vm.assume(amount2 >= minWithdrawableBnbx && amount2 < userBnbxBalance / 2); 166 | 167 | // user unstakes 168 | vm.prank(user1); 169 | stakeManagerV2.requestWithdraw(amount1, ""); 170 | 171 | // user tries to claim before startBatchUndelegation 172 | vm.expectRevert(IStakeManagerV2.NotProcessed.selector); 173 | vm.prank(user1); 174 | stakeManagerV2.claimWithdrawal(0); 175 | 176 | // staderOperator starts batch undelegation 177 | vm.prank(staderOperator); 178 | stakeManagerV2.startBatchUndelegation(1, address(0)); 179 | 180 | // user tries to claim before completeBatchUndelegation 181 | vm.expectRevert(IStakeManagerV2.Unbonding.selector); 182 | vm.prank(user1); 183 | stakeManagerV2.claimWithdrawal(0); 184 | 185 | skip(7 days); 186 | 187 | // user tries to claim again after 7 days before completeBatchUndelegation 188 | vm.expectRevert(IStakeManagerV2.Unbonding.selector); 189 | vm.prank(user1); 190 | stakeManagerV2.claimWithdrawal(0); 191 | 192 | // someone (staderOperator) completes batch undelegation 193 | stakeManagerV2.completeBatchUndelegation(); 194 | 195 | // user successfully claims after completeBatchUndelegation 196 | vm.prank(user1); 197 | stakeManagerV2.claimWithdrawal(0); 198 | 199 | // user tries to claim again 200 | vm.expectRevert(IStakeManagerV2.NoWithdrawalRequests.selector); 201 | vm.prank(user1); 202 | stakeManagerV2.claimWithdrawal(0); 203 | 204 | // ---------------------------------------------------------------- // 205 | // user requests withdrawal again 206 | vm.prank(user1); 207 | stakeManagerV2.requestWithdraw(amount2, ""); 208 | 209 | // user tries to claim before startBatchUndelegation 210 | vm.expectRevert(IStakeManagerV2.NotProcessed.selector); 211 | vm.prank(user1); 212 | stakeManagerV2.claimWithdrawal(0); 213 | 214 | // staderOperator starts batch undelegation 215 | vm.prank(staderOperator); 216 | stakeManagerV2.startBatchUndelegation(1, address(0)); 217 | 218 | // user tries to claim before completeBatchUndelegation 219 | vm.expectRevert(IStakeManagerV2.Unbonding.selector); 220 | vm.prank(user1); 221 | stakeManagerV2.claimWithdrawal(0); 222 | 223 | skip(7 days); 224 | 225 | // user tries to claim again after 7 days before completeBatchUndelegation 226 | vm.expectRevert(IStakeManagerV2.Unbonding.selector); 227 | vm.prank(user1); 228 | stakeManagerV2.claimWithdrawal(0); 229 | 230 | // someone (staderOperator) completes batch undelegation 231 | stakeManagerV2.completeBatchUndelegation(); 232 | 233 | // user successfully claims after completeBatchUndelegation 234 | vm.prank(user1); 235 | stakeManagerV2.claimWithdrawal(0); 236 | 237 | // user tries to claim again 238 | vm.expectRevert(IStakeManagerV2.NoWithdrawalRequests.selector); 239 | vm.prank(user1); 240 | stakeManagerV2.claimWithdrawal(0); 241 | } 242 | 243 | function test_startBatchUndelegationSucceedsForFewWithdrawRequests() public { 244 | _batchUndelegateSetup(5 ether, 3 ether); 245 | 246 | assertEq(stakeManagerV2.getUnprocessedWithdrawalRequestCount(), 6); 247 | uint256 prevNumBatches = stakeManagerV2.getBatchWithdrawalRequestCount(); 248 | 249 | vm.prank(staderOperator); 250 | stakeManagerV2.startBatchUndelegation(8, address(0)); 251 | 252 | assertEq(stakeManagerV2.getBatchWithdrawalRequestCount(), prevNumBatches + 1); 253 | } 254 | 255 | function _batchUndelegateSetup(uint256 bnbxAmount1, uint256 bnbxAmount2) internal { 256 | vm.prank(user1); 257 | stakeManagerV2.requestWithdraw(bnbxAmount1, ""); 258 | vm.prank(user2); 259 | stakeManagerV2.requestWithdraw(bnbxAmount2, ""); 260 | 261 | vm.prank(user1); 262 | stakeManagerV2.requestWithdraw(bnbxAmount1, ""); 263 | vm.prank(user2); 264 | stakeManagerV2.requestWithdraw(bnbxAmount2, ""); 265 | 266 | vm.prank(user1); 267 | stakeManagerV2.requestWithdraw(bnbxAmount1, ""); 268 | vm.prank(user2); 269 | stakeManagerV2.requestWithdraw(bnbxAmount2, ""); 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /test/migration/Migration.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.25; 3 | 4 | import "forge-std/Test.sol"; 5 | 6 | import { ProxyAdmin } from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; 7 | import { Create2 } from "@openzeppelin/contracts/utils/Create2.sol"; 8 | import { 9 | TransparentUpgradeableProxy, 10 | ITransparentUpgradeableProxy 11 | } from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; 12 | 13 | import { StakeManager } from "contracts/StakeManager.sol"; 14 | import { BnbX } from "contracts/BnbX.sol"; 15 | import { StakeManagerV2 } from "contracts/StakeManagerV2.sol"; 16 | import { OperatorRegistry } from "contracts/OperatorRegistry.sol"; 17 | 18 | contract Migration is Test { 19 | bytes32 public constant GENERIC_SALT = keccak256(abi.encodePacked("BNBX-MIGRATION")); 20 | 21 | address public proxyAdmin; 22 | address public timelock; 23 | address public admin; 24 | address public manager; 25 | address public staderOperator; 26 | 27 | address public treasury; 28 | address public devAddr; 29 | 30 | address public STAKE_HUB = 0x0000000000000000000000000000000000002002; 31 | address public BNBx = 0x1bdd3Cf7F79cfB8EdbB955f20ad99211551BA275; 32 | 33 | StakeManager public stakeManagerV1; 34 | StakeManagerV2 public stakeManagerV2; 35 | OperatorRegistry public operatorRegistry; 36 | 37 | function _createProxy(address impl) private returns (address proxy) { 38 | proxy = address(new TransparentUpgradeableProxy{ salt: GENERIC_SALT }(impl, proxyAdmin, "")); 39 | } 40 | 41 | /// @dev Computes the address of a proxy for the given implementation 42 | /// @param implementation the implementation to proxy 43 | /// @return proxyAddr the address of the created proxy 44 | function _computeAddress(address implementation) private view returns (address) { 45 | bytes memory creationCode = type(TransparentUpgradeableProxy).creationCode; 46 | bytes memory contractBytecode = abi.encodePacked(creationCode, abi.encode(implementation, proxyAdmin, "")); 47 | 48 | return Create2.computeAddress(GENERIC_SALT, keccak256(contractBytecode)); 49 | } 50 | 51 | function _deployAndSetupContracts() private { 52 | // deploy operator registry 53 | operatorRegistry = OperatorRegistry(_createProxy(address(new OperatorRegistry()))); 54 | operatorRegistry.initialize(devAddr); 55 | 56 | // grant manager and operator role for operator registry 57 | vm.startPrank(devAddr); 58 | operatorRegistry.grantRole(operatorRegistry.MANAGER_ROLE(), manager); 59 | operatorRegistry.grantRole(operatorRegistry.OPERATOR_ROLE(), staderOperator); 60 | vm.stopPrank(); 61 | 62 | // add preferred operator 63 | address bscOperator = 0x343dA7Ff0446247ca47AA41e2A25c5Bbb230ED0A; 64 | vm.prank(manager); 65 | operatorRegistry.addOperator(bscOperator); 66 | vm.prank(staderOperator); 67 | operatorRegistry.setPreferredDepositOperator(bscOperator); 68 | 69 | // deploy stake manager v2 70 | // stakeManagerV2 impl 71 | address stakeManagerV2Impl = address(new StakeManagerV2()); 72 | stakeManagerV2 = StakeManagerV2(payable(_createProxy(stakeManagerV2Impl))); 73 | stakeManagerV2.initialize(devAddr, address(operatorRegistry), BNBx, treasury); 74 | operatorRegistry.initialize2(address(stakeManagerV2)); 75 | 76 | assertEq(operatorRegistry.stakeManager(), address(stakeManagerV2)); 77 | 78 | vm.startPrank(devAddr); 79 | // grant manager role for stake manager v2 80 | stakeManagerV2.grantRole(stakeManagerV2.MANAGER_ROLE(), manager); 81 | stakeManagerV2.grantRole(stakeManagerV2.OPERATOR_ROLE(), staderOperator); 82 | 83 | // grant default admin role to admin 84 | stakeManagerV2.grantRole(stakeManagerV2.DEFAULT_ADMIN_ROLE(), admin); 85 | operatorRegistry.grantRole(operatorRegistry.DEFAULT_ADMIN_ROLE(), admin); 86 | 87 | // renounce DEFAULT_ADMIN_ROLE from devAddr 88 | stakeManagerV2.renounceRole(stakeManagerV2.DEFAULT_ADMIN_ROLE(), devAddr); 89 | operatorRegistry.renounceRole(operatorRegistry.DEFAULT_ADMIN_ROLE(), devAddr); 90 | 91 | vm.stopPrank(); 92 | 93 | // assert only admin has DEFAULT_ADMIN_ROLE in both the contracts 94 | assertTrue(stakeManagerV2.hasRole(stakeManagerV2.DEFAULT_ADMIN_ROLE(), admin)); 95 | assertTrue(operatorRegistry.hasRole(operatorRegistry.DEFAULT_ADMIN_ROLE(), admin)); 96 | 97 | assertFalse(stakeManagerV2.hasRole(stakeManagerV2.DEFAULT_ADMIN_ROLE(), devAddr)); 98 | assertFalse(operatorRegistry.hasRole(operatorRegistry.DEFAULT_ADMIN_ROLE(), devAddr)); 99 | } 100 | 101 | function _upgradeStakeManagerV1() private { 102 | address stakeManagerV1Impl = 0x410ef6738C98c9478C7d21eF948a6dfd0FA9ED45; 103 | 104 | vm.prank(timelock); 105 | ProxyAdmin(proxyAdmin).upgrade(ITransparentUpgradeableProxy(address(stakeManagerV1)), stakeManagerV1Impl); 106 | } 107 | 108 | function setUp() public { 109 | string memory rpcUrl = vm.envString("BSC_MAINNET_RPC_URL"); 110 | vm.createSelectFork(rpcUrl); 111 | 112 | proxyAdmin = 0xF90e293D34a42CB592Be6BE6CA19A9963655673C; // old proxy admin with timelock 0xD990A252E7e36700d47520e46cD2B3E446836488 113 | timelock = 0xD990A252E7e36700d47520e46cD2B3E446836488; 114 | admin = 0x79A2Ae748AC8bE4118B7a8096681B30310c3adBE; // internal multisig 115 | manager = 0x79A2Ae748AC8bE4118B7a8096681B30310c3adBE; // internal multisig 116 | staderOperator = 0xDfB508E262B683EC52D533B80242Ae74087BC7EB; 117 | treasury = 0x01422247a1d15BB4FcF91F5A077Cf25BA6460130; 118 | 119 | devAddr = address(this); // may change it to your own address 120 | 121 | stakeManagerV1 = StakeManager(payable(0x7276241a669489E4BBB76f63d2A43Bfe63080F2F)); 122 | operatorRegistry = OperatorRegistry(0x9C1759359Aa7D32911c5bAD613E836aEd7c621a8); 123 | stakeManagerV2 = StakeManagerV2(payable(0x3b961e83400D51e6E1AF5c450d3C7d7b80588d28)); 124 | // _deployAndSetupContracts(); 125 | } 126 | 127 | function legacy_test_migrateFunds() public { 128 | // add preferred operator 129 | address bscOperator = 0x343dA7Ff0446247ca47AA41e2A25c5Bbb230ED0A; 130 | vm.startPrank(manager); 131 | operatorRegistry.addOperator(bscOperator); 132 | stakeManagerV2.pause(); 133 | vm.stopPrank(); 134 | 135 | vm.startPrank(staderOperator); 136 | operatorRegistry.setPreferredDepositOperator(bscOperator); 137 | operatorRegistry.setPreferredWithdrawalOperator(bscOperator); 138 | vm.stopPrank(); 139 | 140 | address userV1 = makeAddr("userV1"); 141 | // user gets some bnbx 142 | startHoax(userV1, 10 ether); 143 | stakeManagerV1.deposit{ value: 10 ether }(); 144 | 145 | // some user requests withdraw 146 | BnbX(BNBx).approve(address(stakeManagerV1), 8 ether); 147 | stakeManagerV1.requestWithdraw(8 ether); 148 | vm.stopPrank(); 149 | 150 | assertEq(stakeManagerV1.getUserWithdrawalRequests(userV1).length, 1); // 1 request raised 151 | (bool isClaimable,) = stakeManagerV1.getUserRequestStatus(userV1, 0); 152 | assertFalse(isClaimable); // not claimable 153 | 154 | // claimWallet or staderOperator should have unstaked and rcvd all funds 155 | uint256 stakedFunds = (5760.8394 ether + 5013.0108 ether + 5012.5161 ether + 3512.3595 ether + 19.8107 ether); 156 | vm.deal(staderOperator, staderOperator.balance + stakedFunds); 157 | // manager processes pending withdraw batch 158 | vm.startPrank(staderOperator); 159 | (uint256 uuid, uint256 batchAmountInBNB) = stakeManagerV1.startUndelegation(); 160 | stakeManagerV1.undelegationStarted(uuid); 161 | stakeManagerV1.completeUndelegation{ value: batchAmountInBNB }(uuid); 162 | vm.stopPrank(); 163 | 164 | (isClaimable,) = stakeManagerV1.getUserRequestStatus(userV1, 0); 165 | assertTrue(isClaimable); // claimable 166 | 167 | uint256 prevManagerBal = manager.balance; 168 | uint256 depositsInContractV1 = stakeManagerV1.depositsInContract(); 169 | 170 | _upgradeStakeManagerV1(); // --------- EXECUTE TXN 6 on TIMELOCK -------------------- // 171 | 172 | // admin extracts funds from stake manager v1 173 | vm.startPrank(admin); // internal multisig holds default_admin_role 174 | stakeManagerV1.togglePause(); // pause stakeManagerV1 175 | stakeManagerV1.migrateFunds(); // depositsInContractV1 funds are sent to manager 176 | vm.stopPrank(); 177 | assertEq(manager.balance, prevManagerBal + depositsInContractV1); 178 | 179 | // assumption : claim wallet should have atleast depositDelegatedV1 180 | uint256 depositDelegatedV1 = stakeManagerV1.depositsDelegated(); 181 | 182 | // claim wallet (staderOperator) sends depositDelegatedV1 funds to manager 183 | vm.prank(staderOperator); 184 | (bool success,) = payable(manager).call{ value: depositDelegatedV1 }(""); 185 | require(success, "claim_wallet to manager transfer failed"); 186 | 187 | // assert manager has right balance 188 | uint256 prevTVL = stakeManagerV1.getTotalPooledBnb(); 189 | assertEq(manager.balance, prevManagerBal + prevTVL); 190 | 191 | vm.startPrank(manager); 192 | stakeManagerV2.delegateWithoutMinting{ value: prevTVL }(); 193 | vm.stopPrank(); 194 | 195 | uint256 er1 = stakeManagerV1.convertBnbXToBnb(1 ether); 196 | uint256 er2 = stakeManagerV2.convertBnbXToBnb(1 ether); 197 | console2.log("v1: 1 BNBx to BNB:", er1); 198 | console2.log("v2: 1 BNBx to BNB:", er2); 199 | console2.log("claim wallet balance left:", staderOperator.balance); 200 | assertEq(er1, er2, "migrate funds failed"); 201 | 202 | // set stake Manager on BnbX // --------- EXECUTE TXN 7 on TIMELOCK -------------------- // 203 | vm.prank(timelock); 204 | BnbX(BNBx).setStakeManager(address(stakeManagerV2)); 205 | 206 | vm.prank(admin); 207 | stakeManagerV2.unpause(); 208 | 209 | // simple delegate test on stakeManagerV2 210 | address userV2 = makeAddr("userV2"); 211 | vm.deal(userV2, 2 ether); 212 | 213 | vm.prank(userV2); 214 | stakeManagerV2.delegate{ value: er2 }("referral"); 215 | assertApproxEqAbs(BnbX(BNBx).balanceOf(userV2), 1 ether, 2); 216 | 217 | // previous user1 claims from v1 218 | vm.prank(userV1); 219 | stakeManagerV1.claimWithdraw(0); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "resolveJsonModule": true 10 | } 11 | } 12 | --------------------------------------------------------------------------------