├── .example.env ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── contracts ├── Migrations.sol ├── Staking.sol └── test │ └── LocalFXCToken.sol ├── migrations ├── 1564352876_flexa_staking_wallet.js └── 1_initial_migration.js ├── package-lock.json ├── package.json ├── solhint.json ├── test ├── staking-addWithdrawalRoot.js ├── staking-assumeOwnership.js ├── staking-authorizeOwnershipTransfer.js ├── staking-deposit.js ├── staking-modifyImmediatelyWithdrawableLimit.js ├── staking-refundPendingDeposit.js ├── staking-removeWithdrawalRoots.js ├── staking-renounceWithdrawalAuthorization.js ├── staking-resetFallbackMechanismDate.js ├── staking-setFallbackRoot.js ├── staking-setters.js ├── staking-withdraw.js ├── staking-withdrawFallback.js └── utils.js └── truffle-config.js /.example.env: -------------------------------------------------------------------------------- 1 | MNEMONIC="put your mnemonic here" 2 | RINKEBY_ENDPOINT_KEY="Infura project Rinkeby endpoint key here" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Envionment variables 3 | .env 4 | 5 | # Standard Truffle / Solidity ignores 6 | contracts/.placeholder 7 | test/.placeholder 8 | build 9 | node_modules 10 | 11 | # IntelliJ files 12 | *.iml 13 | .idea/ 14 | .vscode/ 15 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Flexa Network Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Capacity Smart Contracts 2 | 3 | Ethereum smart contracts supporting Flexa capacity wallets 4 | 5 | # Audit 6 | 7 | The contract code was [audited](https://github.com/trailofbits/publications/blob/master/reviews/Flexa.pdf) by [Trail of Bits](https://www.trailofbits.com/) over the course of two weeks in September 2019. Note that [root commit of this repository](https://github.com/flexahq/capacity-smart-contracts/commit/12ab417463516f62634659f76ee71648161894cb) contains the final version of the contract as referenced on page 26 of the audit report. 8 | 9 | # Requirements 10 | * [NodeJS](https://nodejs.org/en/download/) 11 | 12 | # Running locally 13 | In order to run the contracts locally, 14 | * Compile the smart contracts 15 | * Start the Truffle development console 16 | * Deploy the smart contracts 17 | ```javascript 18 | $ cd /path/to/capacity-smart-contracts 19 | $ npm install // Install the required NPM packages (truffle and openzeppelin-solidity for now) 20 | $ truffle compile // compiles the smart contracts 21 | $ truffle develop // starts the truffle development console, including a local blockchain 22 | Truffle Develop started at http://127.0.0.1:9545/ 23 | ... 24 | truffle(develop)> deploy // deploys the smart contracts 25 | Starting migrations... 26 | ... 27 | Deploying 'Staking' 28 | ------------------- 29 | > transaction hash: 0x713118674a2d2d33965896000d0608fc742d4e168200183661e45ae2440b3c8d 30 | > Blocks: 0 Seconds: 0 31 | > contract address: 0x62395D7FF20eBae660b0d212f47E823Ad4Cc1Db2 32 | > account: 0x93606DdFd78D741B6f9d6D572E8b9bfEcB32930B 33 | > balance: 99.88672006 34 | > gas used: 3964787 35 | > gas price: 20 gwei 36 | > value sent: 0 ETH 37 | > total cost: 0.07929574 ETH 38 | 39 | > Saving artifacts 40 | ------------------------------------- 41 | > Total cost: 0.10776974 ETH 42 | ... 43 | ``` 44 | 45 | Now that the contracts are deployed, you can interact with them within the Truffle development console: 46 | ```javascript 47 | truffle(develop)> const staking = await Staking.deployed() 48 | truffle(develop)> await staking._withdrawalPublisher() // simply hitting a getter function. 49 | '0x369CCCb3bF65a6D44C2CE65CAC45Bc02D4052Aa2' 50 | ``` 51 | 52 | For testing locally, we've deployed a vanilla ERC-20 (TFXC) token to mimic FXC. You'll need to grant the FlexaStakingWallet contract access to deposit your TFXC funds: 53 | ```javascript 54 | truffle(develop)> const token = await LocalFXCToken.deployed() 55 | truffle(develop)> await token.approve(staking.address, 100) // Approve 100 TFXC for deposit 56 | ``` 57 | 58 | Now you may test deposit, withdrawal, etc. 59 | ```javascript 60 | truffle(develop)> await staking.deposit(100) 61 | truffle(develop)> await staking._nonceToPendingDeposit(1) 62 | Result { 63 | '0': '', 64 | '1': 65 | BN { 66 | negative: 0, 67 | words: [ 100, <1 empty item> ], 68 | length: 1, 69 | red: null }, 70 | depositor: '', 71 | amount: 72 | BN { 73 | negative: 0, 74 | words: [ 100, <1 empty item> ], 75 | length: 1, 76 | red: null } } 77 | truffle(develop)> const account = (await web3.eth.getAccounts())[0] 78 | truffle(develop)> await staking.withdraw(account, 100, 1, []) 79 | { Error: Returned error: VM Exception while processing transaction: revert Root hash unauthorized -- Reason given: Root hash unauthorized. ... 80 | // Error because the authorization for this withdrawal doesn't exist 81 | ``` 82 | 83 | # Automated Tests 84 | Tests are divided between unit tests and integration tests, which can be run with: 85 | ```javascript 86 | cd /path/to/capacity-smart-contracts 87 | truffle test 88 | ``` 89 | 90 | # Deploying 91 | 92 | ## Rinkeby 93 | ### Setup: 94 | 1) Create a `.env` file of the same format as `.example.env` but with the correct mnemonic and infura Rinkeby Key 95 | 96 | 2) Uncomment the following in `truffle-config.js`: 97 | ``` 98 | // optimizer: { 99 | // enabled: true, 100 | // runs: 200 101 | // }, 102 | ``` 103 | 104 | ### Deployment 105 | Clean start migration: `truffle migrate --network rinkeby --reset --compile-all` 106 | 107 | OR 108 | 109 | Incremental migration: `truffle migrate --network rinkeby --compile-all` 110 | 111 | ### Test 112 | Open up the truffle console pointed to rinkeby: `truffle console --network rinkeby ` 113 | 114 | ```javascript 115 | truffle(rinkeby)> const staking = await Staking.deployed() 116 | truffle(rinkeby)> const fxcAddress = await staking._tokenAddress() 117 | truffle(rinkeby)> fxcAddress 118 | > // this should be the deployed LocalFXCToken address 119 | truffle(rinkeby)> const token = await LocalFXCToken.deployed() 120 | truffle(rinkeby)> const balance = await token.balanceOf('0x369CCCb3bF65a6D44C2CE65CAC45Bc02D4052Aa2') 121 | truffle(rinkeby)> balance.toString() 122 | > '1000000000000000000000000' // Initial supply of test FXC 123 | ``` 124 | 125 | # License 126 | 127 | MIT 128 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.4.21 <0.6.0; 2 | 3 | contract Migrations { 4 | address public owner; 5 | uint public last_completed_migration; 6 | 7 | constructor() public { 8 | owner = msg.sender; 9 | } 10 | 11 | modifier restricted() { 12 | if (msg.sender == owner) _; 13 | } 14 | 15 | function setCompleted(uint completed) public restricted { 16 | last_completed_migration = completed; 17 | } 18 | 19 | function upgrade(address new_address) public restricted { 20 | Migrations upgraded = Migrations(new_address); 21 | upgraded.setCompleted(last_completed_migration); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /contracts/Staking.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.5.3<0.6.0; 2 | 3 | import "openzeppelin-solidity/contracts/math/SafeMath.sol"; 4 | 5 | // Just inlining part of the standard ERC20 contract 6 | interface ERC20Token { 7 | function transfer(address recipient, uint256 amount) external returns (bool); 8 | function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); 9 | } 10 | 11 | /** 12 | * @title Staking is a contract to support locking and releasing ERC-20 tokens 13 | * for the purposes of staking. 14 | */ 15 | contract Staking { 16 | struct PendingDeposit { 17 | address depositor; 18 | uint256 amount; 19 | } 20 | 21 | address public _owner; 22 | address public _authorizedNewOwner; 23 | address public _tokenAddress; 24 | 25 | address public _withdrawalPublisher; 26 | address public _fallbackPublisher; 27 | uint256 public _fallbackWithdrawalDelaySeconds = 1 weeks; 28 | 29 | // 1% of total supply 30 | uint256 public _immediatelyWithdrawableLimit = 100_000 * (10**18); 31 | address public _immediatelyWithdrawableLimitPublisher; 32 | 33 | uint256 public _depositNonce = 0; 34 | mapping(uint256 => PendingDeposit) public _nonceToPendingDeposit; 35 | 36 | uint256 public _maxWithdrawalRootNonce = 0; 37 | mapping(bytes32 => uint256) public _withdrawalRootToNonce; 38 | mapping(address => uint256) public _addressToWithdrawalNonce; 39 | mapping(address => uint256) public _addressToCumulativeAmountWithdrawn; 40 | 41 | bytes32 public _fallbackRoot; 42 | uint256 public _fallbackMaxDepositIncluded = 0; 43 | uint256 public _fallbackSetDate = 2**200; 44 | 45 | event WithdrawalRootHashAddition( 46 | bytes32 indexed rootHash, 47 | uint256 indexed nonce 48 | ); 49 | 50 | event WithdrawalRootHashRemoval( 51 | bytes32 indexed rootHash, 52 | uint256 indexed nonce 53 | ); 54 | 55 | event FallbackRootHashSet( 56 | bytes32 indexed rootHash, 57 | uint256 indexed maxDepositNonceIncluded, 58 | uint256 setDate 59 | ); 60 | 61 | event Deposit( 62 | address indexed depositor, 63 | uint256 indexed amount, 64 | uint256 indexed nonce 65 | ); 66 | 67 | event Withdrawal( 68 | address indexed toAddress, 69 | uint256 indexed amount, 70 | uint256 indexed rootNonce, 71 | uint256 authorizedAccountNonce 72 | ); 73 | 74 | event FallbackWithdrawal( 75 | address indexed toAddress, 76 | uint256 indexed amount 77 | ); 78 | 79 | event PendingDepositRefund( 80 | address indexed depositorAddress, 81 | uint256 indexed amount, 82 | uint256 indexed nonce 83 | ); 84 | 85 | event RenounceWithdrawalAuthorization( 86 | address indexed forAddress 87 | ); 88 | 89 | event FallbackWithdrawalDelayUpdate( 90 | uint256 indexed oldValue, 91 | uint256 indexed newValue 92 | ); 93 | 94 | event FallbackMechanismDateReset( 95 | uint256 indexed newDate 96 | ); 97 | 98 | event ImmediatelyWithdrawableLimitUpdate( 99 | uint256 indexed oldValue, 100 | uint256 indexed newValue 101 | ); 102 | 103 | event OwnershipTransferAuthorization( 104 | address indexed authorizedAddress 105 | ); 106 | 107 | event OwnerUpdate( 108 | address indexed oldValue, 109 | address indexed newValue 110 | ); 111 | 112 | event FallbackPublisherUpdate( 113 | address indexed oldValue, 114 | address indexed newValue 115 | ); 116 | 117 | event WithdrawalPublisherUpdate( 118 | address indexed oldValue, 119 | address indexed newValue 120 | ); 121 | 122 | event ImmediatelyWithdrawableLimitPublisherUpdate( 123 | address indexed oldValue, 124 | address indexed newValue 125 | ); 126 | 127 | constructor( 128 | address tokenAddress, 129 | address fallbackPublisher, 130 | address withdrawalPublisher, 131 | address immediatelyWithdrawableLimitPublisher 132 | ) public { 133 | _owner = msg.sender; 134 | _fallbackPublisher = fallbackPublisher; 135 | _withdrawalPublisher = withdrawalPublisher; 136 | _immediatelyWithdrawableLimitPublisher = immediatelyWithdrawableLimitPublisher; 137 | _tokenAddress = tokenAddress; 138 | } 139 | 140 | /******************** 141 | * STANDARD ACTIONS * 142 | ********************/ 143 | 144 | /** 145 | * @notice Deposits the provided amount of FXC from the message sender into this wallet. 146 | * Note: The sending address must own the provided amount of FXC to deposit, and 147 | * the sender must have indicated to the FXC ERC-20 contract that this contract is 148 | * allowed to transfer at least the provided amount from its address. 149 | * 150 | * @param amount The amount to deposit. 151 | * @return The deposit nonce for this deposit. This can be useful in calling 152 | * refundPendingDeposit(...). 153 | */ 154 | function deposit(uint256 amount) external returns(uint256) { 155 | require( 156 | amount > 0, 157 | "Cannot deposit 0" 158 | ); 159 | 160 | _depositNonce = SafeMath.add(_depositNonce, 1); 161 | _nonceToPendingDeposit[_depositNonce].depositor = msg.sender; 162 | _nonceToPendingDeposit[_depositNonce].amount = amount; 163 | 164 | emit Deposit( 165 | msg.sender, 166 | amount, 167 | _depositNonce 168 | ); 169 | 170 | bool transferred = ERC20Token(_tokenAddress).transferFrom( 171 | msg.sender, 172 | address(this), 173 | amount 174 | ); 175 | require(transferred, "Transfer failed"); 176 | 177 | return _depositNonce; 178 | } 179 | 180 | /** 181 | * @notice Indicates that this address would not like its withdrawable 182 | * funds to be available for withdrawal. This will prevent withdrawal 183 | * for this address until the next withdrawal root is published. 184 | * 185 | * Note: The caller does not need to know or prove the details of the current 186 | * withdrawal authorization in order to renounce it. 187 | * @param forAddress The address for which the withdrawal is being renounced. 188 | */ 189 | function renounceWithdrawalAuthorization(address forAddress) external { 190 | require( 191 | msg.sender == _owner || 192 | msg.sender == _withdrawalPublisher || 193 | msg.sender == forAddress, 194 | "Only the owner, withdrawal publisher, and address in question can renounce a withdrawal authorization" 195 | ); 196 | require( 197 | _addressToWithdrawalNonce[forAddress] < _maxWithdrawalRootNonce, 198 | "Address nonce indicates there are no funds withdrawable" 199 | ); 200 | _addressToWithdrawalNonce[forAddress] = _maxWithdrawalRootNonce; 201 | emit RenounceWithdrawalAuthorization(forAddress); 202 | } 203 | 204 | /** 205 | * @notice Executes a previously authorized token withdrawal. 206 | * @param toAddress The address to which the tokens are to be transferred. 207 | * @param amount The amount of tokens to be withdrawn. 208 | * @param maxAuthorizedAccountNonce The maximum authorized account nonce for the withdrawing 209 | * address encoded within the withdrawal authorization. Prevents double-withdrawals. 210 | * @param merkleProof The Merkle tree proof associated with the withdrawal 211 | * authorization. 212 | */ 213 | function withdraw( 214 | address toAddress, 215 | uint256 amount, 216 | uint256 maxAuthorizedAccountNonce, 217 | bytes32[] calldata merkleProof 218 | ) external { 219 | require( 220 | msg.sender == _owner || msg.sender == toAddress, 221 | "Only the owner or recipient can execute a withdrawal" 222 | ); 223 | 224 | require( 225 | _addressToWithdrawalNonce[toAddress] <= maxAuthorizedAccountNonce, 226 | "Account nonce in contract exceeds provided max authorized withdrawal nonce for this account" 227 | ); 228 | 229 | require( 230 | amount <= _immediatelyWithdrawableLimit, 231 | "Withdrawal would push contract over its immediately withdrawable limit" 232 | ); 233 | 234 | bytes32 leafDataHash = keccak256(abi.encodePacked( 235 | toAddress, 236 | amount, 237 | maxAuthorizedAccountNonce 238 | )); 239 | 240 | bytes32 calculatedRoot = calculateMerkleRoot(merkleProof, leafDataHash); 241 | uint256 withdrawalPermissionRootNonce = _withdrawalRootToNonce[calculatedRoot]; 242 | 243 | require( 244 | withdrawalPermissionRootNonce > 0, 245 | "Root hash unauthorized"); 246 | require( 247 | withdrawalPermissionRootNonce > maxAuthorizedAccountNonce, 248 | "Encoded nonce not greater than max last authorized nonce for this account" 249 | ); 250 | 251 | _immediatelyWithdrawableLimit -= amount; // amount guaranteed <= _immediatelyWithdrawableLimit 252 | _addressToWithdrawalNonce[toAddress] = withdrawalPermissionRootNonce; 253 | _addressToCumulativeAmountWithdrawn[toAddress] = SafeMath.add(amount, _addressToCumulativeAmountWithdrawn[toAddress]); 254 | 255 | emit Withdrawal( 256 | toAddress, 257 | amount, 258 | withdrawalPermissionRootNonce, 259 | maxAuthorizedAccountNonce 260 | ); 261 | 262 | bool transferred = ERC20Token(_tokenAddress).transfer( 263 | toAddress, 264 | amount 265 | ); 266 | 267 | require(transferred, "Transfer failed"); 268 | } 269 | 270 | /** 271 | * @notice Executes a fallback withdrawal transfer. 272 | * @param toAddress The address to which the tokens are to be transferred. 273 | * @param maxCumulativeAmountWithdrawn The lifetime withdrawal limit that this address is 274 | * subject to. This is encoded within the fallback authorization to prevent regular 275 | * withdrawal / fallback withdrawal double-spends 276 | * @param merkleProof The Merkle tree proof associated with the withdrawal authorization. 277 | */ 278 | function withdrawFallback( 279 | address toAddress, 280 | uint256 maxCumulativeAmountWithdrawn, 281 | bytes32[] calldata merkleProof 282 | ) external { 283 | require( 284 | msg.sender == _owner || msg.sender == toAddress, 285 | "Only the owner or recipient can execute a fallback withdrawal" 286 | ); 287 | require( 288 | SafeMath.add(_fallbackSetDate, _fallbackWithdrawalDelaySeconds) <= block.timestamp, 289 | "Fallback withdrawal period is not active" 290 | ); 291 | require( 292 | _addressToCumulativeAmountWithdrawn[toAddress] < maxCumulativeAmountWithdrawn, 293 | "Withdrawal not permitted when amount withdrawn is at lifetime withdrawal limit" 294 | ); 295 | 296 | bytes32 msgHash = keccak256(abi.encodePacked( 297 | toAddress, 298 | maxCumulativeAmountWithdrawn 299 | )); 300 | 301 | bytes32 calculatedRoot = calculateMerkleRoot(merkleProof, msgHash); 302 | require( 303 | _fallbackRoot == calculatedRoot, 304 | "Root hash unauthorized" 305 | ); 306 | 307 | // If user is triggering fallback withdrawal, invalidate all existing regular withdrawals 308 | _addressToWithdrawalNonce[toAddress] = _maxWithdrawalRootNonce; 309 | 310 | // _addressToCumulativeAmountWithdrawn[toAddress] guaranteed < maxCumulativeAmountWithdrawn 311 | uint256 withdrawalAmount = maxCumulativeAmountWithdrawn - _addressToCumulativeAmountWithdrawn[toAddress]; 312 | _addressToCumulativeAmountWithdrawn[toAddress] = maxCumulativeAmountWithdrawn; 313 | 314 | emit FallbackWithdrawal( 315 | toAddress, 316 | withdrawalAmount 317 | ); 318 | 319 | bool transferred = ERC20Token(_tokenAddress).transfer( 320 | toAddress, 321 | withdrawalAmount 322 | ); 323 | 324 | require(transferred, "Transfer failed"); 325 | } 326 | 327 | /** 328 | * @notice Refunds a pending deposit for the provided address, refunding the pending funds. 329 | * This may only take place if the fallback withdrawal period has lapsed. 330 | * @param depositNonce The deposit nonce uniquely identifying the deposit to cancel 331 | */ 332 | function refundPendingDeposit(uint256 depositNonce) external { 333 | address depositor = _nonceToPendingDeposit[depositNonce].depositor; 334 | require( 335 | msg.sender == _owner || msg.sender == depositor, 336 | "Only the owner or depositor can initiate the refund of a pending deposit" 337 | ); 338 | require( 339 | SafeMath.add(_fallbackSetDate, _fallbackWithdrawalDelaySeconds) <= block.timestamp, 340 | "Fallback withdrawal period is not active, so refunds are not permitted" 341 | ); 342 | uint256 amount = _nonceToPendingDeposit[depositNonce].amount; 343 | require( 344 | depositNonce > _fallbackMaxDepositIncluded && 345 | amount > 0, 346 | "There is no pending deposit for the specified nonce" 347 | ); 348 | delete _nonceToPendingDeposit[depositNonce]; 349 | 350 | emit PendingDepositRefund(depositor, amount, depositNonce); 351 | 352 | bool transferred = ERC20Token(_tokenAddress).transfer( 353 | depositor, 354 | amount 355 | ); 356 | require(transferred, "Transfer failed"); 357 | } 358 | 359 | /***************** 360 | * ADMIN ACTIONS * 361 | *****************/ 362 | 363 | /** 364 | * @notice Authorizes the transfer of ownership from _owner to the provided address. 365 | * NOTE: No transfer will occur unless authorizedAddress calls assumeOwnership( ). 366 | * This authorization may be removed by another call to this function authorizing 367 | * the null address. 368 | * @param authorizedAddress The address authorized to become the new owner. 369 | */ 370 | function authorizeOwnershipTransfer(address authorizedAddress) external { 371 | require( 372 | msg.sender == _owner, 373 | "Only the owner can authorize a new address to become owner" 374 | ); 375 | 376 | _authorizedNewOwner = authorizedAddress; 377 | 378 | emit OwnershipTransferAuthorization(_authorizedNewOwner); 379 | } 380 | 381 | /** 382 | * @notice Transfers ownership of this contract to the _authorizedNewOwner. 383 | */ 384 | function assumeOwnership() external { 385 | require( 386 | msg.sender == _authorizedNewOwner, 387 | "Only the authorized new owner can accept ownership" 388 | ); 389 | address oldValue = _owner; 390 | _owner = _authorizedNewOwner; 391 | _authorizedNewOwner = address(0); 392 | 393 | emit OwnerUpdate(oldValue, _owner); 394 | } 395 | 396 | /** 397 | * @notice Updates the Withdrawal Publisher address, the only address other than the 398 | * owner that can publish / remove withdrawal Merkle tree roots. 399 | * @param newWithdrawalPublisher The address of the new Withdrawal Publisher 400 | */ 401 | function setWithdrawalPublisher(address newWithdrawalPublisher) external { 402 | require( 403 | msg.sender == _owner, 404 | "Only the owner can set the withdrawal publisher address" 405 | ); 406 | address oldValue = _withdrawalPublisher; 407 | _withdrawalPublisher = newWithdrawalPublisher; 408 | 409 | emit WithdrawalPublisherUpdate(oldValue, _withdrawalPublisher); 410 | } 411 | 412 | /** 413 | * @notice Updates the Fallback Publisher address, the only address other than 414 | * the owner that can publish / remove fallback withdrawal Merkle tree roots. 415 | * @param newFallbackPublisher The address of the new Fallback Publisher 416 | */ 417 | function setFallbackPublisher(address newFallbackPublisher) external { 418 | require( 419 | msg.sender == _owner, 420 | "Only the owner can set the fallback publisher address" 421 | ); 422 | address oldValue = _fallbackPublisher; 423 | _fallbackPublisher = newFallbackPublisher; 424 | 425 | emit FallbackPublisherUpdate(oldValue, _fallbackPublisher); 426 | } 427 | 428 | /** 429 | * @notice Updates the Immediately Withdrawable Limit Publisher address, the only address 430 | * other than the owner that can set the immediately withdrawable limit. 431 | * @param newImmediatelyWithdrawableLimitPublisher The address of the new Immediately 432 | * Withdrawable Limit Publisher 433 | */ 434 | function setImmediatelyWithdrawableLimitPublisher( 435 | address newImmediatelyWithdrawableLimitPublisher 436 | ) external { 437 | require( 438 | msg.sender == _owner, 439 | "Only the owner can set the immediately withdrawable limit publisher address" 440 | ); 441 | address oldValue = _immediatelyWithdrawableLimitPublisher; 442 | _immediatelyWithdrawableLimitPublisher = newImmediatelyWithdrawableLimitPublisher; 443 | 444 | emit ImmediatelyWithdrawableLimitPublisherUpdate( 445 | oldValue, 446 | _immediatelyWithdrawableLimitPublisher 447 | ); 448 | } 449 | 450 | /** 451 | * @notice Modifies the immediately withdrawable limit (the maximum amount that 452 | * can be withdrawn from withdrawal authorization roots before the limit needs 453 | * to be updated by Flexa) by the provided amount. 454 | * If negative, it will be decreased, if positive, increased. 455 | * This is to prevent contract funds from being drained by error or publisher malice. 456 | * This does not affect the fallback withdrawal mechanism. 457 | * @param amount amount to modify the limit by. 458 | */ 459 | function modifyImmediatelyWithdrawableLimit(int256 amount) external { 460 | require( 461 | msg.sender == _owner || msg.sender == _immediatelyWithdrawableLimitPublisher, 462 | "Only the immediately withdrawable limit publisher and owner can modify the immediately withdrawable limit" 463 | ); 464 | uint256 oldLimit = _immediatelyWithdrawableLimit; 465 | 466 | if (amount < 0) { 467 | uint256 unsignedAmount = uint256(-amount); 468 | _immediatelyWithdrawableLimit = SafeMath.sub(_immediatelyWithdrawableLimit, unsignedAmount); 469 | } else { 470 | uint256 unsignedAmount = uint256(amount); 471 | _immediatelyWithdrawableLimit = SafeMath.add(_immediatelyWithdrawableLimit, unsignedAmount); 472 | } 473 | 474 | emit ImmediatelyWithdrawableLimitUpdate(oldLimit, _immediatelyWithdrawableLimit); 475 | } 476 | 477 | /** 478 | * @notice Updates the time-lock period for a fallback withdrawal to be permitted if no 479 | * action is taken by Flexa. 480 | * @param newFallbackDelaySeconds The new delay period in seconds. 481 | */ 482 | function setFallbackWithdrawalDelay(uint256 newFallbackDelaySeconds) external { 483 | require( 484 | msg.sender == _owner, 485 | "Only the owner can set the fallback withdrawal delay" 486 | ); 487 | require( 488 | newFallbackDelaySeconds != 0, 489 | "New fallback delay may not be 0" 490 | ); 491 | 492 | uint256 oldDelay = _fallbackWithdrawalDelaySeconds; 493 | _fallbackWithdrawalDelaySeconds = newFallbackDelaySeconds; 494 | 495 | emit FallbackWithdrawalDelayUpdate(oldDelay, newFallbackDelaySeconds); 496 | } 497 | 498 | /** 499 | * @notice Adds the root hash of a merkle tree containing authorized token withdrawals. 500 | * @param root The root hash to be added to the repository. 501 | * @param nonce The nonce of the new root hash. Must be exactly one higher 502 | * than the existing max nonce. 503 | * @param replacedRoots The root hashes to be removed from the repository. 504 | */ 505 | function addWithdrawalRoot( 506 | bytes32 root, 507 | uint256 nonce, 508 | bytes32[] calldata replacedRoots 509 | ) external { 510 | require( 511 | msg.sender == _owner || msg.sender == _withdrawalPublisher, 512 | "Only the owner and withdrawal publisher can add and replace withdrawal root hashes" 513 | ); 514 | require( 515 | root != 0, 516 | "Added root may not be 0" 517 | ); 518 | require( 519 | // Overflowing uint256 by incrementing by 1 not plausible and guarded by nonce variable. 520 | _maxWithdrawalRootNonce + 1 == nonce, 521 | "Nonce must be exactly max nonce + 1" 522 | ); 523 | require( 524 | _withdrawalRootToNonce[root] == 0, 525 | "Root already exists and is associated with a different nonce" 526 | ); 527 | 528 | _withdrawalRootToNonce[root] = nonce; 529 | _maxWithdrawalRootNonce = nonce; 530 | 531 | emit WithdrawalRootHashAddition(root, nonce); 532 | 533 | for (uint256 i = 0; i < replacedRoots.length; i++) { 534 | deleteWithdrawalRoot(replacedRoots[i]); 535 | } 536 | } 537 | 538 | /** 539 | * @notice Removes root hashes of a merkle trees containing authorized 540 | * token withdrawals. 541 | * @param roots The root hashes to be removed from the repository. 542 | */ 543 | function removeWithdrawalRoots(bytes32[] calldata roots) external { 544 | require( 545 | msg.sender == _owner || msg.sender == _withdrawalPublisher, 546 | "Only the owner and withdrawal publisher can remove withdrawal root hashes" 547 | ); 548 | 549 | for (uint256 i = 0; i < roots.length; i++) { 550 | deleteWithdrawalRoot(roots[i]); 551 | } 552 | } 553 | 554 | /** 555 | * @notice Resets the _fallbackSetDate to the current block's timestamp. 556 | * This is mainly used to deactivate the fallback mechanism so new 557 | * fallback roots may be published. 558 | */ 559 | function resetFallbackMechanismDate() external { 560 | require( 561 | msg.sender == _owner || msg.sender == _fallbackPublisher, 562 | "Only the owner and fallback publisher can reset fallback mechanism date" 563 | ); 564 | 565 | _fallbackSetDate = block.timestamp; 566 | 567 | emit FallbackMechanismDateReset(_fallbackSetDate); 568 | } 569 | 570 | /** 571 | * @notice Sets the root hash of the Merkle tree containing fallback 572 | * withdrawal authorizations. This is used in scenarios where the contract 573 | * owner has stopped interacting with the contract, and therefore is no 574 | * longer honoring requests to unlock funds. After the configured fallback 575 | * delay elapses, the withdrawal authorizations included in the supplied 576 | * Merkle tree can be executed to recover otherwise locked funds. 577 | * @param root The root hash to be saved as the fallback withdrawal 578 | * authorizations. 579 | * @param maxDepositIncluded The max deposit nonce represented in this root. 580 | */ 581 | function setFallbackRoot(bytes32 root, uint256 maxDepositIncluded) external { 582 | require( 583 | msg.sender == _owner || msg.sender == _fallbackPublisher, 584 | "Only the owner and fallback publisher can set the fallback root hash" 585 | ); 586 | require( 587 | root != 0, 588 | "New root may not be 0" 589 | ); 590 | require( 591 | SafeMath.add(_fallbackSetDate, _fallbackWithdrawalDelaySeconds) > block.timestamp, 592 | "Cannot set fallback root while fallback mechanism is active" 593 | ); 594 | require( 595 | maxDepositIncluded >= _fallbackMaxDepositIncluded, 596 | "Max deposit included must remain the same or increase" 597 | ); 598 | require( 599 | maxDepositIncluded <= _depositNonce, 600 | "Cannot invalidate future deposits" 601 | ); 602 | 603 | _fallbackRoot = root; 604 | _fallbackMaxDepositIncluded = maxDepositIncluded; 605 | _fallbackSetDate = block.timestamp; 606 | 607 | emit FallbackRootHashSet( 608 | root, 609 | _fallbackMaxDepositIncluded, 610 | block.timestamp 611 | ); 612 | } 613 | 614 | /** 615 | * @notice Deletes the provided root from the collection of 616 | * withdrawal authorization merkle tree roots, invalidating the 617 | * withdrawals contained in the tree assocated with this root. 618 | * @param root The root hash to delete. 619 | */ 620 | function deleteWithdrawalRoot(bytes32 root) private { 621 | uint256 nonce = _withdrawalRootToNonce[root]; 622 | 623 | require( 624 | nonce > 0, 625 | "Root hash not set" 626 | ); 627 | 628 | delete _withdrawalRootToNonce[root]; 629 | 630 | emit WithdrawalRootHashRemoval(root, nonce); 631 | } 632 | 633 | /** 634 | * @notice Calculates the Merkle root for the unique Merkle tree described by the provided 635 | Merkle proof and leaf hash. 636 | * @param merkleProof The sibling node hashes at each level of the tree. 637 | * @param leafHash The hash of the leaf data for which merkleProof is an inclusion proof. 638 | * @return The calculated Merkle root. 639 | */ 640 | function calculateMerkleRoot( 641 | bytes32[] memory merkleProof, 642 | bytes32 leafHash 643 | ) private pure returns (bytes32) { 644 | bytes32 computedHash = leafHash; 645 | 646 | for (uint256 i = 0; i < merkleProof.length; i++) { 647 | bytes32 proofElement = merkleProof[i]; 648 | 649 | if (computedHash < proofElement) { 650 | computedHash = keccak256(abi.encodePacked( 651 | computedHash, 652 | proofElement 653 | )); 654 | } else { 655 | computedHash = keccak256(abi.encodePacked( 656 | proofElement, 657 | computedHash 658 | )); 659 | } 660 | } 661 | 662 | return computedHash; 663 | } 664 | } 665 | -------------------------------------------------------------------------------- /contracts/test/LocalFXCToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol"; 4 | import "openzeppelin-solidity/contracts/token/ERC20/ERC20Detailed.sol"; 5 | 6 | contract LocalFXCToken is ERC20, ERC20Detailed { 7 | // modify token name 8 | string public constant NAME = "TEST FXC Token"; 9 | // modify token symbol 10 | string public constant SYMBOL = "TFXC"; 11 | // modify token decimals 12 | uint8 public constant DECIMALS = 18; 13 | // modify initial token supply 14 | uint256 public constant INITIAL_SUPPLY = 1000000 * (10**uint256(DECIMALS)); 15 | 16 | /** 17 | * @dev Constructor that gives msg.sender all of existing tokens. 18 | */ 19 | constructor() public ERC20Detailed(NAME, SYMBOL, DECIMALS) { 20 | _mint(msg.sender, INITIAL_SUPPLY); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /migrations/1564352876_flexa_staking_wallet.js: -------------------------------------------------------------------------------- 1 | const Staking = artifacts.require("Staking"); 2 | const LocalFXCToken = artifacts.require("LocalFXCToken"); 3 | 4 | const flexaTestWalletAddress = '0x369CCCb3bF65a6D44C2CE65CAC45Bc02D4052Aa2' 5 | module.exports = async function(deployer, network) { 6 | if (network === 'test') { 7 | return; 8 | } 9 | 10 | console.log(`Deploying to network: ${network}`); 11 | // Hardcode FXC Token address if live 12 | if (network === 'live') { 13 | console.log('Deploying Staking Contract TO MAINNET!.'); 14 | await deployer.deploy( 15 | Staking, 16 | '0x4a57E687b9126435a9B19E4A802113e266AdeBde', 17 | 0, // TODO: Figure out default fallback publisher 18 | 0, // TODO: Figure out default withdrawal publisher 19 | 0, // TODO: Figure out default immediately withdrawable limit publisher 20 | ); 21 | 22 | } else if (network === 'develop') { 23 | console.log('Deploying Local FXC Token.'); 24 | await deployer.deploy(LocalFXCToken); 25 | 26 | console.log('Deploying Staking Contract.'); 27 | await deployer.deploy( 28 | Staking, 29 | LocalFXCToken.address, 30 | flexaTestWalletAddress, 31 | flexaTestWalletAddress, 32 | flexaTestWalletAddress 33 | ); 34 | } else if (network === 'rinkeby-fork' || network === 'rinkeby') { 35 | console.log('Deploying Local FXC Token.'); 36 | await deployer.deploy(LocalFXCToken); 37 | 38 | console.log('Deploying Staking Contract.'); 39 | await deployer.deploy( 40 | Staking, 41 | LocalFXCToken.address, 42 | flexaTestWalletAddress, 43 | flexaTestWalletAddress, 44 | flexaTestWalletAddress 45 | ); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | const Migrations = artifacts.require("Migrations"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "network-staking-contract", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "prettier:solidity": "./node_modules/.bin/prettier --write contracts/**/*.sol" 6 | }, 7 | "dependencies": { 8 | "dotenv": "^8.0.0", 9 | "openzeppelin-solidity": "2.3.0", 10 | "truffle": "^5.0.29", 11 | "truffle-contract": "^4.0.27", 12 | "truffle-hdwallet-provider": "^1.0.15" 13 | }, 14 | "devDependencies": { 15 | "prettier": "^1.18.2", 16 | "prettier-plugin-solidity": "^1.0.0-alpha.32", 17 | "solhint": "^2.2.0", 18 | "solhint-plugin-prettier": "0.0.3", 19 | "truffle-assertions": "^0.9.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /solhint.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": ["solhint:recommended"], 4 | "rules": { 5 | "prettier/prettier": "error", 6 | "avoid-throw": false, 7 | "avoid-suicide": "error", 8 | "avoid-sha3": "error" 9 | }, 10 | "plugins": ["prettier"] 11 | } -------------------------------------------------------------------------------- /test/staking-addWithdrawalRoot.js: -------------------------------------------------------------------------------- 1 | const BN = require('bn.js'); 2 | const Utils = require("./utils"); 3 | 4 | const Staking = artifacts.require("Staking"); 5 | const LocalFXCToken = artifacts.require("LocalFXCToken"); 6 | 7 | contract("Staking - addWithdrawalRoot", accounts => { 8 | const owner = accounts[0]; 9 | const fallbackPublisherAddress = accounts[1]; 10 | const withdrawalPublisherAddress = accounts[2]; 11 | const immediatelyWithdrawableLimitPublisher = accounts[3]; 12 | 13 | let stakingContract; 14 | let fxcToken; 15 | 16 | beforeEach(async () => { 17 | fxcToken = await LocalFXCToken.new(); 18 | stakingContract = await Staking.new( 19 | fxcToken.address, 20 | fallbackPublisherAddress, 21 | withdrawalPublisherAddress, 22 | immediatelyWithdrawableLimitPublisher 23 | ); 24 | }); 25 | 26 | describe("execution", () => { 27 | it("should add a new root", async () => { 28 | const oldMaxNonce = await stakingContract._maxWithdrawalRootNonce(); 29 | const rootToSet = Utils.keccak256('new withdrawal root'); 30 | 31 | await Utils.addWithdrawalRoot( 32 | withdrawalPublisherAddress, 33 | stakingContract, 34 | rootToSet, 35 | oldMaxNonce.add(new BN(1)), 36 | [] 37 | ); 38 | }); 39 | 40 | it("should remove previous roots", async () => { 41 | const oldMaxNonce = await stakingContract._maxWithdrawalRootNonce(); 42 | 43 | const firstRoot = Utils.keccak256('new withdrawal root'); 44 | await Utils.addWithdrawalRoot( 45 | withdrawalPublisherAddress, 46 | stakingContract, 47 | firstRoot, 48 | oldMaxNonce.add(new BN(1)), 49 | [] 50 | ); 51 | 52 | const secondRoot = Utils.keccak256('new withdrawal root 2'); 53 | await Utils.addWithdrawalRoot( 54 | withdrawalPublisherAddress, 55 | stakingContract, 56 | secondRoot, 57 | oldMaxNonce.add(new BN(2)), 58 | [] 59 | ); 60 | 61 | const third = Utils.keccak256('new withdrawal root 3'); 62 | await Utils.addWithdrawalRoot( 63 | withdrawalPublisherAddress, 64 | stakingContract, 65 | third, 66 | oldMaxNonce.add(new BN(3)), 67 | [firstRoot, secondRoot] 68 | ); 69 | }); 70 | 71 | it("should remove previous roots out of order", async () => { 72 | const oldMaxNonce = await stakingContract._maxWithdrawalRootNonce(); 73 | 74 | const firstRoot = Utils.keccak256('new withdrawal root'); 75 | await Utils.addWithdrawalRoot( 76 | withdrawalPublisherAddress, 77 | stakingContract, 78 | firstRoot, 79 | oldMaxNonce.add(new BN(1)), 80 | [] 81 | ); 82 | 83 | const secondRoot = Utils.keccak256('new withdrawal root 2'); 84 | await Utils.addWithdrawalRoot( 85 | withdrawalPublisherAddress, 86 | stakingContract, 87 | secondRoot, 88 | oldMaxNonce.add(new BN(2)), 89 | [] 90 | ); 91 | 92 | const third = Utils.keccak256('new withdrawal root 3'); 93 | await Utils.addWithdrawalRoot( 94 | withdrawalPublisherAddress, 95 | stakingContract, 96 | third, 97 | oldMaxNonce.add(new BN(3)), 98 | [secondRoot, firstRoot] 99 | ); 100 | }); 101 | }); 102 | 103 | describe("permissions", () => { 104 | it("should allow owner to add a new root", async () => { 105 | const oldMaxNonce = await stakingContract._maxWithdrawalRootNonce(); 106 | const rootToSet = Utils.keccak256('new withdrawal root'); 107 | 108 | await Utils.addWithdrawalRoot( 109 | owner, 110 | stakingContract, 111 | rootToSet, 112 | oldMaxNonce.add(new BN(1)), 113 | [] 114 | ); 115 | }); 116 | 117 | it("should disallow non-owner non-withdrawalPublisher to add a new root", async () => { 118 | const oldMaxNonce = await stakingContract._maxWithdrawalRootNonce(); 119 | const rootToSet = Utils.keccak256('new withdrawal root'); 120 | 121 | try { 122 | await Utils.addWithdrawalRoot( 123 | owner, 124 | stakingContract, 125 | rootToSet, 126 | oldMaxNonce.add(new BN(1)), 127 | [] 128 | ); 129 | } catch(e) { 130 | assert( 131 | e.message.includes("Only the owner and withdrawal publisher can add and replace withdrawal root hashes"), 132 | `Error thrown does not match exepcted error ${e.message}` 133 | ); 134 | } 135 | }); 136 | }); 137 | 138 | describe("constraints", () => { 139 | it("should require new root to be 1 greater than last root", async () => { 140 | const oldMaxNonce = await stakingContract._maxWithdrawalRootNonce(); 141 | const rootToSet = Utils.keccak256('new withdrawal root'); 142 | 143 | try { 144 | await Utils.addWithdrawalRoot( 145 | owner, 146 | stakingContract, 147 | rootToSet, 148 | oldMaxNonce.add(new BN(2)), 149 | [] 150 | ); 151 | } catch(e) { 152 | assert( 153 | e.message.includes("Nonce must be exactly max nonce + 1"), 154 | `Error thrown does not match exepcted error ${e.message}` 155 | ); 156 | } 157 | }); 158 | 159 | it("should not allow the same root to be pulbished twice", async () => { 160 | const oldMaxNonce = await stakingContract._maxWithdrawalRootNonce(); 161 | const rootToSet = Utils.keccak256('new withdrawal root'); 162 | await Utils.addWithdrawalRoot( 163 | owner, 164 | stakingContract, 165 | rootToSet, 166 | oldMaxNonce.add(new BN(1)), 167 | [] 168 | ); 169 | 170 | try { 171 | await Utils.addWithdrawalRoot( 172 | owner, 173 | stakingContract, 174 | rootToSet, 175 | oldMaxNonce.add(new BN(2)), 176 | [] 177 | ); 178 | } catch(e) { 179 | assert( 180 | e.message.includes("Root already exists and is associated with a different nonce"), 181 | `Error thrown does not match exepcted error ${e.message}` 182 | ); 183 | } 184 | }); 185 | 186 | it("should not allow a 0 root to be added", async () => { 187 | const oldMaxNonce = await stakingContract._maxWithdrawalRootNonce(); 188 | const rootToSet = Utils.getEmptyBytes32(); 189 | 190 | try { 191 | await Utils.addWithdrawalRoot( 192 | withdrawalPublisherAddress, 193 | stakingContract, 194 | rootToSet, 195 | oldMaxNonce.add(new BN(1)), 196 | [] 197 | ); 198 | } catch(e) { 199 | assert( 200 | e.message.includes("Added root may not be 0"), 201 | `Error thrown does not match exepcted error ${e.message}` 202 | ); 203 | } 204 | }); 205 | }); 206 | }); -------------------------------------------------------------------------------- /test/staking-assumeOwnership.js: -------------------------------------------------------------------------------- 1 | const Utils = require("./utils"); 2 | const truffleAssert = require('truffle-assertions'); 3 | 4 | const Staking = artifacts.require("Staking"); 5 | const LocalFXCToken = artifacts.require("LocalFXCToken"); 6 | 7 | contract("Staking - authorizeOwnershipTransfer", accounts => { 8 | const owner = accounts[0]; 9 | const fallbackPublisherAddress = accounts[1]; 10 | const withdrawalPublisherAddress = accounts[2]; 11 | const immediatelyWithdrawableLimitPublisher = accounts[3]; 12 | 13 | const authorized = accounts[5]; 14 | 15 | let stakingContract; 16 | let fxcToken; 17 | 18 | beforeEach(async () => { 19 | fxcToken = await LocalFXCToken.new(); 20 | stakingContract = await Staking.new( 21 | fxcToken.address, 22 | fallbackPublisherAddress, 23 | withdrawalPublisherAddress, 24 | immediatelyWithdrawableLimitPublisher 25 | ); 26 | 27 | await stakingContract.authorizeOwnershipTransfer(authorized); 28 | }); 29 | 30 | it("should assume ownership", async () => { 31 | const currentOwner = await stakingContract._owner(); 32 | 33 | const receipt = await stakingContract.assumeOwnership({from: authorized}); 34 | 35 | truffleAssert.eventEmitted(receipt, 'OwnerUpdate', (ev) => { 36 | return ev.oldValue === currentOwner && 37 | ev.newValue === authorized; 38 | }); 39 | 40 | const ownerAfter = await stakingContract._owner(); 41 | assert.notEqual(currentOwner, ownerAfter, 'Owner should be updated!'); 42 | assert.equal(ownerAfter, authorized, "New owner is not authorized new owner!"); 43 | 44 | const authorizedNewOwnerAfter = await stakingContract._authorizedNewOwner(); 45 | assert.equal(authorizedNewOwnerAfter, Utils.getNullAddress(), 'Authorized new owner should be null address!'); 46 | 47 | }); 48 | 49 | it("should fail update if not authorized new owner", async () => { 50 | try { 51 | await stakingContract.assumeOwnership({from: owner}); 52 | assert(false, "This should have thrown"); 53 | } catch(e) { 54 | assert( 55 | e.message.includes("Only the authorized new owner can accept ownership"), 56 | `Error thrown does not match exepcted error ${e.message}` 57 | ); 58 | } 59 | }); 60 | }) -------------------------------------------------------------------------------- /test/staking-authorizeOwnershipTransfer.js: -------------------------------------------------------------------------------- 1 | const truffleAssert = require('truffle-assertions'); 2 | 3 | const Staking = artifacts.require("Staking"); 4 | const LocalFXCToken = artifacts.require("LocalFXCToken"); 5 | 6 | contract("Staking - authorizeOwnershipTransfer", accounts => { 7 | const fallbackPublisherAddress = accounts[1]; 8 | const withdrawalPublisherAddress = accounts[2]; 9 | const immediatelyWithdrawableLimitPublisher = accounts[3]; 10 | 11 | let stakingContract; 12 | let fxcToken; 13 | 14 | beforeEach(async () => { 15 | fxcToken = await LocalFXCToken.new(); 16 | stakingContract = await Staking.new( 17 | fxcToken.address, 18 | fallbackPublisherAddress, 19 | withdrawalPublisherAddress, 20 | immediatelyWithdrawableLimitPublisher 21 | ); 22 | }); 23 | 24 | it("should authorize new owner", async () => { 25 | const currentOwner = await stakingContract._owner(); 26 | const authorizedNewOwner = await stakingContract._authorizedNewOwner(); 27 | 28 | const receipt = await stakingContract.authorizeOwnershipTransfer(accounts[3]); 29 | 30 | truffleAssert.eventEmitted(receipt, 'OwnershipTransferAuthorization', (ev) => { 31 | return ev.authorizedAddress === accounts[3]; 32 | }); 33 | 34 | const ownerAfter = await stakingContract._owner(); 35 | assert.equal(currentOwner, ownerAfter, 'Owner should not be updated!'); 36 | 37 | const authorizedNewOwnerAfter = await stakingContract._authorizedNewOwner(); 38 | assert.notEqual(authorizedNewOwner, authorizedNewOwnerAfter, 'Authorized new owner should have been updated!'); 39 | assert.equal(authorizedNewOwnerAfter, accounts[3], "Authorized new owner does not match account it was updated to!"); 40 | }); 41 | 42 | it("should fail update if not owner", async () => { 43 | try { 44 | await stakingContract.authorizeOwnershipTransfer(accounts[3], {from: accounts[1]}); 45 | assert(false, "This should have thrown"); 46 | } catch(e) { 47 | assert( 48 | e.message.includes("Only the owner can authorize a new address to become owner"), 49 | `Error thrown does not match exepcted error ${e.message}` 50 | ); 51 | } 52 | }); 53 | }) -------------------------------------------------------------------------------- /test/staking-deposit.js: -------------------------------------------------------------------------------- 1 | const BN = require('bn.js'); 2 | const Utils = require("./utils"); 3 | 4 | const Staking = artifacts.require("Staking"); 5 | const LocalFXCToken = artifacts.require("LocalFXCToken"); 6 | 7 | contract("Staking - Deposit", accounts => { 8 | const owner = accounts[0]; 9 | const fallbackPublisherAddress = accounts[1]; 10 | const withdrawalPublisherAddress = accounts[2]; 11 | const immediatelyWithdrawableLimitPublisher = accounts[3]; 12 | 13 | let stakingContract; 14 | let fxcToken; 15 | 16 | beforeEach(async () => { 17 | fxcToken = await LocalFXCToken.new(); 18 | stakingContract = await Staking.new( 19 | fxcToken.address, 20 | fallbackPublisherAddress, 21 | withdrawalPublisherAddress, 22 | immediatelyWithdrawableLimitPublisher 23 | ); 24 | }); 25 | 26 | describe("execution", () => { 27 | it("should deposit successfully", async () => { 28 | const nonce = await Utils.approveAndDeposit( 29 | owner, 30 | fxcToken, 31 | stakingContract, 32 | 100 33 | ); 34 | 35 | const pendingDeposit = await stakingContract._nonceToPendingDeposit(nonce); 36 | assert.equal( 37 | owner, pendingDeposit.depositor, 38 | "Pending Deposit depositor should be owner!" 39 | ); 40 | assert( 41 | new BN(100).eq(pendingDeposit.amount), 42 | "Pending deposit amount should match deposited amount!" 43 | ); 44 | }); 45 | }); 46 | 47 | describe("constraints", () => { 48 | it("should fail if amount is 0", async () => { 49 | const nonceBefore = await stakingContract._depositNonce(); 50 | try { 51 | await stakingContract.deposit(0, {from: owner}); 52 | assert(false, "This should have thrown"); 53 | } catch(e) { 54 | assert( 55 | e.message.includes("Cannot deposit 0"), 56 | `Error thrown does not match exepcted error ${e.message}` 57 | ); 58 | } 59 | const nonceAfter = await stakingContract._depositNonce(); 60 | 61 | assert( 62 | nonceBefore.eq(nonceAfter), 63 | "Nonce should not have increased!" 64 | ); 65 | }); 66 | 67 | it("should fail if transfer is not approved", async () => { 68 | const nonceBefore = await stakingContract._depositNonce(); 69 | try { 70 | await stakingContract.deposit(100, {from: owner}); 71 | assert(false, "This should have thrown"); 72 | } catch(e) { 73 | // Transfer without approval makes the approaved balance go below 0. 74 | assert( 75 | e.message.includes("SafeMath: subtraction overflow"), 76 | `Error thrown does not match exepcted error ${e.message}` 77 | ); 78 | } 79 | const nonceAfter = await stakingContract._depositNonce(); 80 | 81 | assert( 82 | nonceBefore.eq(nonceAfter), 83 | "Nonce should not have increased!" 84 | ); 85 | }); 86 | }) 87 | }); -------------------------------------------------------------------------------- /test/staking-modifyImmediatelyWithdrawableLimit.js: -------------------------------------------------------------------------------- 1 | const BN = require('bn.js'); 2 | const Utils = require("./utils"); 3 | 4 | const Staking = artifacts.require("Staking"); 5 | const LocalFXCToken = artifacts.require("LocalFXCToken"); 6 | 7 | contract("Staking - modifyImmediatelyWithdrawableLimit", accounts => { 8 | const owner = accounts[0]; 9 | const fallbackPublisherAddress = accounts[1]; 10 | const withdrawalPublisherAddress = accounts[2]; 11 | const immediatelyWithdrawableLimitPublisher = accounts[3]; 12 | 13 | let stakingContract; 14 | let fxcToken; 15 | 16 | beforeEach(async () => { 17 | fxcToken = await LocalFXCToken.new(); 18 | stakingContract = await Staking.new( 19 | fxcToken.address, 20 | fallbackPublisherAddress, 21 | withdrawalPublisherAddress, 22 | immediatelyWithdrawableLimitPublisher 23 | ); 24 | }); 25 | 26 | describe("execution", () => { 27 | it("should add to immediately withdrawable limit", async () => { 28 | await Utils.modifyImmediatelyWithdrawableLimit( 29 | immediatelyWithdrawableLimitPublisher, 30 | new BN(10), 31 | stakingContract 32 | ); 33 | }); 34 | 35 | it("should subtract from immediately withdrawable limit", async () => { 36 | await Utils.modifyImmediatelyWithdrawableLimit( 37 | immediatelyWithdrawableLimitPublisher, 38 | new BN(-10), 39 | stakingContract 40 | ); 41 | }); 42 | }); 43 | 44 | describe("permissions", () => { 45 | it("should allow owner to update immediately withdrawable limit", async () => { 46 | await Utils.modifyImmediatelyWithdrawableLimit( 47 | owner, 48 | new BN(-10), 49 | stakingContract 50 | ); 51 | }); 52 | 53 | it("should fail update if not publisher or owner", async () => { 54 | try { 55 | await Utils.modifyImmediatelyWithdrawableLimit( 56 | accounts[1], 57 | new BN(-10), 58 | stakingContract 59 | ); 60 | assert(false, "This should have thrown"); 61 | } catch(e) { 62 | assert( 63 | e.message.includes("Only the immediately withdrawable limit publisher and owner can modify the immediately withdrawable limit"), 64 | `Error thrown does not match exepcted error ${e.message}` 65 | ); 66 | } 67 | }); 68 | }); 69 | 70 | describe("constraints", () => { 71 | it("should not allow underflow", async () => { 72 | const oldLimit = await stakingContract._immediatelyWithdrawableLimit(); 73 | const modifyValue = oldLimit.neg().sub(new BN(1)) 74 | try { 75 | await Utils.modifyImmediatelyWithdrawableLimit( 76 | immediatelyWithdrawableLimitPublisher, 77 | modifyValue, 78 | stakingContract 79 | ); 80 | assert(false, "This should have thrown"); 81 | } catch(e) { 82 | assert( 83 | e.message.includes("SafeMath: subtraction overflow"), 84 | `Error thrown does not match exepcted error ${e.message}` 85 | ); 86 | } 87 | }); 88 | 89 | it("should not allow overflow", async () => { 90 | const modifyValue = (new BN(2)).pow(new BN(255)).sub(new BN(1)) 91 | await Utils.modifyImmediatelyWithdrawableLimit( 92 | immediatelyWithdrawableLimitPublisher, 93 | modifyValue, 94 | stakingContract 95 | ); 96 | try { 97 | await Utils.modifyImmediatelyWithdrawableLimit( 98 | immediatelyWithdrawableLimitPublisher, 99 | modifyValue, 100 | stakingContract 101 | ); 102 | assert(false, "This should have thrown"); 103 | } catch(e) { 104 | assert( 105 | e.message.includes("SafeMath: addition overflow"), 106 | `Error thrown does not match exepcted error ${e.message}` 107 | ); 108 | } 109 | }); 110 | }); 111 | }); -------------------------------------------------------------------------------- /test/staking-refundPendingDeposit.js: -------------------------------------------------------------------------------- 1 | const BN = require('bn.js'); 2 | const Utils = require("./utils"); 3 | 4 | const Staking = artifacts.require("Staking"); 5 | const LocalFXCToken = artifacts.require("LocalFXCToken"); 6 | 7 | contract("Staking - refundPendingDeposit", async accounts => { 8 | await Utils.moveTimeForwardSecondsAndMineBlock(1); 9 | 10 | const owner = accounts[0]; 11 | const fallbackPublisherAddress = accounts[1]; 12 | const withdrawalPublisherAddress = accounts[2]; 13 | const immediatelyWithdrawableLimitPublisher = accounts[3]; 14 | const account = accounts[4]; 15 | 16 | let stakingContract; 17 | let fxcToken; 18 | 19 | beforeEach(async () => { 20 | fxcToken = await LocalFXCToken.new(); 21 | stakingContract = await Staking.new( 22 | fxcToken.address, 23 | fallbackPublisherAddress, 24 | withdrawalPublisherAddress, 25 | immediatelyWithdrawableLimitPublisher 26 | ); 27 | 28 | await stakingContract.setFallbackRoot( 29 | Utils.keccak256("invalid"), 30 | 0, 31 | {from: fallbackPublisherAddress} 32 | ); 33 | 34 | 35 | await Utils.setFallbackWithdrawalDelay(stakingContract, 1); 36 | await fxcToken.transfer(account, 100, {from: owner}); 37 | 38 | await Utils.moveTimeForwardSecondsAndMineBlock(5); 39 | }); 40 | 41 | describe("execution", () => { 42 | it("should refund pending deposit", async () => { 43 | const depositAmount = new BN(50); 44 | const nonce = await Utils.approveAndDeposit(account, fxcToken, stakingContract, depositAmount.toNumber()); 45 | 46 | await Utils.executePendingDepositRefund(account, account, fxcToken, stakingContract, depositAmount, nonce); 47 | }); 48 | 49 | it("should refund both pending deposits when multiple pending", async () => { 50 | const depositAmount = new BN(50); 51 | const firstNonce = await Utils.approveAndDeposit(account, fxcToken, stakingContract, depositAmount.toNumber()); 52 | const secondNonce = await Utils.approveAndDeposit(account, fxcToken, stakingContract, depositAmount.toNumber()); 53 | 54 | await Utils.executePendingDepositRefund(account, account, fxcToken, stakingContract, depositAmount, firstNonce); 55 | await Utils.executePendingDepositRefund(account, account, fxcToken, stakingContract, depositAmount, secondNonce); 56 | }); 57 | 58 | it("should refund both pending deposits out of order when multiple pending", async () => { 59 | const depositAmount = new BN(50); 60 | const firstNonce = await Utils.approveAndDeposit(account, fxcToken, stakingContract, depositAmount.toNumber()); 61 | const secondNonce = await Utils.approveAndDeposit(account, fxcToken, stakingContract, depositAmount.toNumber()); 62 | 63 | await Utils.executePendingDepositRefund(account, account, fxcToken, stakingContract, depositAmount, secondNonce); 64 | await Utils.executePendingDepositRefund(account, account, fxcToken, stakingContract, depositAmount, firstNonce); 65 | }); 66 | }); 67 | 68 | describe("permissions", () => { 69 | it("should permit owner to refund", async () => { 70 | const depositAmount = new BN(50); 71 | const nonce = await Utils.approveAndDeposit(account, fxcToken, stakingContract, depositAmount.toNumber()); 72 | 73 | Utils.executePendingDepositRefund(owner, account, fxcToken, stakingContract, depositAmount, nonce); 74 | }); 75 | 76 | it("should disallow non-owner, non-depositor to refund", async () => { 77 | const depositAmount = new BN(50); 78 | const nonce = await Utils.approveAndDeposit(account, fxcToken, stakingContract, depositAmount.toNumber()); 79 | 80 | try { 81 | await Utils.executePendingDepositRefund(accounts[5], account, fxcToken, stakingContract, depositAmount, nonce); 82 | assert(false, "This should have thrown"); 83 | } catch(e) { 84 | assert( 85 | e.message.includes("Only the owner or depositor can initiate the refund of a pending deposit"), 86 | `Error thrown does not match exepcted error ${e.message}` 87 | ); 88 | } 89 | }); 90 | }); 91 | 92 | describe("constraints", () => { 93 | it("should disallow refunds when fallback mechanism is not active", async () => { 94 | await Utils.setFallbackWithdrawalDelay(stakingContract, 100); 95 | 96 | const depositAmount = new BN(50); 97 | const nonce = await Utils.approveAndDeposit(account, fxcToken, stakingContract, depositAmount.toNumber()); 98 | 99 | try { 100 | await Utils.executePendingDepositRefund(account, account, fxcToken, stakingContract, depositAmount, nonce); 101 | assert(false, "This should have thrown"); 102 | } catch(e) { 103 | assert( 104 | e.message.includes("Fallback withdrawal period is not active, so refunds are not permitted"), 105 | `Error thrown does not match exepcted error ${e.message}` 106 | ); 107 | } 108 | }); 109 | 110 | it("should disallow refund of deposit included in fallback root tree", async () => { 111 | const depositAmount = new BN(50); 112 | const nonce = await Utils.approveAndDeposit(account, fxcToken, stakingContract, depositAmount.toNumber()); 113 | 114 | await Utils.setFallbackWithdrawalDelay(stakingContract, 10000); 115 | await Utils.moveTimeForwardSecondsAndMineBlock(1); 116 | 117 | await stakingContract.setFallbackRoot( 118 | Utils.keccak256("invalid"), 119 | 1, 120 | {from: fallbackPublisherAddress} 121 | ); 122 | 123 | await Utils.setFallbackWithdrawalDelay(stakingContract, 1); 124 | await Utils.moveTimeForwardSecondsAndMineBlock(5); 125 | 126 | try { 127 | await Utils.executePendingDepositRefund(account, account, fxcToken, stakingContract, depositAmount, nonce); 128 | assert(false, "This should have thrown"); 129 | } catch(e) { 130 | assert( 131 | e.message.includes("There is no pending deposit for the specified nonce"), 132 | `Error thrown does not match exepcted error ${e.message}` 133 | ); 134 | } 135 | }); 136 | }); 137 | }); -------------------------------------------------------------------------------- /test/staking-removeWithdrawalRoots.js: -------------------------------------------------------------------------------- 1 | const BN = require('bn.js'); 2 | const Utils = require("./utils"); 3 | 4 | const Staking = artifacts.require("Staking"); 5 | const LocalFXCToken = artifacts.require("LocalFXCToken"); 6 | 7 | contract("Staking - removeWithdrawalRoots", accounts => { 8 | const owner = accounts[0]; 9 | const fallbackPublisherAddress = accounts[1]; 10 | const withdrawalPublisherAddress = accounts[2]; 11 | const immediatelyWithdrawableLimitPublisher = accounts[3]; 12 | 13 | let stakingContract; 14 | let fxcToken; 15 | 16 | let rootOne; 17 | let rootTwo; 18 | 19 | beforeEach(async () => { 20 | fxcToken = await LocalFXCToken.new(); 21 | stakingContract = await Staking.new( 22 | fxcToken.address, 23 | fallbackPublisherAddress, 24 | withdrawalPublisherAddress, 25 | immediatelyWithdrawableLimitPublisher 26 | ); 27 | 28 | const oldMaxNonce = await stakingContract._maxWithdrawalRootNonce(); 29 | rootOne = Utils.keccak256('new withdrawal root'); 30 | 31 | await Utils.addWithdrawalRoot( 32 | withdrawalPublisherAddress, 33 | stakingContract, 34 | rootOne, 35 | oldMaxNonce.add(new BN(1)) 36 | ); 37 | 38 | rootTwo = Utils.keccak256('new withdrawal root 2'); 39 | 40 | await Utils.addWithdrawalRoot( 41 | withdrawalPublisherAddress, 42 | stakingContract, 43 | rootTwo, 44 | oldMaxNonce.add(new BN(2)) 45 | ); 46 | }); 47 | 48 | describe("execution", () => { 49 | it("should remove first withdrawal root", async () => { 50 | await Utils.removeWithdrawalRoots( 51 | withdrawalPublisherAddress, 52 | [rootOne], 53 | stakingContract 54 | ); 55 | }); 56 | 57 | it("should remove second withdrawal root", async () => { 58 | await Utils.removeWithdrawalRoots( 59 | withdrawalPublisherAddress, 60 | [rootTwo], 61 | stakingContract 62 | ); 63 | }); 64 | 65 | it("should remove both withdrawal roots", async () => { 66 | await Utils.removeWithdrawalRoots( 67 | withdrawalPublisherAddress, 68 | [rootOne, rootTwo], 69 | stakingContract 70 | ); 71 | }); 72 | 73 | it("should remove both withdrawal roots out of order", async () => { 74 | await Utils.removeWithdrawalRoots( 75 | withdrawalPublisherAddress, 76 | [rootTwo, rootOne], 77 | stakingContract 78 | ); 79 | }); 80 | }); 81 | 82 | describe("permissions", () => { 83 | it("should allow owner to add a new root", async () => { 84 | await Utils.removeWithdrawalRoots( 85 | owner, 86 | [rootOne], 87 | stakingContract 88 | ); 89 | }); 90 | 91 | it("should disallow non-owner non-withdrawalPublisher to add a new root", async () => { 92 | try { 93 | await Utils.removeWithdrawalRoots( 94 | fallbackPublisherAddress, 95 | [rootOne], 96 | stakingContract 97 | ); 98 | } catch(e) { 99 | assert( 100 | e.message.includes("Only the owner and withdrawal publisher can remove withdrawal root hashes"), 101 | `Error thrown does not match exepcted error ${e.message}` 102 | ); 103 | } 104 | }); 105 | }); 106 | }); -------------------------------------------------------------------------------- /test/staking-renounceWithdrawalAuthorization.js: -------------------------------------------------------------------------------- 1 | const BN = require('bn.js'); 2 | const truffleAssert = require('truffle-assertions'); 3 | const Utils = require("./utils"); 4 | 5 | const Staking = artifacts.require("Staking"); 6 | const LocalFXCToken = artifacts.require("LocalFXCToken"); 7 | 8 | contract("Staking - renounceWithdrawalAuthorization", accounts => { 9 | const owner = accounts[0]; 10 | const fallbackPublisherAddress = accounts[1]; 11 | const withdrawalPublisherAddress = accounts[2]; 12 | const immediatelyWithdrawableLimitPublisher = accounts[3]; 13 | 14 | let stakingContract; 15 | let fxcToken; 16 | 17 | beforeEach(async () => { 18 | fxcToken = await LocalFXCToken.new(); 19 | stakingContract = await Staking.new( 20 | fxcToken.address, 21 | fallbackPublisherAddress, 22 | withdrawalPublisherAddress, 23 | immediatelyWithdrawableLimitPublisher 24 | ); 25 | 26 | await stakingContract.addWithdrawalRoot( 27 | Utils.keccak256("invalid"), 28 | 1, 29 | [], 30 | {from: withdrawalPublisherAddress} 31 | ); 32 | }); 33 | 34 | describe("execution", () => { 35 | it("should renounce for sender", async () => { 36 | const previousNonce = await stakingContract._addressToWithdrawalNonce(accounts[4]); 37 | const receipt = await stakingContract.renounceWithdrawalAuthorization(accounts[4], {from: accounts[4]}); 38 | 39 | truffleAssert.eventEmitted(receipt, 'RenounceWithdrawalAuthorization', (ev) => { 40 | return ev.forAddress === accounts[4]; 41 | }); 42 | 43 | const withdrawalNonce = await stakingContract._addressToWithdrawalNonce(accounts[4]); 44 | assert(previousNonce.lt(withdrawalNonce), "Withdrawal nonce should have increased!") 45 | assert(withdrawalNonce.eq(new BN(1)), "Withdrawal nonce should be 1!") 46 | }); 47 | }); 48 | 49 | describe("permissions", () => { 50 | it("should allow owner to stake anybody", async () => { 51 | const previousNonce = await stakingContract._addressToWithdrawalNonce(accounts[4]); 52 | const receipt = await stakingContract.renounceWithdrawalAuthorization(accounts[4], {from: withdrawalPublisherAddress}); 53 | 54 | truffleAssert.eventEmitted(receipt, 'RenounceWithdrawalAuthorization', (ev) => { 55 | return ev.forAddress === accounts[4]; 56 | }); 57 | 58 | const withdrawalNonce = await stakingContract._addressToWithdrawalNonce(accounts[4]); 59 | assert(previousNonce.lt(withdrawalNonce), "Withdrawal nonce should have increased!") 60 | assert(withdrawalNonce.eq(new BN(1)), "Withdrawal nonce should be 1!") 61 | }); 62 | 63 | it("should allow withdrawal publisher to stake anybody", async () => { 64 | const previousNonce = await stakingContract._addressToWithdrawalNonce(accounts[4]); 65 | const receipt = await stakingContract.renounceWithdrawalAuthorization(accounts[4], {from: owner}); 66 | 67 | truffleAssert.eventEmitted(receipt, 'RenounceWithdrawalAuthorization', (ev) => { 68 | return ev.forAddress === accounts[4]; 69 | }); 70 | 71 | const withdrawalNonce = await stakingContract._addressToWithdrawalNonce(accounts[4]); 72 | assert(previousNonce.lt(withdrawalNonce), "Withdrawal nonce should have increased!") 73 | assert(withdrawalNonce.eq(new BN(1)), "Withdrawal nonce should be 1!") 74 | }); 75 | 76 | it("should fail if from address is not sender and sender is not owner", async () => { 77 | try { 78 | await stakingContract.renounceWithdrawalAuthorization(accounts[5], {from: accounts[4]}); 79 | assert(false, "This should have thrown"); 80 | } catch(e) { 81 | assert( 82 | e.message.includes("Only the owner, withdrawal publisher, and address in question can renounce a withdrawal authorization"), 83 | `Error thrown does not match exepcted error ${e.message}` 84 | ); 85 | } 86 | }); 87 | }); 88 | 89 | describe("constraints", () => { 90 | it("should fail to stake if nonce >= max withdrawal nonce", async () => { 91 | const previousNonce = await stakingContract._addressToWithdrawalNonce(accounts[4]); 92 | const receipt = await stakingContract.renounceWithdrawalAuthorization(accounts[4], {from: accounts[4]}); 93 | 94 | truffleAssert.eventEmitted(receipt, 'RenounceWithdrawalAuthorization', (ev) => { 95 | return ev.forAddress === accounts[4]; 96 | }); 97 | 98 | const withdrawalNonce = await stakingContract._addressToWithdrawalNonce(accounts[4]); 99 | assert(previousNonce.lt(withdrawalNonce), "Withdrawal nonce should have increased!") 100 | assert(withdrawalNonce.eq(new BN(1)), "Withdrawal nonce should be 1!") 101 | 102 | 103 | try { 104 | await stakingContract.renounceWithdrawalAuthorization(accounts[4], {from: accounts[4]}); 105 | assert(false, "This should have thrown"); 106 | } catch(e) { 107 | assert( 108 | e.message.includes("Address nonce indicates there are no funds withdrawable"), 109 | `Error thrown does not match exepcted error ${e.message}` 110 | ); 111 | } 112 | }); 113 | }); 114 | }); -------------------------------------------------------------------------------- /test/staking-resetFallbackMechanismDate.js: -------------------------------------------------------------------------------- 1 | const Utils = require("./utils"); 2 | 3 | const Staking = artifacts.require("Staking"); 4 | const LocalFXCToken = artifacts.require("LocalFXCToken"); 5 | 6 | contract("Staking - resetFallbackMechanismDate", accounts => { 7 | const owner = accounts[0]; 8 | const fallbackPublisherAddress = accounts[1]; 9 | const withdrawalPublisherAddress = accounts[2]; 10 | const immediatelyWithdrawableLimitPublisher = accounts[3]; 11 | 12 | let stakingContract; 13 | let fxcToken; 14 | 15 | beforeEach(async () => { 16 | fxcToken = await LocalFXCToken.new(); 17 | stakingContract = await Staking.new( 18 | fxcToken.address, 19 | fallbackPublisherAddress, 20 | withdrawalPublisherAddress, 21 | immediatelyWithdrawableLimitPublisher 22 | ); 23 | }); 24 | 25 | describe("execution", () => { 26 | it("should reset fallback date", async () => { 27 | await Utils.resetFallbackMechanismDate(fallbackPublisherAddress, stakingContract); 28 | }); 29 | }); 30 | 31 | describe("permissions", () => { 32 | it("should allow owner to reset fallback date", async () => { 33 | await Utils.resetFallbackMechanismDate(owner, stakingContract); 34 | }); 35 | 36 | it("should disallow non-owner non-fallbackPublisher to reset fallback date", async () => { 37 | try { 38 | await Utils.resetFallbackMechanismDate(withdrawalPublisherAddress, stakingContract); 39 | } catch(e) { 40 | assert( 41 | e.message.includes("Only the owner and fallback publisher can reset fallback mechanism date"), 42 | `Error thrown does not match exepcted error ${e.message}` 43 | ); 44 | } 45 | }); 46 | }); 47 | }); -------------------------------------------------------------------------------- /test/staking-setFallbackRoot.js: -------------------------------------------------------------------------------- 1 | const BN = require('bn.js'); 2 | const Utils = require("./utils"); 3 | 4 | const Staking = artifacts.require("Staking"); 5 | const LocalFXCToken = artifacts.require("LocalFXCToken"); 6 | 7 | contract("Staking - setFallbackRoot", accounts => { 8 | const owner = accounts[0]; 9 | const fallbackPublisherAddress = accounts[1]; 10 | const withdrawalPublisherAddress = accounts[2]; 11 | const immediatelyWithdrawableLimitPublisher = accounts[3]; 12 | 13 | let stakingContract; 14 | let fxcToken; 15 | 16 | beforeEach(async () => { 17 | fxcToken = await LocalFXCToken.new(); 18 | stakingContract = await Staking.new( 19 | fxcToken.address, 20 | fallbackPublisherAddress, 21 | withdrawalPublisherAddress, 22 | immediatelyWithdrawableLimitPublisher 23 | ); 24 | 25 | // Bump deposit nonce so we can invalidate it. 26 | await Utils.approveAndDeposit( 27 | owner, 28 | fxcToken, 29 | stakingContract, 30 | 100 31 | ); 32 | }); 33 | 34 | describe("execution", () => { 35 | it("should set fallback root", async () => { 36 | const maxDepositIncluded = await stakingContract._fallbackMaxDepositIncluded(); 37 | 38 | const rootToSet = Utils.keccak256('new fallback root'); 39 | 40 | await Utils.setFallbackRoot( 41 | fallbackPublisherAddress, 42 | stakingContract, 43 | rootToSet, 44 | maxDepositIncluded.add(new BN(1)), 45 | ); 46 | }); 47 | }); 48 | 49 | describe("permissions", () => { 50 | it("should allow owner to add a new root", async () => { 51 | const maxDepositIncluded = await stakingContract._fallbackMaxDepositIncluded(); 52 | 53 | const rootToSet = Utils.keccak256('new fallback root'); 54 | 55 | await Utils.setFallbackRoot( 56 | owner, 57 | stakingContract, 58 | rootToSet, 59 | maxDepositIncluded.add(new BN(1)), 60 | ); 61 | }); 62 | 63 | it("should disallow non-owner non-fallbackPublisher to add a new root", async () => { 64 | const maxDepositIncluded = await stakingContract._fallbackMaxDepositIncluded(); 65 | 66 | const rootToSet = Utils.keccak256('new fallback root'); 67 | 68 | try { 69 | await Utils.setFallbackRoot( 70 | withdrawalPublisherAddress, 71 | stakingContract, 72 | rootToSet, 73 | maxDepositIncluded.add(new BN(1)), 74 | ); 75 | } catch(e) { 76 | assert( 77 | e.message.includes("Only the owner and fallback publisher can set the fallback root hash"), 78 | `Error thrown does not match exepcted error ${e.message}` 79 | ); 80 | } 81 | }); 82 | }); 83 | 84 | describe("constraints", () => { 85 | it("prevents deposit nonce from decreasing", async () => { 86 | const maxDepositIncluded = await stakingContract._fallbackMaxDepositIncluded(); 87 | 88 | const rootToSet = Utils.keccak256('new fallback root'); 89 | 90 | try { 91 | await Utils.setFallbackRoot( 92 | fallbackPublisherAddress, 93 | stakingContract, 94 | rootToSet, 95 | maxDepositIncluded.add(new BN(1)), 96 | ); 97 | } catch(e) { 98 | assert( 99 | e.message.includes("Max deposit included must remain the same or increase"), 100 | `Error thrown does not match exepcted error ${e.message}` 101 | ); 102 | } 103 | }); 104 | 105 | it("prevents invalidating future deposits", async () => { 106 | const rootToSet = Utils.keccak256('new fallback root'); 107 | const depositNonce = await stakingContract._depositNonce(); 108 | 109 | try { 110 | await Utils.setFallbackRoot( 111 | fallbackPublisherAddress, 112 | stakingContract, 113 | rootToSet, 114 | depositNonce.add(new BN(1)), 115 | ); 116 | } catch(e) { 117 | assert( 118 | e.message.includes("Cannot invalidate future deposits"), 119 | `Error thrown does not match exepcted error ${e.message}` 120 | ); 121 | } 122 | }); 123 | 124 | it("should fail if new root is 0", async () => { 125 | const maxDepositIncluded = await stakingContract._fallbackMaxDepositIncluded(); 126 | const rootToSet = Utils.getEmptyBytes32(); 127 | try { 128 | await Utils.setFallbackRoot( 129 | fallbackPublisherAddress, 130 | stakingContract, 131 | rootToSet, 132 | maxDepositIncluded.add(new BN(1)), 133 | ); 134 | } catch(e) { 135 | assert( 136 | e.message.includes("New root may not be 0"), 137 | `Error thrown does not match exepcted error ${e.message}` 138 | ); 139 | } 140 | }); 141 | 142 | it("prevents fallback root from being set if fallback mechanism is active", async () => { 143 | await stakingContract.resetFallbackMechanismDate({from: fallbackPublisherAddress}); 144 | await Utils.setFallbackWithdrawalDelay(stakingContract, 1); 145 | await Utils.moveTimeForwardSecondsAndMineBlock(5); 146 | 147 | const maxDepositIncluded = await stakingContract._fallbackMaxDepositIncluded(); 148 | const rootToSet = Utils.keccak256('new fallback root'); 149 | 150 | try { 151 | await Utils.setFallbackRoot( 152 | fallbackPublisherAddress, 153 | stakingContract, 154 | rootToSet, 155 | maxDepositIncluded.add(new BN(1)), 156 | ); 157 | } catch(e) { 158 | assert( 159 | e.message.includes("Cannot set fallback root while fallback mechanism is active"), 160 | `Error thrown does not match exepcted error ${e.message}` 161 | ); 162 | } 163 | }); 164 | }); 165 | }); -------------------------------------------------------------------------------- /test/staking-setters.js: -------------------------------------------------------------------------------- 1 | const Utils = require("./utils"); 2 | const truffleAssert = require('truffle-assertions'); 3 | 4 | const Staking = artifacts.require("Staking"); 5 | const LocalFXCToken = artifacts.require("LocalFXCToken"); 6 | 7 | contract("Staking - Setters", accounts => { 8 | const owner = accounts[0]; 9 | const fallbackPublisherAddress = accounts[1]; 10 | const withdrawalPublisherAddress = accounts[2]; 11 | const immediatelyWithdrawableLimitPublisher = accounts[3]; 12 | 13 | let stakingContract; 14 | let fxcToken; 15 | 16 | beforeEach(async () => { 17 | fxcToken = await LocalFXCToken.new(); 18 | stakingContract = await Staking.new( 19 | fxcToken.address, 20 | fallbackPublisherAddress, 21 | withdrawalPublisherAddress, 22 | immediatelyWithdrawableLimitPublisher 23 | ); 24 | }); 25 | 26 | describe("setWithdrawalPublisher", () => { 27 | it("should update publisher", async () => { 28 | const currentPublisher = await stakingContract._withdrawalPublisher(); 29 | const receipt = await stakingContract.setWithdrawalPublisher(accounts[4]); 30 | const newPublisher = await stakingContract._withdrawalPublisher(); 31 | 32 | truffleAssert.eventEmitted(receipt, 'WithdrawalPublisherUpdate', (ev) => { 33 | return ev.oldValue === currentPublisher && 34 | ev.newValue === newPublisher; 35 | }); 36 | 37 | assert.notEqual(currentPublisher, newPublisher, 'Publisher was not updated!'); 38 | assert.equal(newPublisher, accounts[4], "Publisher does not match account it was updated to!"); 39 | }); 40 | 41 | it("should fail update if not owner", async () => { 42 | try { 43 | await stakingContract.setWithdrawalPublisher(accounts[4], {from: accounts[1]}); 44 | assert(false, "This should have thrown"); 45 | } catch(e) { 46 | assert( 47 | e.message.includes("Only the owner can set the withdrawal publisher address"), 48 | `Error thrown does not match exepcted error ${e.message}` 49 | ); 50 | } 51 | }); 52 | }); 53 | 54 | describe("setFallbackPublisher", () => { 55 | it("should update publisher", async () => { 56 | const currentPublisher = await stakingContract._fallbackPublisher(); 57 | const receipt = await stakingContract.setFallbackPublisher(accounts[4]); 58 | const newPublisher = await stakingContract._fallbackPublisher(); 59 | 60 | truffleAssert.eventEmitted(receipt, 'FallbackPublisherUpdate', (ev) => { 61 | return ev.oldValue === currentPublisher && 62 | ev.newValue === newPublisher; 63 | }); 64 | 65 | assert.notEqual(currentPublisher, newPublisher, 'Publisher was not updated!'); 66 | assert.equal(newPublisher, accounts[4], "Publisher does not match account it was updated to!"); 67 | }); 68 | 69 | it("should fail update if not owner", async () => { 70 | try { 71 | await stakingContract.setFallbackPublisher(accounts[4], {from: accounts[1]}); 72 | assert(false, "This should have thrown"); 73 | } catch(e) { 74 | assert( 75 | e.message.includes("Only the owner can set the fallback publisher address"), 76 | `Error thrown does not match exepcted error ${e.message}` 77 | ); 78 | } 79 | }); 80 | }); 81 | 82 | describe("setImmediatelyWithdrawableLimitPublisher", () => { 83 | it("should update publisher", async () => { 84 | const currentPublisher = await stakingContract._immediatelyWithdrawableLimitPublisher(); 85 | const receipt = await stakingContract.setImmediatelyWithdrawableLimitPublisher(accounts[4]); 86 | const newPublisher = await stakingContract._immediatelyWithdrawableLimitPublisher(); 87 | 88 | truffleAssert.eventEmitted(receipt, 'ImmediatelyWithdrawableLimitPublisherUpdate', (ev) => { 89 | return ev.oldValue === currentPublisher && 90 | ev.newValue === newPublisher; 91 | }); 92 | 93 | assert.notEqual(currentPublisher, newPublisher, 'Publisher was not updated!'); 94 | assert.equal(newPublisher, accounts[4], "Publisher does not match account it was updated to!"); 95 | }); 96 | 97 | it("should fail update if not owner", async () => { 98 | try { 99 | await stakingContract.setImmediatelyWithdrawableLimitPublisher(accounts[4], {from: accounts[1]}); 100 | assert(false, "This should have thrown"); 101 | } catch(e) { 102 | assert( 103 | e.message.includes("Only the owner can set the immediately withdrawable limit publisher address"), 104 | `Error thrown does not match exepcted error ${e.message}` 105 | ); 106 | } 107 | }); 108 | }); 109 | 110 | describe("setFallbackWithdrawalDelay", () => { 111 | it("should update delay", async () => { 112 | await Utils.setFallbackWithdrawalDelay(stakingContract, 10); 113 | }); 114 | 115 | it("should fail update if not owner", async () => { 116 | try { 117 | await stakingContract.setFallbackWithdrawalDelay(10, {from: accounts[1]}); 118 | assert(false, "This should have thrown"); 119 | } catch(e) { 120 | assert( 121 | e.message.includes("Only the owner can set the fallback withdrawal delay"), 122 | `Error thrown does not match exepcted error ${e.message}` 123 | ); 124 | } 125 | }); 126 | 127 | it("should not allow it to be set to 0", async () => { 128 | try { 129 | await Utils.setFallbackWithdrawalDelay(stakingContract, 0); 130 | assert(false, "This should have thrown"); 131 | } catch(e) { 132 | assert( 133 | e.message.includes("New fallback delay may not be 0"), 134 | `Error thrown does not match exepcted error ${e.message}` 135 | ); 136 | } 137 | }); 138 | }); 139 | }); -------------------------------------------------------------------------------- /test/staking-withdraw.js: -------------------------------------------------------------------------------- 1 | const BN = require('bn.js'); 2 | const Utils = require("./utils"); 3 | const truffleAssert = require('truffle-assertions'); 4 | 5 | const Staking = artifacts.require("Staking"); 6 | const LocalFXCToken = artifacts.require("LocalFXCToken"); 7 | 8 | contract("Staking - withdraw", accounts => { 9 | const owner = accounts[0]; 10 | const fallbackPublisherAddress = accounts[1]; 11 | const withdrawalPublisherAddress = accounts[2]; 12 | const immediatelyWithdrawableLimitPublisher = accounts[3]; 13 | 14 | const account = accounts[4]; 15 | const accountNonce = new BN(0); 16 | const amount = new BN(100); 17 | const rootNonce = new BN(1); 18 | 19 | let stakingContract; 20 | let fxcToken; 21 | 22 | let merkleRoot; 23 | let merkleProof; 24 | 25 | beforeEach(async () => { 26 | fxcToken = await LocalFXCToken.new(); 27 | stakingContract = await Staking.new( 28 | fxcToken.address, 29 | fallbackPublisherAddress, 30 | withdrawalPublisherAddress, 31 | immediatelyWithdrawableLimitPublisher 32 | ); 33 | 34 | await Utils.approveAndDeposit(owner, fxcToken, stakingContract, 10000); 35 | ({merkleRoot, merkleProof} = await Utils.addWithdrawalRootGivingAddressWithdrawableBalance( 36 | withdrawalPublisherAddress, 37 | account, 38 | amount, 39 | rootNonce, 40 | accountNonce, 41 | stakingContract 42 | )); 43 | 44 | // Set the limit to one withdrawal 45 | await Utils.setImmediatelyWithdrawableLimit( 46 | immediatelyWithdrawableLimitPublisher, 47 | amount, 48 | stakingContract 49 | ); 50 | }); 51 | 52 | describe("execution", () => { 53 | it("should withdraw", async () => { 54 | await Utils.withdraw( 55 | account, 56 | account, 57 | amount, 58 | accountNonce, 59 | rootNonce, 60 | merkleProof, 61 | stakingContract, 62 | fxcToken 63 | ); 64 | }); 65 | }); 66 | 67 | describe("permissions", () => { 68 | it("should allow owner to force withdrawal", async () => { 69 | await Utils.withdraw( 70 | owner, 71 | account, 72 | amount, 73 | accountNonce, 74 | rootNonce, 75 | merkleProof, 76 | stakingContract, 77 | fxcToken 78 | ); 79 | }); 80 | 81 | it("should disallow non-contract-owner, non-account-owner withdrawal", async () => { 82 | try { 83 | await Utils.withdraw( 84 | fallbackPublisherAddress, 85 | account, 86 | amount, 87 | accountNonce, 88 | rootNonce, 89 | merkleProof, 90 | stakingContract, 91 | fxcToken 92 | ); 93 | assert(false, "This should have thrown"); 94 | } catch(e) { 95 | assert( 96 | e.message.includes("Only the owner or recipient can execute a withdrawal"), 97 | `Error thrown does not match exepcted error ${e.message}` 98 | ); 99 | } 100 | }); 101 | }); 102 | 103 | describe("constraints", () => { 104 | it("makes sure provided account nonce is <= account nonce in contract", async () => { 105 | // Will update the account nonce such that the next withdrawal is not permitted 106 | await Utils.withdraw( 107 | account, 108 | account, 109 | amount, 110 | accountNonce, 111 | rootNonce, 112 | merkleProof, 113 | stakingContract, 114 | fxcToken 115 | ); 116 | try { 117 | await Utils.withdraw( 118 | account, 119 | account, 120 | amount, 121 | accountNonce, 122 | rootNonce, 123 | merkleProof, 124 | stakingContract, 125 | fxcToken 126 | ); 127 | assert(false, "This should have thrown"); 128 | } catch(e) { 129 | assert( 130 | e.message.includes("Account nonce in contract exceeds provided max authorized withdrawal nonce for this account"), 131 | `Error thrown does not match exepcted error ${e.message}` 132 | ); 133 | } 134 | }); 135 | 136 | it("does not allow cumulative withdrawals in excess of immediately withdrawable limit", async () => { 137 | // Will decrease immediately withdrawable limit to 0 138 | await Utils.withdraw( 139 | account, 140 | account, 141 | amount, 142 | accountNonce, 143 | rootNonce, 144 | merkleProof, 145 | stakingContract, 146 | fxcToken 147 | ); 148 | try { 149 | await Utils.withdraw( 150 | account, 151 | account, 152 | amount, 153 | accountNonce.add(new BN(1)), 154 | rootNonce, 155 | merkleProof, 156 | stakingContract, 157 | fxcToken 158 | ); 159 | assert(false, "This should have thrown"); 160 | } catch(e) { 161 | assert( 162 | e.message.includes("Withdrawal would push contract over its immediately withdrawable limit"), 163 | `Error thrown does not match exepcted error ${e.message}` 164 | ); 165 | } 166 | }); 167 | 168 | it("fails if provided account nonce is not in any merkle root's leaf data", async () => { 169 | try { 170 | await Utils.withdraw( 171 | account, 172 | account, 173 | amount, 174 | accountNonce.add(new BN(1)), 175 | rootNonce, 176 | merkleProof, 177 | stakingContract, 178 | fxcToken 179 | ); 180 | assert(false, "This should have thrown"); 181 | } catch(e) { 182 | assert( 183 | e.message.includes("Root hash unauthorized"), 184 | `Error thrown does not match exepcted error ${e.message}` 185 | ); 186 | } 187 | }); 188 | 189 | it("fails if provided amount is not in any merkle root's leaf data", async () => { 190 | await Utils.setImmediatelyWithdrawableLimit( 191 | immediatelyWithdrawableLimitPublisher, 192 | amount.add(new BN(1)), 193 | stakingContract 194 | ); 195 | 196 | try { 197 | await Utils.withdraw( 198 | account, 199 | account, 200 | amount.add(new BN(1)), 201 | accountNonce, 202 | rootNonce, 203 | merkleProof, 204 | stakingContract, 205 | fxcToken 206 | ); 207 | assert(false, "This should have thrown"); 208 | } catch(e) { 209 | assert( 210 | e.message.includes("Root hash unauthorized"), 211 | `Error thrown does not match exepcted error ${e.message}` 212 | ); 213 | } 214 | }); 215 | 216 | it("fails if merkle proof does not match", async () => { 217 | try { 218 | await Utils.withdraw( 219 | account, 220 | account, 221 | amount, 222 | accountNonce, 223 | rootNonce, 224 | [Utils.keccak256("Not the right root")], 225 | stakingContract, 226 | fxcToken 227 | ); 228 | assert(false, "This should have thrown"); 229 | } catch(e) { 230 | assert( 231 | e.message.includes("Root hash unauthorized"), 232 | `Error thrown does not match exepcted error ${e.message}` 233 | ); 234 | } 235 | }); 236 | 237 | it("prevents double-withdrawals", async () => { 238 | const exceedingAccountNonce = rootNonce.add(new BN(2)); 239 | 240 | ({_, merkleProof} = await Utils.addWithdrawalRootGivingAddressWithdrawableBalance( 241 | withdrawalPublisherAddress, 242 | account, 243 | amount, 244 | rootNonce.add(new BN(1)), 245 | exceedingAccountNonce, 246 | stakingContract, 247 | [merkleRoot] 248 | )); 249 | try { 250 | await Utils.withdraw( 251 | account, 252 | account, 253 | amount, 254 | exceedingAccountNonce, 255 | rootNonce, 256 | merkleProof, 257 | stakingContract, 258 | fxcToken 259 | ); 260 | assert(false, "This should have thrown"); 261 | } catch(e) { 262 | assert( 263 | e.message.includes("Encoded nonce not greater than max last authorized nonce for this account"), 264 | `Error thrown does not match exepcted error ${e.message}` 265 | ); 266 | } 267 | }); 268 | 269 | }); 270 | }); -------------------------------------------------------------------------------- /test/staking-withdrawFallback.js: -------------------------------------------------------------------------------- 1 | const BN = require('bn.js'); 2 | const Utils = require("./utils"); 3 | const truffleAssert = require('truffle-assertions'); 4 | 5 | const Staking = artifacts.require("Staking"); 6 | const LocalFXCToken = artifacts.require("LocalFXCToken"); 7 | 8 | contract("Staking - withdraw", async accounts => { 9 | await Utils.moveTimeForwardSecondsAndMineBlock(1); 10 | 11 | const owner = accounts[0]; 12 | const fallbackPublisherAddress = accounts[1]; 13 | const withdrawalPublisherAddress = accounts[2]; 14 | const immediatelyWithdrawableLimitPublisher = accounts[3]; 15 | 16 | const account = accounts[4]; 17 | const maxCumulativeWithdrawalAmount = new BN(100); 18 | const maxDepositNonce = new BN(1); 19 | 20 | let stakingContract; 21 | let fxcToken; 22 | 23 | let merkleProof; 24 | 25 | beforeEach(async () => { 26 | fxcToken = await LocalFXCToken.new(); 27 | stakingContract = await Staking.new( 28 | fxcToken.address, 29 | fallbackPublisherAddress, 30 | withdrawalPublisherAddress, 31 | immediatelyWithdrawableLimitPublisher 32 | ); 33 | 34 | await Utils.approveAndDeposit(owner, fxcToken, stakingContract, 10000); 35 | ({merkleRoot, merkleProof} = await Utils.setFallbackRootGivingAddressWithdrawableBalance( 36 | fallbackPublisherAddress, 37 | account, 38 | maxCumulativeWithdrawalAmount, 39 | maxDepositNonce, 40 | stakingContract 41 | )); 42 | 43 | await Utils.setFallbackWithdrawalDelay(stakingContract, 1); 44 | await Utils.moveTimeForwardSecondsAndMineBlock(5); 45 | }); 46 | 47 | describe("execution", () => { 48 | it("should fallback withdraw", async () => { 49 | await Utils.withdrawFallback( 50 | account, 51 | account, 52 | maxCumulativeWithdrawalAmount, 53 | merkleProof, 54 | stakingContract, 55 | fxcToken 56 | ); 57 | }); 58 | }); 59 | 60 | describe("permissions", () => { 61 | it("should allow owner to force withdrawal", async () => { 62 | await Utils.withdrawFallback( 63 | owner, 64 | account, 65 | maxCumulativeWithdrawalAmount, 66 | merkleProof, 67 | stakingContract, 68 | fxcToken 69 | ); 70 | }); 71 | 72 | it("should disallow non-contract-owner, non-account-owner withdrawal", async () => { 73 | try { 74 | await Utils.withdrawFallback( 75 | withdrawalPublisherAddress, 76 | account, 77 | maxCumulativeWithdrawalAmount, 78 | merkleProof, 79 | stakingContract, 80 | fxcToken 81 | ); 82 | assert(false, "This should have thrown"); 83 | } catch(e) { 84 | assert( 85 | e.message.includes("Only the owner or recipient can execute a fallback withdrawal"), 86 | `Error thrown does not match exepcted error ${e.message}` 87 | ); 88 | } 89 | }); 90 | }); 91 | 92 | describe("constraints", () => { 93 | it("fails if the fallback withdrawal delay hasn't lapsed", async () => { 94 | await Utils.setFallbackWithdrawalDelay(stakingContract, 15); 95 | 96 | await Utils.moveTimeForwardSecondsAndMineBlock(1); 97 | 98 | try { 99 | await Utils.withdrawFallback( 100 | account, 101 | account, 102 | maxCumulativeWithdrawalAmount, 103 | merkleProof, 104 | stakingContract, 105 | fxcToken 106 | ); 107 | assert(false, "This should have thrown"); 108 | } catch(e) { 109 | assert( 110 | e.message.includes("Fallback withdrawal period is not active"), 111 | `Error thrown does not match exepcted error ${e.message}` 112 | ); 113 | } 114 | }); 115 | 116 | it("fails for double-withdrawals", async () => { 117 | // Will set the account's fallback nonce to the root nonce 118 | await Utils.withdrawFallback( 119 | account, 120 | account, 121 | maxCumulativeWithdrawalAmount, 122 | merkleProof, 123 | stakingContract, 124 | fxcToken 125 | ); 126 | 127 | try { 128 | await Utils.withdrawFallback( 129 | account, 130 | account, 131 | maxCumulativeWithdrawalAmount, 132 | merkleProof, 133 | stakingContract, 134 | fxcToken 135 | ); 136 | assert(false, "This should have thrown"); 137 | } catch(e) { 138 | assert( 139 | e.message.includes("Withdrawal not permitted when amount withdrawn is at lifetime withdrawal limit"), 140 | `Error thrown does not match exepcted error ${e.message}` 141 | ); 142 | } 143 | }); 144 | 145 | it("fails for invalid merkle proof", async () => { 146 | const badMerkleProof = [Utils.keccak256("invalid")]; 147 | try { 148 | await Utils.withdrawFallback( 149 | account, 150 | account, 151 | maxCumulativeWithdrawalAmount, 152 | badMerkleProof, 153 | stakingContract, 154 | fxcToken 155 | ); 156 | assert(false, "This should have thrown"); 157 | } catch(e) { 158 | assert( 159 | e.message.includes("Root hash unauthorized"), 160 | `Error thrown does not match exepcted error ${e.message}` 161 | ); 162 | } 163 | }); 164 | 165 | it("fails if max cumulative amount is wrong", async () => { 166 | const badMerkleProof = [Utils.keccak256("invalid")]; 167 | try { 168 | await Utils.withdrawFallback( 169 | account, 170 | account, 171 | maxCumulativeWithdrawalAmount.add(new BN(1)), 172 | badMerkleProof, 173 | stakingContract, 174 | fxcToken, 175 | ); 176 | assert(false, "This should have thrown"); 177 | } catch(e) { 178 | assert( 179 | e.message.includes("Root hash unauthorized"), 180 | `Error thrown does not match exepcted error ${e.message}` 181 | ); 182 | } 183 | }); 184 | 185 | it("fails for old merkle proof if root is updated", async () => { 186 | await Utils.setFallbackWithdrawalDelay(stakingContract, 10000); 187 | await Utils.moveTimeForwardSecondsAndMineBlock(1); 188 | 189 | // Sets the fallback nonce 2 ahead of authorized account fallback nonce 190 | ({merkleRoot, merkleProof} = await Utils.setFallbackRootGivingAddressWithdrawableBalance( 191 | fallbackPublisherAddress, 192 | withdrawalPublisherAddress, 193 | maxCumulativeWithdrawalAmount, 194 | maxDepositNonce, 195 | stakingContract 196 | )); 197 | 198 | await Utils.setFallbackWithdrawalDelay(stakingContract, 1); 199 | await Utils.moveTimeForwardSecondsAndMineBlock(5); 200 | 201 | try { 202 | await Utils.withdrawFallback( 203 | account, 204 | account, 205 | maxCumulativeWithdrawalAmount, 206 | merkleProof, 207 | stakingContract, 208 | fxcToken 209 | ); 210 | assert(false, "This should have thrown"); 211 | } catch(e) { 212 | assert( 213 | e.message.includes("Root hash unauthorized"), 214 | `Error thrown does not match exepcted error ${e.message}` 215 | ); 216 | } 217 | }); 218 | 219 | it("fails to withdraw from regular authorized withdrawal with honest authorized account nonce after fallback withdrawal", async () => { 220 | const rootNonce = new BN(1); 221 | const accountNonce = new BN(0); 222 | 223 | ({merkleProof: regularWithdrawalMerkleProof} = await Utils.addWithdrawalRootGivingAddressWithdrawableBalance( 224 | withdrawalPublisherAddress, 225 | account, 226 | maxCumulativeWithdrawalAmount, 227 | rootNonce, 228 | accountNonce, 229 | stakingContract 230 | )); 231 | 232 | await Utils.setImmediatelyWithdrawableLimit( 233 | immediatelyWithdrawableLimitPublisher, 234 | maxCumulativeWithdrawalAmount, 235 | stakingContract 236 | ); 237 | 238 | await Utils.withdrawFallback( 239 | account, 240 | account, 241 | maxCumulativeWithdrawalAmount, 242 | merkleProof, 243 | stakingContract, 244 | fxcToken 245 | ); 246 | 247 | try { 248 | await Utils.withdraw( 249 | account, 250 | account, 251 | maxCumulativeWithdrawalAmount, 252 | accountNonce, 253 | rootNonce, 254 | regularWithdrawalMerkleProof, 255 | stakingContract, 256 | fxcToken 257 | ); 258 | assert(false, "This should have thrown"); 259 | } catch(e) { 260 | assert( 261 | e.message.includes("Account nonce in contract exceeds provided max authorized withdrawal nonce for this account"), 262 | `Error thrown does not match exepcted error ${e.message}` 263 | ); 264 | } 265 | }) 266 | 267 | it("fails to withdraw from regular authorized withdrawal with dishonest authorized account nonce after fallback withdrawal", async () => { 268 | const rootNonce = new BN(1); 269 | const accountNonce = new BN(0); 270 | 271 | ({merkleProof: regularWithdrawalMerkleProof} = await Utils.addWithdrawalRootGivingAddressWithdrawableBalance( 272 | withdrawalPublisherAddress, 273 | account, 274 | maxCumulativeWithdrawalAmount, 275 | rootNonce, 276 | accountNonce, 277 | stakingContract 278 | )); 279 | 280 | await Utils.setImmediatelyWithdrawableLimit( 281 | immediatelyWithdrawableLimitPublisher, 282 | maxCumulativeWithdrawalAmount, 283 | stakingContract 284 | ); 285 | 286 | await Utils.withdrawFallback( 287 | account, 288 | account, 289 | maxCumulativeWithdrawalAmount, 290 | merkleProof, 291 | stakingContract, 292 | fxcToken 293 | ); 294 | 295 | try { 296 | await Utils.withdraw( 297 | account, 298 | account, 299 | maxCumulativeWithdrawalAmount, 300 | accountNonce.add(new BN(1)), 301 | rootNonce, 302 | regularWithdrawalMerkleProof, 303 | stakingContract, 304 | fxcToken 305 | ); 306 | assert(false, "This should have thrown"); 307 | } catch(e) { 308 | assert( 309 | e.message.includes("Root hash unauthorized"), 310 | `Error thrown does not match exepcted error ${e.message}` 311 | ); 312 | } 313 | }) 314 | }); 315 | }); -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | const BN = require("bn.js"); 2 | const truffleAssert = require('truffle-assertions'); 3 | 4 | const NULL_ADDRESS = "0x0000000000000000000000000000000000000000"; 5 | const EMPTY_BYTES32 = "0x0000000000000000000000000000000000000000000000000000000000000000" 6 | const EMPTY_LEAF_HASH = web3.utils.keccak256(web3.eth.abi.encodeParameter('uint256', 0)) 7 | let nonce = 1; 8 | 9 | module.exports = class Utils { 10 | 11 | static async deposit(address, tokenContract, stakingContract, amount) { 12 | const balance = await tokenContract.balanceOf(stakingContract.address); 13 | 14 | const nonceBefore = await stakingContract._depositNonce(); 15 | const receipt = await stakingContract.deposit(amount, {from: address}); 16 | const nonce = await stakingContract._depositNonce(); 17 | 18 | truffleAssert.eventEmitted(receipt, 'Deposit', (ev) => { 19 | return ev.depositor === address && 20 | ev.amount.eq(new BN(amount)) && 21 | ev.nonce.eq(nonceBefore.add(new BN(1))); 22 | }); 23 | 24 | assert( 25 | nonce.eq(nonceBefore.add(new BN(1))), 26 | "Deposit nonce not incremented!" 27 | ); 28 | 29 | const updatedBalance = await tokenContract.balanceOf(stakingContract.address); 30 | assert( 31 | updatedBalance.eq(balance.add(new BN(amount))), 32 | "Approve and deposit did not work. Balance unchanged." 33 | ); 34 | 35 | return nonce; 36 | } 37 | 38 | static async approveAndDeposit(address, tokenContract, stakingContract, amount) { 39 | await tokenContract.approve(stakingContract.address, amount, {from: address}); 40 | return await Utils.deposit(address, tokenContract, stakingContract, amount); 41 | } 42 | 43 | static async executePendingDepositRefund(address, account, tokenContract, stakingContract, depositAmount, depositNonce) { 44 | const depositBalance = await tokenContract.balanceOf(account); 45 | 46 | const receipt = await stakingContract.refundPendingDeposit(depositNonce, {from: address}); 47 | 48 | truffleAssert.eventEmitted(receipt, 'PendingDepositRefund', (ev) => { 49 | return ev.depositorAddress === account && 50 | ev.amount.eq(depositAmount) && 51 | ev.nonce.eq(depositNonce); 52 | }); 53 | 54 | const refundBalance = await tokenContract.balanceOf(account); 55 | 56 | assert( 57 | depositBalance.add(depositAmount).eq(refundBalance), 58 | "Refund balance not added to account FXC balance!" 59 | ); 60 | 61 | const pendingDeposit = stakingContract._nonceToPendingDeposit(depositNonce); 62 | assert( 63 | pendingDeposit.amount === undefined && 64 | pendingDeposit.depositor === undefined, 65 | "Pending Deposit should be deleted!" 66 | ) 67 | } 68 | 69 | static async addWithdrawalRoot(address, stakingContract, root, nonce, replacedRoots = []) { 70 | const oldMaxNonce = await stakingContract._maxWithdrawalRootNonce(); 71 | 72 | const replacedRootNonces = [] 73 | for (const replacedRoot of replacedRoots) { 74 | replacedRootNonces.push(await stakingContract._withdrawalRootToNonce(replacedRoot)) 75 | } 76 | 77 | const receipt = await stakingContract.addWithdrawalRoot( 78 | root, 79 | nonce, 80 | replacedRoots, 81 | {from: address} 82 | ); 83 | 84 | truffleAssert.eventEmitted(receipt, 'WithdrawalRootHashAddition', (ev) => { 85 | return ev.rootHash === root && 86 | ev.nonce.eq(nonce); 87 | }); 88 | 89 | for (let i = 0; i < replacedRoots.length; i++) { 90 | await Utils.assertWithdrawalRootDeleted( 91 | replacedRoots[i], 92 | replacedRootNonces[i], 93 | stakingContract, 94 | receipt 95 | ); 96 | } 97 | 98 | const newMaxNonce = await stakingContract._maxWithdrawalRootNonce(); 99 | assert(newMaxNonce.eq(nonce), "Max nonce not updated to nonce passed to function!"); 100 | assert(newMaxNonce.eq(oldMaxNonce.add(new BN(1))), "Max nonce not incremented by 1!"); 101 | 102 | const rootNonce = await stakingContract._withdrawalRootToNonce(root); 103 | assert(rootNonce.eq(newMaxNonce), "Root to nonce map not updated!"); 104 | } 105 | 106 | static async withdraw(address, account, amount, accountNonce, rootNonce, merkleProof, stakingContract, tokenContract) { 107 | const withdrawableLimitBefore = await stakingContract._immediatelyWithdrawableLimit(); 108 | const cumulativeWithdrawalTotalBefore = await stakingContract._addressToCumulativeAmountWithdrawn(account); 109 | const balanceBefore = await tokenContract.balanceOf(account); 110 | 111 | const receipt = await stakingContract.withdraw( 112 | account, 113 | amount, 114 | accountNonce, 115 | merkleProof, 116 | {from: address} 117 | ); 118 | 119 | truffleAssert.eventEmitted(receipt, 'Withdrawal', (ev) => { 120 | return ev.toAddress === account && 121 | ev.amount.eq(amount) && 122 | ev.rootNonce.eq(rootNonce) && 123 | ev.authorizedAccountNonce.eq(accountNonce); 124 | }); 125 | 126 | const balanceAfter = await tokenContract.balanceOf(account); 127 | assert( 128 | balanceAfter.sub(balanceBefore).eq(amount), 129 | "Account FXC Balance didn't increase by the correct amount!" 130 | ); 131 | 132 | const accountNonceAfter = await stakingContract._addressToWithdrawalNonce(account); 133 | assert( 134 | accountNonceAfter.eq(rootNonce), 135 | "Account nonce did not update properly!" 136 | ); 137 | 138 | const withdrawableLimitAfter = await stakingContract._immediatelyWithdrawableLimit(); 139 | assert( 140 | withdrawableLimitBefore.sub(withdrawableLimitAfter).eq(amount), 141 | "Immediately withdrawable limit didn't decrease by the proper amount!" 142 | ); 143 | 144 | const cumulativeWithdrawalTotalAfter = await stakingContract._addressToCumulativeAmountWithdrawn(account); 145 | assert( 146 | cumulativeWithdrawalTotalAfter.sub(amount).eq(cumulativeWithdrawalTotalBefore), 147 | "Address cumulative amount withdrawn should have increased by the withdrawal amount!" 148 | ); 149 | } 150 | 151 | static async withdrawFallback(address, account, maxCumulativeAmountWithdrawn, merkleProof, stakingContract, tokenContract) { 152 | 153 | const balanceBefore = await tokenContract.balanceOf(account); 154 | const cumulativeWithdrawalTotalBefore = await stakingContract._addressToCumulativeAmountWithdrawn(account); 155 | 156 | const receipt = await stakingContract.withdrawFallback( 157 | account, 158 | maxCumulativeAmountWithdrawn, 159 | merkleProof, 160 | {from: address} 161 | ); 162 | 163 | const withdrawalAmount = maxCumulativeAmountWithdrawn.sub(cumulativeWithdrawalTotalBefore) 164 | 165 | truffleAssert.eventEmitted(receipt, 'FallbackWithdrawal', (ev) => { 166 | return ev.toAddress === account && 167 | ev.amount.eq(withdrawalAmount) 168 | }); 169 | 170 | const balanceAfter = await tokenContract.balanceOf(account); 171 | assert( 172 | balanceAfter.sub(balanceBefore).eq(withdrawalAmount), 173 | "Account FXC Balance didn't increase by the correct amount!" 174 | ); 175 | 176 | const cumulativeWithdrawalTotalAfter = await stakingContract._addressToCumulativeAmountWithdrawn(account); 177 | assert( 178 | cumulativeWithdrawalTotalAfter.gt(cumulativeWithdrawalTotalBefore), 179 | "Cumulative withdrawal total should be more after!" 180 | ); 181 | 182 | const withdrawalNonceAfter = await stakingContract._addressToWithdrawalNonce(account); 183 | const maxWithdrawalRootNonce = await stakingContract._maxWithdrawalRootNonce(); 184 | assert( 185 | withdrawalNonceAfter.eq(maxWithdrawalRootNonce), 186 | "Fallback withdrawal should invalidate any authorized normal withdrawals!" 187 | ); 188 | } 189 | 190 | static async setFallbackRoot(address, stakingContract, rootToSet, maxDepositIncluded) { 191 | const oldFallbackSetDate = await stakingContract._fallbackSetDate(); 192 | 193 | const receipt = await stakingContract.setFallbackRoot( 194 | rootToSet, 195 | maxDepositIncluded, 196 | {from: address} 197 | ); 198 | 199 | truffleAssert.eventEmitted(receipt, 'FallbackRootHashSet', (ev) => { 200 | return ev.rootHash === rootToSet && 201 | ev.maxDepositNonceIncluded.eq(maxDepositIncluded); 202 | }); 203 | 204 | const newFallbackSetDate = await stakingContract._fallbackSetDate(); 205 | assert(!newFallbackSetDate.eq(oldFallbackSetDate), "New fallback set date equals old one!"); 206 | 207 | const newMaxDepositIncluded = await stakingContract._fallbackMaxDepositIncluded(); 208 | assert(newMaxDepositIncluded.eq(maxDepositIncluded), "new Max deposit does not match max deposit included!"); 209 | 210 | const newRoot = await stakingContract._fallbackRoot(); 211 | assert(newRoot === rootToSet, "Root does not match root to set!"); 212 | } 213 | 214 | static async setImmediatelyWithdrawableLimit(address, amount, stakingContract) { 215 | const oldLimit = await stakingContract._immediatelyWithdrawableLimit(); 216 | await Utils.modifyImmediatelyWithdrawableLimit( 217 | address, 218 | oldLimit.gt(amount) ? oldLimit.sub(amount).neg() : amount.sub(oldLimit), 219 | stakingContract 220 | ); 221 | } 222 | 223 | static async modifyImmediatelyWithdrawableLimit(address, amount, stakingContract) { 224 | const oldLimit = await stakingContract._immediatelyWithdrawableLimit(); 225 | const receipt = await stakingContract.modifyImmediatelyWithdrawableLimit(amount, {from: address}); 226 | const newLimit = await stakingContract._immediatelyWithdrawableLimit(); 227 | 228 | const expectedNewLimit = oldLimit.add(amount) 229 | 230 | truffleAssert.eventEmitted(receipt, 'ImmediatelyWithdrawableLimitUpdate', (ev) => { 231 | return ev.oldValue.eq(oldLimit) && 232 | ev.newValue.eq(expectedNewLimit); 233 | }); 234 | 235 | assert(!oldLimit.eq(newLimit), 'Limit was not updated!'); 236 | assert(newLimit.eq(expectedNewLimit), "Limit does not match value it was updated to!"); 237 | } 238 | 239 | static async removeWithdrawalRoots(address, rootsToRemove, stakingContract) { 240 | const removedNonces = [] 241 | for (const rootToRemove of rootsToRemove) { 242 | removedNonces.push(await stakingContract._withdrawalRootToNonce(rootToRemove)) 243 | } 244 | 245 | const receipt = await stakingContract.removeWithdrawalRoots(rootsToRemove, {from: address}); 246 | 247 | for (let i = 0; i < rootsToRemove.length; i++) { 248 | await Utils.assertWithdrawalRootDeleted(rootsToRemove[i], removedNonces[i], stakingContract, receipt); 249 | } 250 | } 251 | 252 | static async assertWithdrawalRootDeleted(root, nonce, stakingContract, txReceipt) { 253 | truffleAssert.eventEmitted(txReceipt, 'WithdrawalRootHashRemoval', (ev) => { 254 | return ev.rootHash === root && 255 | ev.nonce.eq(nonce); 256 | }); 257 | 258 | const rootNonce = await stakingContract._withdrawalRootToNonce(root); 259 | assert(rootNonce.eq(new BN(0)), `Root nonce still exists: ${rootNonce}, root: ${root}`); 260 | } 261 | 262 | static async setFallbackWithdrawalDelay(stakingContract, delay) { 263 | const oldValue = await stakingContract._fallbackWithdrawalDelaySeconds(); 264 | const receipt = await stakingContract.setFallbackWithdrawalDelay(delay); 265 | const newValue = await stakingContract._fallbackWithdrawalDelaySeconds(); 266 | 267 | truffleAssert.eventEmitted(receipt, 'FallbackWithdrawalDelayUpdate', (ev) => { 268 | return ev.oldValue.eq(oldValue) && 269 | ev.newValue.eq(new BN(delay)); 270 | }); 271 | 272 | assert.equal(newValue, delay); 273 | } 274 | 275 | static async resetFallbackMechanismDate(address, stakingContract) { 276 | const fallbackSetDate = await stakingContract._fallbackSetDate(); 277 | 278 | const receipt = await stakingContract.resetFallbackMechanismDate({from: address}); 279 | 280 | truffleAssert.eventEmitted(receipt, 'FallbackMechanismDateReset', (ev) => { 281 | return !ev.newDate.eq(fallbackSetDate); 282 | }); 283 | 284 | const updatedFallbackSetDate = await stakingContract._fallbackSetDate(); 285 | assert( 286 | !fallbackSetDate.eq(updatedFallbackSetDate), 287 | "Fallback set date should have been updated!" 288 | ) 289 | } 290 | 291 | static async addWithdrawalRootGivingAddressWithdrawableBalance( 292 | publishAccount, 293 | withdrawAccount, 294 | withdrawableAmount, 295 | rootNonce, 296 | addressNonce, 297 | stakingContract, 298 | rootsToRemove = [] 299 | ) { 300 | 301 | const datahash = web3.utils.soliditySha3( 302 | withdrawAccount, 303 | withdrawableAmount, 304 | addressNonce 305 | ); 306 | let root; 307 | if (datahash < EMPTY_LEAF_HASH) { 308 | root = web3.utils.soliditySha3(datahash, EMPTY_LEAF_HASH); 309 | } else { 310 | root = web3.utils.soliditySha3(EMPTY_LEAF_HASH, datahash); 311 | } 312 | 313 | await Utils.addWithdrawalRoot(publishAccount, stakingContract, root, rootNonce, rootsToRemove); 314 | 315 | return { 316 | merkleRoot: root, 317 | merkleProof: [EMPTY_LEAF_HASH] 318 | } 319 | } 320 | 321 | static async setFallbackRootGivingAddressWithdrawableBalance( 322 | publishAccount, 323 | withdrawAccount, 324 | maxCumulativeAmountWithdrawn, 325 | maxDepositIncluded, 326 | stakingContract 327 | ) { 328 | 329 | const datahash = web3.utils.soliditySha3( 330 | withdrawAccount, 331 | maxCumulativeAmountWithdrawn, 332 | ); 333 | let root; 334 | if (datahash < EMPTY_LEAF_HASH) { 335 | root = web3.utils.soliditySha3(datahash, EMPTY_LEAF_HASH); 336 | } else { 337 | root = web3.utils.soliditySha3(EMPTY_LEAF_HASH, datahash); 338 | } 339 | 340 | await Utils.setFallbackRoot(publishAccount, stakingContract, root, maxDepositIncluded); 341 | 342 | return { 343 | merkleRoot: root, 344 | merkleProof: [EMPTY_LEAF_HASH] 345 | } 346 | } 347 | 348 | static getNullAddress() { 349 | return NULL_ADDRESS; 350 | } 351 | 352 | static getEmptyBytes32() { 353 | return EMPTY_BYTES32; 354 | } 355 | 356 | static hexStringToBN(str) { 357 | return new BN(str.slice(2), "hex"); 358 | } 359 | 360 | static keccak256(str) { 361 | return web3.utils.keccak256(str); 362 | } 363 | 364 | static async moveTimeForwardSecondsAndMineBlock(seconds) { 365 | await Utils.web3Send({ 366 | jsonrpc: "2.0", 367 | method: "evm_increaseTime", 368 | params: [seconds], 369 | id: 0 370 | }); 371 | 372 | await Utils.web3Send({ 373 | jsonrpc: "2.0", 374 | method: "evm_mine", 375 | id: nonce++ 376 | }); 377 | } 378 | 379 | static web3Send(params) { 380 | return new Promise((resolve, reject) => { 381 | web3.currentProvider.send( 382 | params, 383 | (err, result) => { 384 | if (err) { return reject(err); } 385 | return resolve(result); 386 | } 387 | ); 388 | }); 389 | } 390 | } 391 | -------------------------------------------------------------------------------- /truffle-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this file to configure your truffle project. It's seeded with some 3 | * common settings for different networks and features like migrations, 4 | * compilation and testing. Uncomment the ones you need or modify 5 | * them to suit your project as necessary. 6 | * 7 | * More information about configuration can be found at: 8 | * 9 | * truffleframework.com/docs/advanced/configuration 10 | * 11 | * To deploy via Infura you'll need a wallet provider (like truffle-hdwallet-provider) 12 | * to sign your transactions before they're sent to a remote public node. Infura accounts 13 | * are available for free at: infura.io/register. 14 | * 15 | * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate 16 | * public/private key pairs. If you're publishing your code to GitHub make sure you load this 17 | * phrase from a file you've .gitignored so it doesn't accidentally become public. 18 | * 19 | */ 20 | 21 | require('dotenv').config(); 22 | const HDWalletProvider = require("truffle-hdwallet-provider"); 23 | const mnemonic = process.env["MNEMONIC"]; 24 | const rinkebyKey = process.env["RINKEBY_ENDPOINT_KEY"]; 25 | 26 | module.exports = { 27 | /** 28 | * Networks define how you connect to your ethereum client and let you set the 29 | * defaults web3 uses to send transactions. If you don't specify one truffle 30 | * will spin up a development blockchain for you on port 9545 when you 31 | * run `develop` or `test`. You can ask a truffle command to use a specific 32 | * network from the command line, e.g 33 | * 34 | * $ truffle test --network 35 | */ 36 | 37 | networks: { 38 | // Useful for testing. The `development` name is special - truffle uses it by default 39 | // if it's defined here and no other network is specified at the command line. 40 | // You should run a client (like ganache-cli, geth or parity) in a separate terminal 41 | // tab if you use this network and you must also set the `host`, `port` and `network_id` 42 | // options below to some value. 43 | // 44 | // development: { 45 | // host: "127.0.0.1", // Localhost (default: none) 46 | // port: 8545, // Standard Ethereum port (default: none) 47 | // network_id: "*", // Any network (default: none) 48 | // }, 49 | 50 | // Another network with more advanced options... 51 | // advanced: { 52 | // port: 8777, // Custom port 53 | // network_id: 1342, // Custom network 54 | // gas: 8500000, // Gas sent with each transaction (default: ~6700000) 55 | // gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei) 56 | // from:
, // Account to send txs from (default: accounts[0]) 57 | // websockets: true // Enable EventEmitter interface for web3 (default: false) 58 | // }, 59 | 60 | rinkeby: { 61 | provider: () => new HDWalletProvider(mnemonic, `https://rinkeby.infura.io/v3/${rinkebyKey}`), 62 | network_id: 4, // Rinkeby's id 63 | gasPrice : 1000000000, 64 | gas: 4000000, // Rinkeby has a lower block limit than mainnet 65 | confirmations: 1, // # of confs to wait between deployments. (default: 0) 66 | timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50) 67 | skipDryRun: false // Skip dry run before migrations? (default: false for public nets ) 68 | }, 69 | 70 | // Useful for deploying to a public network. 71 | // NB: It's important to wrap the provider as a function. 72 | // ropsten: { 73 | // provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/YOUR-PROJECT-ID`), 74 | // network_id: 3, // Ropsten's id 75 | // gas: 5500000, // Ropsten has a lower block limit than mainnet 76 | // confirmations: 2, // # of confs to wait between deployments. (default: 0) 77 | // timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50) 78 | // skipDryRun: true // Skip dry run before migrations? (default: false for public nets ) 79 | // }, 80 | 81 | // Useful for private networks 82 | // private: { 83 | // provider: () => new HDWalletProvider(mnemonic, `https://network.io`), 84 | // network_id: 2111, // This network is yours, in the cloud. 85 | // production: true // Treats this network as if it was a public net. (default: false) 86 | // } 87 | }, 88 | 89 | // Set default mocha options here, use special reporters etc. 90 | mocha: { 91 | timeout: 60000 92 | }, 93 | 94 | // Configure your compilers 95 | compilers: { 96 | solc: { 97 | version: "^0.5.3", // Fetch exact version from solc-bin (default: truffle's version) 98 | // docker: true, // Use "0.5.1" you've installed locally with docker (default: false) 99 | // settings: { // See the solidity docs for advice about optimization and evmVersion 100 | // optimizer: { 101 | // enabled: true, 102 | // runs: 200 103 | // }, 104 | // evmVersion: "byzantium" 105 | // } 106 | } 107 | } 108 | } 109 | --------------------------------------------------------------------------------