├── .eslintrc.json ├── .gitignore ├── .solhint.json ├── .soliumrc.json ├── README.md ├── config └── default.json ├── contracts ├── CrodoPublicSale ├── Migrations.sol ├── TestToken.sol ├── crodoContract.sol ├── crodoToken.sol ├── distributionContract.sol ├── limitedSale.sol ├── rewardToken.sol └── sampleSwap.sol ├── migrations ├── 1_initial_migration.js ├── 2_crodo_token.js ├── 3_crodo_stake.js ├── 4_crodo_sales.js ├── 5_crodo_rewards.js └── utils.js ├── package-lock.json ├── package.json ├── test ├── crodo_token.js ├── private_sale.js ├── stake.js └── swap.js └── truffle-config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true 6 | }, 7 | "extends": "standard", 8 | "globals": { 9 | "Atomics": "readonly", 10 | "SharedArrayBuffer": "readonly" 11 | }, 12 | "parserOptions": { 13 | "ecmaVersion": 2018 14 | }, 15 | "rules": { 16 | "no-undef": "off", 17 | "indent": ["warn", 4], 18 | "quotes": ["warn", "double"] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build 2 | build/ 3 | 4 | # dependencies 5 | node_modules/ 6 | 7 | #test 8 | .coverage/ 9 | .coverage-contracts/ 10 | coverage/ 11 | coverage.json 12 | 13 | # ide 14 | .idea/ 15 | .vscode/ 16 | 17 | tags 18 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:default" 3 | } 4 | -------------------------------------------------------------------------------- /.soliumrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solium:all" 3 | } 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Contracts 2 | Crodo.io contracts ERC-20 3 | -------------------------------------------------------------------------------- /config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "development": { 3 | "blockchain": { 4 | "rpc": "http://127.0.0.1:8545", 5 | "networkId": "*" 6 | }, 7 | "truffle": { 8 | "mnemonic": "" 9 | } 10 | }, 11 | "testnet": { 12 | "blockchain": { 13 | "rpc": "https://cronos-testnet-3.crypto.org:8545", 14 | "networkId": "338" 15 | }, 16 | "truffle": { 17 | "privateKey": "" 18 | } 19 | }, 20 | "testnet-local": { 21 | "blockchain": { 22 | "rpc": "http://127.0.0.1:8545", 23 | "networkId": "338" 24 | }, 25 | "truffle": { 26 | "privateKey": "" 27 | } 28 | }, 29 | "mainnet": { 30 | "blockchain": { 31 | "rpc": "https://rpc-cronos.crypto.org/", 32 | "networkId": "*" 33 | }, 34 | "truffle": { 35 | "privateKey": "" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /contracts/CrodoPublicSale: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | import "@openzeppelin/contracts/utils/math/SafeMath.sol"; 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | import "@openzeppelin/contracts/utils/Strings.sol"; 7 | 8 | abstract contract BaseLimitedSale is Ownable { 9 | using SafeMath for uint256; 10 | 11 | event ParticipantAdded(address participant); 12 | event ParticipantRemoved(address participant); 13 | event ReleaseFinished(uint8 release); 14 | 15 | ERC20 public crodoToken; 16 | ERC20 public usdtToken; 17 | // address public USDTAddress = address(0x66e428c3f67a68878562e79A0234c1F83c208770); 18 | 19 | struct Participant { 20 | uint256 minBuyAllowed; 21 | uint256 maxBuyAllowed; 22 | uint256 reserved; 23 | uint256 sent; 24 | } 25 | 26 | uint256 public totalMaxBuyAllowed; 27 | uint256 public totalMinBuyAllowed; 28 | uint256 public totalBought; 29 | uint256 public USDTPerToken; 30 | uint256 public saleDecimals; 31 | uint48 public initReleaseDate; 32 | uint256 public latestRelease; // Time of the latest release 33 | uint48 public releaseInterval = 30 days; 34 | uint8 public totalReleases = 10; 35 | uint8 public currentRelease; 36 | 37 | mapping(address => Participant) public participants; 38 | address[] participantAddrs; 39 | 40 | constructor( 41 | address _crodoToken, 42 | address _usdtAddress, 43 | uint256 _USDTPerToken, 44 | uint48 _initReleaseDate, 45 | uint8 _totalReleases 46 | ) Ownable() { 47 | crodoToken = ERC20(_crodoToken); 48 | saleDecimals = 10**crodoToken.decimals(); 49 | usdtToken = ERC20(_usdtAddress); 50 | USDTPerToken = _USDTPerToken; 51 | initReleaseDate = _initReleaseDate; 52 | totalReleases = _totalReleases; 53 | } 54 | 55 | function reservedBy(address participant) public view returns (uint256) { 56 | return participants[participant].reserved * saleDecimals; 57 | } 58 | 59 | function setReleaseInterval(uint48 _interval) external onlyOwner { 60 | releaseInterval = _interval; 61 | } 62 | 63 | function contractBalance() internal view returns (uint256) { 64 | return crodoToken.balanceOf(address(this)); 65 | } 66 | 67 | function getParticipant(address _participant) 68 | public 69 | view 70 | returns ( 71 | uint256, 72 | uint256, 73 | uint256, 74 | uint256 75 | ) 76 | { 77 | Participant memory participant = participants[_participant]; 78 | return ( 79 | participant.minBuyAllowed, 80 | participant.maxBuyAllowed, 81 | participant.reserved, 82 | participant.sent 83 | ); 84 | } 85 | 86 | function addParticipant( 87 | address _participant, 88 | uint256 minBuyAllowed, 89 | uint256 maxBuyAllowed 90 | ) public onlyOwner { 91 | Participant storage participant = participants[_participant]; 92 | participant.minBuyAllowed = minBuyAllowed; 93 | participant.maxBuyAllowed = maxBuyAllowed; 94 | totalMinBuyAllowed += minBuyAllowed; 95 | totalMaxBuyAllowed += maxBuyAllowed; 96 | 97 | participantAddrs.push(_participant); 98 | emit ParticipantAdded(_participant); 99 | } 100 | 101 | function addParticipants( 102 | address[] memory _participants, 103 | uint256[] memory minBuyAllowed, 104 | uint256[] memory maxBuyAllowed 105 | ) public onlyOwner { 106 | require( 107 | (_participants.length == minBuyAllowed.length) && (_participants.length == maxBuyAllowed.length), 108 | "Provided participant info arrays must all have the same length" 109 | ); 110 | for (uint i = 0; i < _participants.length; ++i) { 111 | addParticipant(_participants[i], minBuyAllowed[i], maxBuyAllowed[i]); 112 | } 113 | } 114 | 115 | function removeParticipant(address _participant) external onlyOwner { 116 | Participant memory participant = participants[_participant]; 117 | 118 | require( 119 | participant.reserved == 0, 120 | "Can't remove participant that has already locked some tokens" 121 | ); 122 | 123 | totalMaxBuyAllowed -= participant.maxBuyAllowed; 124 | totalMinBuyAllowed -= participant.minBuyAllowed; 125 | 126 | delete participants[_participant]; 127 | emit ParticipantRemoved(_participant); 128 | } 129 | 130 | function calculateUSDTPrice(uint256 amount) 131 | internal 132 | view 133 | returns (uint256) 134 | { 135 | return amount * USDTPerToken; 136 | } 137 | 138 | // Main function to purchase tokens during Private Sale. Buyer pays in fixed 139 | // rate of USDT for requested amount of CROD tokens. The USDT tokens must be 140 | // delegated for use to this contract beforehand by the user (call to ERC20.approve) 141 | // 142 | // @IMPORTANT: `amount` is expected to be in non-decimal form, 143 | // so 'boughtTokens = amount * (10 ^ crodoToken.decimals())' 144 | // 145 | // We need to cover some cases here: 146 | // 1) Our contract doesn't have requested amount of tokens left 147 | // 2) User tries to exceed their buy limit 148 | // 3) User tries to purchase tokens below their min limit 149 | function lockTokens(uint256 amount) external returns (uint256) { 150 | // Cover case 1 151 | require( 152 | (totalBought + amount * saleDecimals) <= contractBalance(), 153 | "Contract doesn't have requested amount of tokens left" 154 | ); 155 | 156 | Participant storage participant = participants[msg.sender]; 157 | 158 | // Cover case 2 159 | require( 160 | participant.reserved + amount <= participant.maxBuyAllowed, 161 | "User tried to exceed their buy-high limit" 162 | ); 163 | 164 | // Cover case 3 165 | require( 166 | participant.reserved + amount >= participant.minBuyAllowed, 167 | "User tried to purchase tokens below their minimum limit" 168 | ); 169 | 170 | uint256 usdtPrice = calculateUSDTPrice(amount); 171 | require( 172 | usdtToken.balanceOf(msg.sender) >= usdtPrice, 173 | "User doesn't have enough USDT to buy requested tokens" 174 | ); 175 | 176 | require( 177 | usdtToken.allowance(msg.sender, address(this)) >= usdtPrice, 178 | "User hasn't delegated required amount of tokens for the operation" 179 | ); 180 | 181 | usdtToken.transferFrom(msg.sender, address(this), usdtPrice); 182 | participant.reserved += amount; 183 | totalBought += amount * saleDecimals; 184 | return amount; 185 | } 186 | 187 | function releaseTokens() external returns (uint256) { 188 | require( 189 | initReleaseDate <= block.timestamp, 190 | "Initial release date hasn't passed yet" 191 | ); 192 | require( 193 | (initReleaseDate + currentRelease * releaseInterval) <= 194 | block.timestamp, 195 | string( 196 | abi.encodePacked( 197 | "Can only release tokens after initial release date has passed and once " 198 | "in the release interval. inital date: ", 199 | Strings.toString(initReleaseDate) 200 | ) 201 | ) 202 | ); 203 | 204 | ++currentRelease; 205 | uint256 tokensSent = 0; 206 | for (uint32 i = 0; i < participantAddrs.length; ++i) { 207 | address participantAddr = participantAddrs[i]; 208 | Participant storage participant = participants[participantAddr]; 209 | uint256 lockedTokensLeft = (participant.reserved * saleDecimals) - 210 | participant.sent; 211 | if ( 212 | (participant.reserved * saleDecimals) > 0 && 213 | (lockedTokensLeft > 0) 214 | ) { 215 | uint256 roundAmount = (participant.reserved * saleDecimals) / 216 | totalReleases; 217 | 218 | // If on the last release tokens don't round up after dividing, 219 | // or locked tokens is less than calcualted amount to send, 220 | // just send the whole remaining tokens 221 | if ( 222 | (currentRelease >= totalReleases && 223 | roundAmount != lockedTokensLeft) || 224 | (roundAmount > lockedTokensLeft) 225 | ) { 226 | roundAmount = lockedTokensLeft; 227 | } 228 | 229 | require( 230 | roundAmount <= contractBalance(), 231 | "Internal Error: Contract doens't have enough tokens to transfer to buyer" 232 | ); 233 | 234 | crodoToken.transfer(participantAddr, roundAmount); 235 | participant.sent += roundAmount; 236 | tokensSent += roundAmount; 237 | } 238 | } 239 | 240 | emit ReleaseFinished(currentRelease - 1); 241 | return tokensSent; 242 | } 243 | 244 | /* 245 | * Owner-only functions 246 | */ 247 | 248 | function pullUSDT(address receiver, uint256 amount) external onlyOwner { 249 | usdtToken.transfer(receiver, amount); 250 | } 251 | 252 | function lockForParticipant(address _participant, uint256 amount) 253 | external 254 | onlyOwner 255 | returns (uint256) 256 | { 257 | require( 258 | (totalBought + amount * saleDecimals) <= contractBalance(), 259 | "Contract doesn't have requested amount of tokens left" 260 | ); 261 | 262 | Participant storage participant = participants[_participant]; 263 | 264 | require( 265 | participant.reserved + amount < participant.maxBuyAllowed, 266 | "User tried to exceed their buy-high limit" 267 | ); 268 | 269 | require( 270 | participant.reserved + amount > participant.minBuyAllowed, 271 | "User tried to purchase tokens below their minimum limit" 272 | ); 273 | 274 | participant.reserved += amount; 275 | totalBought += amount * saleDecimals; 276 | return amount; 277 | } 278 | } 279 | 280 | contract CrodoSeedSale is BaseLimitedSale { 281 | constructor( 282 | address _crodoToken, 283 | address _usdtAddress, 284 | uint256 _USDTPerToken, 285 | uint48 _initReleaseDate, 286 | uint8 _totalReleases 287 | ) 288 | BaseLimitedSale( 289 | _crodoToken, 290 | _usdtAddress, 291 | _USDTPerToken, 292 | _initReleaseDate, 293 | _totalReleases 294 | ) 295 | {} 296 | } 297 | 298 | contract CrodoPrivateSale is BaseLimitedSale { 299 | constructor( 300 | address _crodoToken, 301 | address _usdtAddress, 302 | uint256 _USDTPerToken, 303 | uint48 _initReleaseDate, 304 | uint8 _totalReleases 305 | ) 306 | BaseLimitedSale( 307 | _crodoToken, 308 | _usdtAddress, 309 | _USDTPerToken, 310 | _initReleaseDate, 311 | _totalReleases 312 | ) 313 | {} 314 | } 315 | 316 | contract CrodoStrategicSale is BaseLimitedSale { 317 | constructor( 318 | address _crodoToken, 319 | address _usdtAddress, 320 | uint256 _USDTPerToken, 321 | uint48 _initReleaseDate, 322 | uint8 _totalReleases 323 | ) 324 | BaseLimitedSale( 325 | _crodoToken, 326 | _usdtAddress, 327 | _USDTPerToken, 328 | _initReleaseDate, 329 | _totalReleases 330 | ) 331 | {} 332 | } 333 | 334 | contract CrodoPublicSale is BaseLimitedSale { 335 | constructor( 336 | address _crodoToken, 337 | address _usdtAddress, 338 | uint256 _USDTPerToken, 339 | uint48 _initReleaseDate, 340 | uint8 _totalReleases 341 | ) 342 | BaseLimitedSale( 343 | _crodoToken, 344 | _usdtAddress, 345 | _USDTPerToken, 346 | _initReleaseDate, 347 | _totalReleases 348 | ) 349 | {} 350 | } 351 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.4.22 <0.9.0; 3 | 4 | contract Migrations { 5 | address public owner = msg.sender; 6 | uint256 public lastCompletedMigration; 7 | 8 | modifier restricted() { 9 | require( 10 | msg.sender == owner, 11 | "This function is restricted to the contract's owner" 12 | ); 13 | _; 14 | } 15 | 16 | function setCompleted(uint256 completed) public restricted { 17 | lastCompletedMigration = completed; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /contracts/TestToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract TestToken is ERC20 { 7 | string private _name = "TestToken"; 8 | string private _symbol = "TK"; 9 | uint8 private _decimals; 10 | 11 | constructor( 12 | uint8 num_decimals, 13 | address mintAddress, 14 | uint256 mintAmount 15 | ) ERC20(_name, _symbol) { 16 | _decimals = num_decimals; 17 | _mint(mintAddress, mintAmount); 18 | } 19 | 20 | function mint(address account, uint256 amount) public { 21 | _mint(account, amount); 22 | } 23 | 24 | function decimals() public view override returns (uint8) { 25 | return _decimals; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /contracts/crodoContract.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "@openzeppelin/contracts/access/AccessControl.sol"; 6 | import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; 7 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 8 | 9 | contract CRDStake is AccessControl, ReentrancyGuard { 10 | using SafeERC20 for IERC20; 11 | 12 | // bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00; 13 | bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); 14 | 15 | event Stake(address indexed wallet, uint256 amount, uint256 date); 16 | event Withdraw(address indexed wallet, uint256 amount, uint256 date); 17 | event Claimed( 18 | address indexed wallet, 19 | address indexed rewardToken, 20 | uint256 amount 21 | ); 22 | 23 | event RewardTokenChanged( 24 | address indexed oldRewardToken, 25 | uint256 returnedAmount, 26 | address indexed newRewardToken 27 | ); 28 | event LockTimePeriodMinChanged(uint48 lockTimePeriodMin); 29 | event LockTimePeriodMaxChanged(uint48 lockTimePeriodMax); 30 | event StakeRewardFactorChanged(uint256 stakeRewardFactor); 31 | event StakeRewardEndTimeChanged(uint48 stakeRewardEndTime); 32 | event RewardsBurned(address indexed staker, uint256 amount); 33 | event ERC20TokensRemoved( 34 | address indexed tokenAddress, 35 | address indexed receiver, 36 | uint256 amount 37 | ); 38 | 39 | uint48 public constant MAX_TIME = type(uint48).max; // = 2^48 - 1 40 | 41 | struct User { 42 | uint48 stakeTime; 43 | uint48 unlockTime; 44 | uint48 lockTime; 45 | // Used to calculate how long the tokens are being staked, 46 | // the difference between `stakeTime` is that `stakedSince` only updates 47 | // when user withdraws tokens from the stake pull. 48 | uint48 stakedSince; 49 | uint256 stakeAmount; 50 | uint256 accumulatedRewards; 51 | } 52 | 53 | mapping(address => User) public userMap; 54 | 55 | uint256 public tokenTotalStaked; // sum of all staked tokens 56 | 57 | address public immutable stakingToken; // address of token which can be staked into this contract 58 | address public rewardToken; // address of reward token 59 | 60 | /** 61 | * Using block.timestamp instead of block.number for reward calculation 62 | * 1) Easier to handle for users 63 | * 2) Should result in same rewards across different chain with different block times 64 | * 3) "The current block timestamp must be strictly larger than the timestamp of the last block, ... 65 | * but the only guarantee is that it will be somewhere between the timestamps ... 66 | * of two consecutive blocks in the canonical chain." 67 | * https://docs.soliditylang.org/en/v0.7.6/cheatsheet.html?highlight=block.timestamp#global-variables 68 | */ 69 | 70 | // time in seconds a user has to wait after calling unlock until staked token can be withdrawn 71 | uint48 public lockTimePeriodMin; 72 | uint48 public lockTimePeriodMax; 73 | uint48 public stakeRewardEndTime; // unix time in seconds when the reward scheme will end 74 | uint256 public stakeRewardFactor; // time in seconds * amount of staked token to receive 1 reward token 75 | 76 | constructor( 77 | address _stakingToken, 78 | uint48 _lockTimePeriodMin, 79 | uint48 _lockTimePeriodMax 80 | ) { 81 | require(_stakingToken != address(0), "stakingToken.address == 0"); 82 | stakingToken = _stakingToken; 83 | lockTimePeriodMin = _lockTimePeriodMin; 84 | lockTimePeriodMax = _lockTimePeriodMax; 85 | // set some defaults 86 | stakeRewardFactor = 1000 * 1 days; // a user has to stake 1000 token for 1 day to receive 1 reward token 87 | stakeRewardEndTime = uint48(block.timestamp + 366 days); // reward scheme ends in 1 year 88 | _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); 89 | } 90 | 91 | /** 92 | * based on OpenZeppelin SafeCast v4.3 93 | * https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.3/contracts/utils/math/SafeCast.sol 94 | */ 95 | 96 | function toUint48(uint256 value) internal pure returns (uint48) { 97 | require(value <= type(uint48).max, "value doesn't fit in 48 bits"); 98 | return uint48(value); 99 | } 100 | 101 | function toUint160(uint256 value) internal pure returns (uint160) { 102 | require(value <= type(uint160).max, "value doesn't fit in 160 bits"); 103 | return uint160(value); 104 | } 105 | 106 | /** 107 | * External API functions 108 | */ 109 | 110 | function stakeTime(address _staker) 111 | external 112 | view 113 | returns (uint48 dateTime) 114 | { 115 | return userMap[_staker].stakeTime; 116 | } 117 | 118 | function stakedSince(address _staker) 119 | external 120 | view 121 | returns (uint48 dateTime) 122 | { 123 | return userMap[_staker].stakedSince; 124 | } 125 | 126 | function stakeAmount(address _staker) 127 | external 128 | view 129 | returns (uint256 balance) 130 | { 131 | return userMap[_staker].stakeAmount; 132 | } 133 | 134 | function getLockTime(address _staker) 135 | external 136 | view 137 | returns (uint48 lockTime) 138 | { 139 | return userMap[_staker].lockTime; 140 | } 141 | 142 | // redundant with stakeAmount() for compatibility 143 | function balanceOf(address _staker) 144 | external 145 | view 146 | returns (uint256 balance) 147 | { 148 | return userMap[_staker].stakeAmount; 149 | } 150 | 151 | function userAccumulatedRewards(address _staker) 152 | external 153 | view 154 | returns (uint256 rewards) 155 | { 156 | return userMap[_staker].accumulatedRewards; 157 | } 158 | 159 | /** 160 | * @dev return unix epoch time when staked tokens will be unlocked 161 | * @dev return MAX_INT_UINT48 = 2**48-1 if user has no token staked 162 | * @dev this always allows an easy check with : require(block.timestamp > getUnlockTime(account)); 163 | * @return unlockTime unix epoch time in seconds 164 | */ 165 | function getUnlockTime(address _staker) 166 | public 167 | view 168 | returns (uint48 unlockTime) 169 | { 170 | return 171 | userMap[_staker].stakeAmount > 0 172 | ? userMap[_staker].unlockTime 173 | : MAX_TIME; 174 | } 175 | 176 | /** 177 | * @return balance of reward tokens held by this contract 178 | */ 179 | function getRewardTokenBalance() public view returns (uint256 balance) { 180 | if (rewardToken == address(0)) return 0; 181 | balance = IERC20(rewardToken).balanceOf(address(this)); 182 | if (stakingToken == rewardToken) { 183 | balance -= tokenTotalStaked; 184 | } 185 | } 186 | 187 | // onlyOwner / DEFAULT_ADMIN_ROLE functions -------------------------------------------------- 188 | 189 | /** 190 | * @notice setting rewardToken to address(0) disables claim/mint 191 | * @notice if there was a reward token set before, return remaining tokens to msg.sender/admin 192 | * @param newRewardToken address 193 | */ 194 | function setRewardToken(address newRewardToken) 195 | external 196 | nonReentrant 197 | onlyRole(DEFAULT_ADMIN_ROLE) 198 | { 199 | address oldRewardToken = rewardToken; 200 | uint256 rewardBalance = getRewardTokenBalance(); // balance of oldRewardToken 201 | if (rewardBalance > 0) { 202 | IERC20(oldRewardToken).safeTransfer(msg.sender, rewardBalance); 203 | } 204 | rewardToken = newRewardToken; 205 | emit RewardTokenChanged(oldRewardToken, rewardBalance, newRewardToken); 206 | } 207 | 208 | /** 209 | * @notice set min time a user has to wait after calling unlock until staked token can be withdrawn 210 | * @param _lockTimePeriodMin time in seconds 211 | */ 212 | function setLockTimePeriodMin(uint48 _lockTimePeriodMin) 213 | external 214 | onlyRole(DEFAULT_ADMIN_ROLE) 215 | { 216 | lockTimePeriodMin = _lockTimePeriodMin; 217 | emit LockTimePeriodMinChanged(_lockTimePeriodMin); 218 | } 219 | 220 | /** 221 | * @notice set max time a user has to wait after calling unlock until staked token can be withdrawn 222 | * @param _lockTimePeriodMax time in seconds 223 | */ 224 | function setLockTimePeriodMax(uint48 _lockTimePeriodMax) 225 | external 226 | onlyRole(DEFAULT_ADMIN_ROLE) 227 | { 228 | lockTimePeriodMax = _lockTimePeriodMax; 229 | emit LockTimePeriodMaxChanged(_lockTimePeriodMax); 230 | } 231 | 232 | /** 233 | * @notice see calculateUserClaimableReward() docs 234 | * @dev requires that reward token has the same decimals as stake token 235 | * @param _stakeRewardFactor time in seconds * amount of staked token to receive 1 reward token 236 | */ 237 | function setStakeRewardFactor(uint256 _stakeRewardFactor) 238 | external 239 | onlyRole(DEFAULT_ADMIN_ROLE) 240 | { 241 | stakeRewardFactor = _stakeRewardFactor; 242 | emit StakeRewardFactorChanged(_stakeRewardFactor); 243 | } 244 | 245 | /** 246 | * @notice set block time when stake reward scheme will end 247 | * @param _stakeRewardEndTime unix time in seconds 248 | */ 249 | function setStakeRewardEndTime(uint48 _stakeRewardEndTime) 250 | external 251 | onlyRole(DEFAULT_ADMIN_ROLE) 252 | { 253 | require( 254 | stakeRewardEndTime > block.timestamp, 255 | "time has to be in the future" 256 | ); 257 | stakeRewardEndTime = _stakeRewardEndTime; 258 | emit StakeRewardEndTimeChanged(_stakeRewardEndTime); 259 | } 260 | 261 | /** 262 | * ADMIN_ROLE has to set BURNER_ROLE 263 | * allows an external (lottery token sale) contract to substract rewards 264 | */ 265 | function burnRewards(address _staker, uint256 _amount) 266 | external 267 | onlyRole(BURNER_ROLE) 268 | { 269 | User storage user = _updateRewards(_staker); 270 | 271 | if (_amount < user.accumulatedRewards) { 272 | user.accumulatedRewards -= _amount; // safe 273 | } else { 274 | user.accumulatedRewards = 0; // burn at least all what's there 275 | } 276 | emit RewardsBurned(_staker, _amount); 277 | } 278 | 279 | /** msg.sender external view convenience functions *********************************/ 280 | 281 | function stakeAmount_msgSender() public view returns (uint256) { 282 | return userMap[msg.sender].stakeAmount; 283 | } 284 | 285 | function stakeLockTime_msgSender() external view returns (uint48) { 286 | return userMap[msg.sender].lockTime; 287 | } 288 | 289 | function stakeTime_msgSender() external view returns (uint48) { 290 | return userMap[msg.sender].stakeTime; 291 | } 292 | 293 | function getUnlockTime_msgSender() 294 | external 295 | view 296 | returns (uint48 unlockTime) 297 | { 298 | return getUnlockTime(msg.sender); 299 | } 300 | 301 | function userClaimableRewards_msgSender() external view returns (uint256) { 302 | return userClaimableRewards(msg.sender); 303 | } 304 | 305 | function userAccumulatedRewards_msgSender() 306 | external 307 | view 308 | returns (uint256) 309 | { 310 | return userMap[msg.sender].accumulatedRewards; 311 | } 312 | 313 | function userTotalRewards_msgSender() external view returns (uint256) { 314 | return userTotalRewards(msg.sender); 315 | } 316 | 317 | function getEarnedRewardTokens_msgSender() external view returns (uint256) { 318 | return getEarnedRewardTokens(msg.sender); 319 | } 320 | 321 | /** public external view functions (also used internally) **************************/ 322 | 323 | /** 324 | * calculates unclaimed rewards 325 | * unclaimed rewards = expired time since last stake/unstake transaction * current staked amount 326 | * 327 | * We have to cover 6 cases here : 328 | * 1) block time < stake time < end time : should never happen => error 329 | * 2) block time < end time < stake time : should never happen => error 330 | * 3) end time < block time < stake time : should never happen => error 331 | * 4) end time < stake time < block time : staked after reward period is over => no rewards 332 | * 5) stake time < block time < end time : end time in the future 333 | * 6) stake time < end time < block time : end time in the past & staked before 334 | * @param _staker address 335 | * @return claimableRewards = timePeriod * stakeAmount 336 | */ 337 | function userClaimableRewards(address _staker) 338 | public 339 | view 340 | returns (uint256) 341 | { 342 | User storage user = userMap[_staker]; 343 | // case 1) 2) 3) 344 | // stake time in the future - should never happen - actually an (internal ?) error 345 | if (block.timestamp <= user.stakeTime) return 0; 346 | 347 | // case 4) 348 | // staked after reward period is over => no rewards 349 | // end time < stake time < block time 350 | if (stakeRewardEndTime <= user.stakeTime) return 0; 351 | 352 | uint256 timePeriod; 353 | 354 | // case 5 355 | // we have not reached the end of the reward period 356 | // stake time < block time < end time 357 | if (block.timestamp <= stakeRewardEndTime) { 358 | timePeriod = block.timestamp - user.stakeTime; // covered by case 1) 2) 3) 'if' 359 | } else { 360 | // case 6 361 | // user staked before end of reward period , but that is in the past now 362 | // stake time < end time < block time 363 | timePeriod = stakeRewardEndTime - user.stakeTime; // covered case 4) 364 | } 365 | 366 | return timePeriod * user.stakeAmount; 367 | } 368 | 369 | function userTotalRewards(address _staker) public view returns (uint256) { 370 | return 371 | userClaimableRewards(_staker) + userMap[_staker].accumulatedRewards; 372 | } 373 | 374 | function getEarnedRewardTokens(address _staker) 375 | public 376 | view 377 | returns (uint256 claimableRewardTokens) 378 | { 379 | if (address(rewardToken) == address(0) || stakeRewardFactor == 0) { 380 | return 0; 381 | } else { 382 | return userTotalRewards(_staker) / stakeRewardFactor; // safe 383 | } 384 | } 385 | 386 | /** 387 | * @dev whenver the staked balance changes do ... 388 | * 389 | * @dev calculate userClaimableRewards = previous staked amount * (current time - last stake time) 390 | * @dev add userClaimableRewards to userAccumulatedRewards 391 | * @dev reset userClaimableRewards to 0 by setting stakeTime to current time 392 | * @dev not used as doing it inline, local, within a function consumes less gas 393 | * 394 | * @return user reference pointer for further processing 395 | */ 396 | function _updateRewards(address _staker) 397 | internal 398 | returns (User storage user) 399 | { 400 | // calculate reward credits using previous staking amount and previous time period 401 | // add new reward credits to already accumulated reward credits 402 | user = userMap[_staker]; 403 | user.accumulatedRewards += userClaimableRewards(_staker); 404 | 405 | // update stake Time to current time (start new reward period) 406 | // will also reset userClaimableRewards() 407 | user.stakeTime = toUint48(block.timestamp); 408 | 409 | if (user.stakedSince == 0) { 410 | user.stakedSince = toUint48(block.timestamp); 411 | } 412 | } 413 | 414 | /** 415 | * add stake token to staking pool 416 | * @dev requires the token to be approved for transfer 417 | * @dev we assume that (our) stake token is not malicious, so no special checks 418 | * @param _amount of token to be staked 419 | * @param _lockTime period for staking 420 | */ 421 | function _stake(uint256 _amount, uint48 _lockTime) 422 | internal 423 | returns (uint256) 424 | { 425 | require(_amount > 0, "stake amount must be > 0"); 426 | require( 427 | _lockTime <= lockTimePeriodMax, 428 | "lockTime must by < lockTimePeriodMax" 429 | ); 430 | require( 431 | _lockTime >= lockTimePeriodMin, 432 | "lockTime must by > lockTimePeriodMin" 433 | ); 434 | 435 | User storage user = _updateRewards(msg.sender); // update rewards and return reference to user 436 | 437 | require( 438 | block.timestamp + _lockTime >= user.unlockTime, 439 | "locktime must be >= current lock time" 440 | ); 441 | 442 | user.stakeAmount = toUint160(user.stakeAmount + _amount); 443 | tokenTotalStaked += _amount; 444 | 445 | user.unlockTime = toUint48(block.timestamp + _lockTime); 446 | 447 | user.lockTime = toUint48(_lockTime); 448 | 449 | // using SafeERC20 for IERC20 => will revert in case of error 450 | IERC20(stakingToken).safeTransferFrom( 451 | msg.sender, 452 | address(this), 453 | _amount 454 | ); 455 | 456 | emit Stake(msg.sender, _amount, toUint48(block.timestamp)); // = user.stakeTime 457 | return _amount; 458 | } 459 | 460 | /** 461 | * withdraw staked token, ... 462 | * do not withdraw rewards token (it might not be worth the gas) 463 | * @return amount of tokens sent to user's account 464 | */ 465 | function _withdraw(uint256 amount) internal returns (uint256) { 466 | require(amount > 0, "amount to withdraw not > 0"); 467 | require( 468 | block.timestamp > getUnlockTime(msg.sender), 469 | "staked tokens are still locked" 470 | ); 471 | 472 | User storage user = _updateRewards(msg.sender); // update rewards and return reference to user 473 | 474 | require(amount <= user.stakeAmount, "withdraw amount > staked amount"); 475 | user.stakeAmount -= toUint160(amount); 476 | user.stakedSince = toUint48(block.timestamp); 477 | tokenTotalStaked -= amount; 478 | 479 | // using SafeERC20 for IERC20 => will revert in case of error 480 | IERC20(stakingToken).safeTransfer(msg.sender, amount); 481 | 482 | emit Withdraw(msg.sender, amount, toUint48(block.timestamp)); // = user.stakeTime 483 | return amount; 484 | } 485 | 486 | /** 487 | * claim reward tokens for accumulated reward credits 488 | * ... but do not unstake staked token 489 | */ 490 | function _claim() internal returns (uint256) { 491 | require(rewardToken != address(0), "no reward token contract"); 492 | uint256 earnedRewardTokens = getEarnedRewardTokens(msg.sender); 493 | require(earnedRewardTokens > 0, "no tokens to claim"); 494 | 495 | // like _updateRewards() , but reset all rewards to 0 496 | User storage user = userMap[msg.sender]; 497 | user.accumulatedRewards = 0; 498 | user.stakeTime = toUint48(block.timestamp); // will reset userClaimableRewards to 0 499 | user.stakedSince = toUint48(block.timestamp); 500 | // user.stakeAmount = unchanged 501 | 502 | require( 503 | earnedRewardTokens <= getRewardTokenBalance(), 504 | "not enough reward tokens" 505 | ); // redundant but dedicated error message 506 | IERC20(rewardToken).safeTransfer(msg.sender, earnedRewardTokens); 507 | 508 | emit Claimed(msg.sender, rewardToken, earnedRewardTokens); 509 | return earnedRewardTokens; 510 | } 511 | 512 | function restakeRewards() public returns (uint256) { 513 | require( 514 | stakingToken == rewardToken, 515 | "Can't restake rewards, pool has different stake and reward tokens" 516 | ); 517 | 518 | User storage user = userMap[msg.sender]; 519 | user.stakeAmount += getEarnedRewardTokens(msg.sender); 520 | user.stakeTime = toUint48(block.timestamp); // will reset userClaimableRewards to 0 521 | user.accumulatedRewards = 0; 522 | 523 | return user.stakeAmount; 524 | } 525 | 526 | function stake(uint256 _amount, uint48 _lockTime) 527 | external 528 | nonReentrant 529 | returns (uint256) 530 | { 531 | return _stake(_amount, _lockTime); 532 | } 533 | 534 | function claim() external nonReentrant returns (uint256) { 535 | return _claim(); 536 | } 537 | 538 | function withdraw(uint256 amount) external nonReentrant returns (uint256) { 539 | return _withdraw(amount); 540 | } 541 | 542 | function withdrawAll() external nonReentrant returns (uint256) { 543 | return _withdraw(stakeAmount_msgSender()); 544 | } 545 | 546 | /** 547 | * Do not accept accidently sent ETH : 548 | * If neither a receive Ether nor a payable fallback function is present, 549 | * the contract cannot receive Ether through regular transactions and throws an exception. 550 | * https://docs.soliditylang.org/en/v0.8.7/contracts.html#receive-ether-function 551 | */ 552 | 553 | /** 554 | * @notice withdraw accidently sent ERC20 tokens 555 | * @param _tokenAddress address of token to withdraw 556 | */ 557 | function removeOtherERC20Tokens(address _tokenAddress) 558 | external 559 | onlyRole(DEFAULT_ADMIN_ROLE) 560 | { 561 | require( 562 | _tokenAddress != address(stakingToken), 563 | "can not withdraw staking token" 564 | ); 565 | uint256 balance = IERC20(_tokenAddress).balanceOf(address(this)); 566 | IERC20(_tokenAddress).safeTransfer(msg.sender, balance); 567 | emit ERC20TokensRemoved(_tokenAddress, msg.sender, balance); 568 | } 569 | } 570 | -------------------------------------------------------------------------------- /contracts/crodoToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Capped.sol"; 7 | import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol"; 8 | 9 | contract CrodoToken is ERC20, ERC20Pausable, ERC20Capped { 10 | string private _name = "CrodoToken"; 11 | string private _symbol = "CROD"; 12 | uint8 private _decimals = 18; 13 | address public distributionContractAddress; 14 | // 100 Million <---------| |-----------------> 10^18 15 | uint256 constant TOTAL_CAP = 100000000 * 1 ether; 16 | 17 | constructor(address _distributionContract) 18 | ERC20Capped(TOTAL_CAP) 19 | ERC20(_name, _symbol) 20 | { 21 | distributionContractAddress = _distributionContract; 22 | _mint(distributionContractAddress, TOTAL_CAP); 23 | } 24 | 25 | function mint(address account, uint256 amount) public { 26 | _mint(account, amount); 27 | } 28 | 29 | function _mint(address account, uint256 amount) 30 | internal 31 | virtual 32 | override(ERC20, ERC20Capped) 33 | { 34 | ERC20Capped._mint(account, amount); 35 | } 36 | 37 | function _beforeTokenTransfer( 38 | address from, 39 | address to, 40 | uint256 amount 41 | ) internal virtual override(ERC20, ERC20Pausable) { 42 | ERC20._beforeTokenTransfer(from, to, amount); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /contracts/distributionContract.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "@openzeppelin/contracts/utils/math/SafeMath.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 7 | import "@openzeppelin/contracts/access/Ownable.sol"; 8 | import "@openzeppelin/contracts/security/Pausable.sol"; 9 | 10 | /* 11 | * Params for the CROD token: 12 | * Total supply - 100_000_000 CROD 13 | * Initial supply - 26_300_000 CROD 14 | * 15 | * Implemented vesting: 16 | * 17 | * 6% Seed - cliff for 6 month, 3.7% unlocked each month within 27 months 18 | * 8% Private sale - cliff for 3 month, 3,85% unlocked each month within 26 months 19 | * 8% Strategic sale - cliff for 3 month, 4,17% unlocked each month within 24 months 20 | * 4% Public sale - 12% unlocked, cliff for 3 month, 17,6% unlocked each month within 5 months 21 | * 15% Team - cliff for 17 months, 4% unlocked each month within 25 months 22 | * 6% Advisors - cliff for 17 months, 4% unlocked each month within 25 months 23 | * 12% Liquidity - Fully unlocked 24 | * 20% Strategic Reserve - cliff for 6 month, 2,85% unlocked each month within 35 months 25 | * 21% Community / Ecosystem - 5% unlocked, 2,97% unlocked each month within 33 months 26 | */ 27 | 28 | contract CrodoDistributionContract is Pausable, Ownable { 29 | using SafeMath for uint256; 30 | 31 | uint256 public decimals; 32 | uint48 public TGEDate = 0; /* Date From where the distribution starts (TGE) */ 33 | uint256 public constant month = 30 days; 34 | uint256 public constant year = 365 days; 35 | uint256 public lastDateDistribution = 0; 36 | 37 | // All these addresses must be unique 38 | address[] public seedWallet; 39 | address[] public privSaleWallet; 40 | address[] public strategicSaleWallet; 41 | address[] public pubSaleWallet; 42 | address[] public teamWallet; 43 | address[] public teamWallets = [ 44 | 0xcF528152C7619E23d0c6A16de75E6B30A45Bf502, 45 | 0x72245A3E23E7F73e5eaD2857b990b74a27FB95d4, 46 | 0xC1A14B3CC70d3a1FD4f8e45FeA6B0c755f5a3D4A, 47 | 0xC6F8fa17836fEebBD836f7F5986942e8d102B683 48 | ]; // TODO: Change these to correct addresses 49 | address[] public advisorsWallet; 50 | address[] public liquidityWallet; 51 | address[] public strategicWallet; 52 | address[] public communityWallet; 53 | 54 | uint256 private currentCategory; 55 | /* Distribution object */ 56 | mapping(uint256 => DistributionCategory) private distributionCategories; 57 | 58 | ERC20 public erc20; 59 | 60 | struct DistributionCategory { 61 | address[] destinations; 62 | DistributionStep[] distributions; 63 | } 64 | 65 | struct DistributionStep { 66 | uint256 amountAllocated; 67 | uint256 currentAllocated; 68 | uint256 unlockDay; 69 | uint256 amountSent; 70 | } 71 | 72 | constructor( 73 | address[] memory _seedWallet, 74 | address[] memory _privSaleWallet, 75 | address[] memory _strategicSaleWallet, 76 | address[] memory _pubSaleWallet, 77 | address[] memory _advisorsWallet, 78 | address[] memory _liquidityWallet, 79 | address[] memory _strategicWallet, 80 | address[] memory _communityWallet 81 | ) Ownable() Pausable() { 82 | seedWallet = _seedWallet; 83 | privSaleWallet = _privSaleWallet; 84 | strategicSaleWallet = _strategicSaleWallet; 85 | pubSaleWallet = _pubSaleWallet; 86 | advisorsWallet = _advisorsWallet; 87 | liquidityWallet = _liquidityWallet; 88 | strategicWallet = _strategicWallet; 89 | communityWallet = _communityWallet; 90 | } 91 | 92 | function initAllRounds() external onlyOwner { 93 | setSeedRound(); 94 | setPrivateRound(); 95 | setStrategicSaleRound(); 96 | setPublicRound(); 97 | setTeamRound(); 98 | setAdvisorsRound(); 99 | setLiquidityRound(); 100 | setStrategicRound(); 101 | setCommunityRound(); 102 | } 103 | 104 | function setSeedRound() public onlyOwner { 105 | // 6% Seed - cliff for 6 month, 3.7% unlocked each month within 27 months 106 | // The locking and vesting is done in seed sale contract. 107 | initializeDistributionCategory(currentCategory, seedWallet); 108 | addDistributionToCategory(currentCategory, 6000000, 0); 109 | ++currentCategory; 110 | } 111 | 112 | function setPrivateRound() public onlyOwner { 113 | // 8% Private sale - cliff for 3 month, 3,85% unlocked each month within 26 months 114 | // The locking and vesting is done in private sale contract. 115 | initializeDistributionCategory(currentCategory, privSaleWallet); 116 | addDistributionToCategory(currentCategory, 8000000, 0); 117 | ++currentCategory; 118 | } 119 | 120 | function setStrategicSaleRound() public onlyOwner { 121 | // 8% Strategic sale - cliff for 3 month, 4,17% unlocked each month within 24 months 122 | // The locking and vesting is done in strategic sale contract. 123 | initializeDistributionCategory(currentCategory, strategicSaleWallet); 124 | addDistributionToCategory(currentCategory, 8000000, 0); 125 | ++currentCategory; 126 | } 127 | 128 | function setPublicRound() public onlyOwner { 129 | // 4% Public sale - 12% unlocked, cliff for 3 month, 17,6% unlocked each month within 5 months 130 | initializeDistributionCategory(currentCategory, pubSaleWallet); 131 | addDistributionToCategory(currentCategory, 4000000, 0); 132 | ++currentCategory; 133 | } 134 | 135 | function setTeamRound() public onlyOwner { 136 | // 15% Team - cliff for 17 months, 4% unlocked each month within 25 months 137 | initializeDistributionCategory(currentCategory, teamWallets); 138 | for (uint8 i = 17; i < 42; ++i) { 139 | addDistributionToCategory(currentCategory, 600000, i * month); 140 | } 141 | ++currentCategory; 142 | } 143 | 144 | function setAdvisorsRound() public onlyOwner { 145 | // 6% Advisors - cliff for 17 months, 4% unlocked each month within 25 months 146 | initializeDistributionCategory(currentCategory, advisorsWallet); 147 | for (uint8 i = 17; i < 42; ++i) { 148 | addDistributionToCategory(currentCategory, 240000, i * month); 149 | } 150 | ++currentCategory; 151 | } 152 | 153 | function setLiquidityRound() public onlyOwner { 154 | // 12% Liquidity - Fully unlocked 155 | initializeDistributionCategory(currentCategory, liquidityWallet); 156 | addDistributionToCategory(currentCategory, 12000000, 0); 157 | ++currentCategory; 158 | } 159 | 160 | function setStrategicRound() public onlyOwner { 161 | // 20% Strategic Reserve - cliff for 6 month, 2,85% unlocked each month within 35 months 162 | initializeDistributionCategory(currentCategory, strategicWallet); 163 | uint256 amountEachRound = 570000; 164 | for (uint8 i = 6; i < 40; ++i) { 165 | addDistributionToCategory( 166 | currentCategory, 167 | amountEachRound, 168 | i * month 169 | ); 170 | } 171 | addDistributionToCategory( 172 | currentCategory, 173 | 20000000 - (amountEachRound * 34), 174 | 40 * month 175 | ); 176 | ++currentCategory; 177 | } 178 | 179 | function setCommunityRound() public onlyOwner { 180 | // 21% Community / Ecosystem - 5% unlocked, 2,97% unlocked each month within 32 months 181 | initializeDistributionCategory(currentCategory, communityWallet); 182 | uint256 amountEachRound = 623700; 183 | addDistributionToCategory(currentCategory, 1050000, 0); 184 | uint256 remainingForDist = 21000000 - 1050000; 185 | for (uint8 i = 1; i < 32; ++i) { 186 | addDistributionToCategory( 187 | currentCategory, 188 | amountEachRound, 189 | i * month 190 | ); 191 | } 192 | addDistributionToCategory( 193 | currentCategory, 194 | remainingForDist - (amountEachRound * 31), 195 | 32 * month 196 | ); 197 | ++currentCategory; 198 | } 199 | 200 | function setTokenAddress(address _tokenAddress) 201 | external 202 | onlyOwner 203 | whenNotPaused 204 | { 205 | erc20 = ERC20(_tokenAddress); 206 | decimals = 10**erc20.decimals(); 207 | } 208 | 209 | function safeGuardAllTokens(address _address) 210 | external 211 | onlyOwner 212 | whenPaused 213 | { 214 | /* In case of needed urgency for the sake of contract bug */ 215 | require(erc20.transfer(_address, erc20.balanceOf(address(this)))); 216 | } 217 | 218 | function setTGEDate(uint48 _time) external onlyOwner whenNotPaused { 219 | TGEDate = _time; 220 | } 221 | 222 | function getTGEDate() external view returns (uint48) { 223 | return TGEDate; 224 | } 225 | 226 | /** 227 | * Should allow any address to trigger it, but since the calls are atomic it should do only once per day 228 | */ 229 | 230 | function triggerTokenSend() external whenNotPaused { 231 | /* Require TGE Date already been set */ 232 | require(TGEDate != 0, "TGE date not set yet"); 233 | /* TGE has not started */ 234 | require(block.timestamp > TGEDate, "TGE still hasn't started"); 235 | /* Test that the call be only done once per day */ 236 | require( 237 | block.timestamp.sub(lastDateDistribution) > 1 days, 238 | "Can only be called once a day" 239 | ); 240 | lastDateDistribution = block.timestamp; 241 | for (uint256 i = 0; i < currentCategory; i++) { 242 | /* Get Address Distribution */ 243 | DistributionCategory storage category = distributionCategories[i]; 244 | DistributionStep[] storage d = category.distributions; 245 | /* Go thru all distributions array */ 246 | for (uint256 j = 0; j < d.length; j++) { 247 | /* If lock time has passed and address didn't take all the tokens already */ 248 | if ( 249 | (block.timestamp.sub(TGEDate) > d[j].unlockDay) && 250 | (d[j].currentAllocated > 0) 251 | ) { 252 | uint256 sendingAmount = d[j].currentAllocated; 253 | uint256 amountEach = sendingAmount / 254 | category.destinations.length; 255 | for (uint32 t = 0; t < category.destinations.length; t++) { 256 | require( 257 | erc20.transfer(category.destinations[t], amountEach) 258 | ); 259 | } 260 | d[j].currentAllocated = d[j].currentAllocated.sub( 261 | sendingAmount 262 | ); 263 | d[j].amountSent = d[j].amountSent.add(sendingAmount); 264 | } 265 | } 266 | } 267 | } 268 | 269 | function initializeDistributionCategory( 270 | uint256 _category, 271 | address[] memory _destinations 272 | ) internal onlyOwner whenNotPaused { 273 | distributionCategories[_category].destinations = _destinations; 274 | } 275 | 276 | function addDistributionToCategory( 277 | uint256 _category, 278 | uint256 _tokenAmount, 279 | uint256 _unlockDays 280 | ) internal onlyOwner whenNotPaused { 281 | /* Create DistributionStep Object */ 282 | DistributionStep memory step = DistributionStep( 283 | _tokenAmount * decimals, 284 | _tokenAmount * decimals, 285 | _unlockDays, 286 | 0 287 | ); 288 | /* Attach */ 289 | distributionCategories[_category].distributions.push(step); 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /contracts/limitedSale.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "@openzeppelin/contracts/utils/math/SafeMath.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 7 | import "@openzeppelin/contracts/access/Ownable.sol"; 8 | import "@openzeppelin/contracts/utils/Strings.sol"; 9 | 10 | abstract contract BaseLimitedSale is Ownable { 11 | using SafeMath for uint256; 12 | 13 | event ParticipantAdded(address participant); 14 | event ParticipantRemoved(address participant); 15 | event ReleaseFinished(uint8 release); 16 | 17 | ERC20 public crodoToken; 18 | ERC20 public usdtToken; 19 | // address public USDTAddress = address(0x66e428c3f67a68878562e79A0234c1F83c208770); 20 | 21 | struct Participant { 22 | uint256 minBuyAllowed; 23 | uint256 maxBuyAllowed; 24 | uint256 reserved; 25 | uint256 sent; 26 | } 27 | 28 | uint256 public totalMaxBuyAllowed; 29 | uint256 public totalMinBuyAllowed; 30 | uint256 public totalBought; 31 | uint256 public USDTPerToken; 32 | uint256 public saleDecimals; 33 | uint48 public initReleaseDate; 34 | uint256 public latestRelease; // Time of the latest release 35 | uint48 public releaseInterval = 30 days; 36 | uint8 public totalReleases = 10; 37 | uint8 public currentRelease; 38 | 39 | mapping(address => Participant) public participants; 40 | address[] participantAddrs; 41 | 42 | constructor( 43 | address _crodoToken, 44 | address _usdtAddress, 45 | uint256 _USDTPerToken, 46 | uint48 _initReleaseDate, 47 | uint8 _totalReleases 48 | ) Ownable() { 49 | crodoToken = ERC20(_crodoToken); 50 | saleDecimals = 10**crodoToken.decimals(); 51 | usdtToken = ERC20(_usdtAddress); 52 | USDTPerToken = _USDTPerToken; 53 | initReleaseDate = _initReleaseDate; 54 | totalReleases = _totalReleases; 55 | } 56 | 57 | function reservedBy(address participant) public view returns (uint256) { 58 | return participants[participant].reserved * saleDecimals; 59 | } 60 | 61 | function setReleaseInterval(uint48 _interval) external onlyOwner { 62 | releaseInterval = _interval; 63 | } 64 | 65 | function contractBalance() internal view returns (uint256) { 66 | return crodoToken.balanceOf(address(this)); 67 | } 68 | 69 | function getParticipant(address _participant) 70 | public 71 | view 72 | returns ( 73 | uint256, 74 | uint256, 75 | uint256, 76 | uint256 77 | ) 78 | { 79 | Participant memory participant = participants[_participant]; 80 | return ( 81 | participant.minBuyAllowed, 82 | participant.maxBuyAllowed, 83 | participant.reserved, 84 | participant.sent 85 | ); 86 | } 87 | 88 | function addParticipant( 89 | address _participant, 90 | uint256 minBuyAllowed, 91 | uint256 maxBuyAllowed 92 | ) public onlyOwner { 93 | Participant storage participant = participants[_participant]; 94 | participant.minBuyAllowed = minBuyAllowed; 95 | participant.maxBuyAllowed = maxBuyAllowed; 96 | totalMinBuyAllowed += minBuyAllowed; 97 | totalMaxBuyAllowed += maxBuyAllowed; 98 | 99 | participantAddrs.push(_participant); 100 | emit ParticipantAdded(_participant); 101 | } 102 | 103 | function addParticipants( 104 | address[] memory _participants, 105 | uint256[] memory minBuyAllowed, 106 | uint256[] memory maxBuyAllowed 107 | ) public onlyOwner { 108 | require( 109 | (_participants.length == minBuyAllowed.length) && (_participants.length == maxBuyAllowed.length), 110 | "Provided participant info arrays must all have the same length" 111 | ); 112 | for (uint i = 0; i < _participants.length; ++i) { 113 | addParticipant(_participants[i], minBuyAllowed[i], maxBuyAllowed[i]); 114 | } 115 | } 116 | 117 | function removeParticipant(address _participant) external onlyOwner { 118 | Participant memory participant = participants[_participant]; 119 | 120 | require( 121 | participant.reserved == 0, 122 | "Can't remove participant that has already locked some tokens" 123 | ); 124 | 125 | totalMaxBuyAllowed -= participant.maxBuyAllowed; 126 | totalMinBuyAllowed -= participant.minBuyAllowed; 127 | 128 | delete participants[_participant]; 129 | emit ParticipantRemoved(_participant); 130 | } 131 | 132 | function calculateUSDTPrice(uint256 amount) 133 | internal 134 | view 135 | returns (uint256) 136 | { 137 | return amount * USDTPerToken; 138 | } 139 | 140 | // Main function to purchase tokens during Private Sale. Buyer pays in fixed 141 | // rate of USDT for requested amount of CROD tokens. The USDT tokens must be 142 | // delegated for use to this contract beforehand by the user (call to ERC20.approve) 143 | // 144 | // @IMPORTANT: `amount` is expected to be in non-decimal form, 145 | // so 'boughtTokens = amount * (10 ^ crodoToken.decimals())' 146 | // 147 | // We need to cover some cases here: 148 | // 1) Our contract doesn't have requested amount of tokens left 149 | // 2) User tries to exceed their buy limit 150 | // 3) User tries to purchase tokens below their min limit 151 | function lockTokens(uint256 amount) external returns (uint256) { 152 | // Cover case 1 153 | require( 154 | (totalBought + amount * saleDecimals) <= contractBalance(), 155 | "Contract doesn't have requested amount of tokens left" 156 | ); 157 | 158 | Participant storage participant = participants[msg.sender]; 159 | 160 | // Cover case 2 161 | require( 162 | participant.reserved + amount <= participant.maxBuyAllowed, 163 | "User tried to exceed their buy-high limit" 164 | ); 165 | 166 | // Cover case 3 167 | require( 168 | participant.reserved + amount >= participant.minBuyAllowed, 169 | "User tried to purchase tokens below their minimum limit" 170 | ); 171 | 172 | uint256 usdtPrice = calculateUSDTPrice(amount); 173 | require( 174 | usdtToken.balanceOf(msg.sender) >= usdtPrice, 175 | "User doesn't have enough USDT to buy requested tokens" 176 | ); 177 | 178 | require( 179 | usdtToken.allowance(msg.sender, address(this)) >= usdtPrice, 180 | "User hasn't delegated required amount of tokens for the operation" 181 | ); 182 | 183 | usdtToken.transferFrom(msg.sender, address(this), usdtPrice); 184 | participant.reserved += amount; 185 | totalBought += amount * saleDecimals; 186 | return amount; 187 | } 188 | 189 | function releaseTokens() external returns (uint256) { 190 | require( 191 | initReleaseDate <= block.timestamp, 192 | "Initial release date hasn't passed yet" 193 | ); 194 | require( 195 | (initReleaseDate + currentRelease * releaseInterval) <= 196 | block.timestamp, 197 | string( 198 | abi.encodePacked( 199 | "Can only release tokens after initial release date has passed and once " 200 | "in the release interval. inital date: ", 201 | Strings.toString(initReleaseDate) 202 | ) 203 | ) 204 | ); 205 | 206 | ++currentRelease; 207 | uint256 tokensSent = 0; 208 | for (uint32 i = 0; i < participantAddrs.length; ++i) { 209 | address participantAddr = participantAddrs[i]; 210 | Participant storage participant = participants[participantAddr]; 211 | uint256 lockedTokensLeft = (participant.reserved * saleDecimals) - 212 | participant.sent; 213 | if ( 214 | (participant.reserved * saleDecimals) > 0 && 215 | (lockedTokensLeft > 0) 216 | ) { 217 | uint256 roundAmount = (participant.reserved * saleDecimals) / 218 | totalReleases; 219 | 220 | // If on the last release tokens don't round up after dividing, 221 | // or locked tokens is less than calcualted amount to send, 222 | // just send the whole remaining tokens 223 | if ( 224 | (currentRelease >= totalReleases && 225 | roundAmount != lockedTokensLeft) || 226 | (roundAmount > lockedTokensLeft) 227 | ) { 228 | roundAmount = lockedTokensLeft; 229 | } 230 | 231 | require( 232 | roundAmount <= contractBalance(), 233 | "Internal Error: Contract doens't have enough tokens to transfer to buyer" 234 | ); 235 | 236 | crodoToken.transfer(participantAddr, roundAmount); 237 | participant.sent += roundAmount; 238 | tokensSent += roundAmount; 239 | } 240 | } 241 | 242 | emit ReleaseFinished(currentRelease - 1); 243 | return tokensSent; 244 | } 245 | 246 | /* 247 | * Owner-only functions 248 | */ 249 | 250 | function pullUSDT(address receiver, uint256 amount) external onlyOwner { 251 | usdtToken.transfer(receiver, amount); 252 | } 253 | 254 | function lockForParticipant(address _participant, uint256 amount) 255 | external 256 | onlyOwner 257 | returns (uint256) 258 | { 259 | require( 260 | (totalBought + amount * saleDecimals) <= contractBalance(), 261 | "Contract doesn't have requested amount of tokens left" 262 | ); 263 | 264 | Participant storage participant = participants[_participant]; 265 | 266 | require( 267 | participant.reserved + amount < participant.maxBuyAllowed, 268 | "User tried to exceed their buy-high limit" 269 | ); 270 | 271 | require( 272 | participant.reserved + amount > participant.minBuyAllowed, 273 | "User tried to purchase tokens below their minimum limit" 274 | ); 275 | 276 | participant.reserved += amount; 277 | totalBought += amount * saleDecimals; 278 | return amount; 279 | } 280 | } 281 | 282 | contract CrodoSeedSale is BaseLimitedSale { 283 | constructor( 284 | address _crodoToken, 285 | address _usdtAddress, 286 | uint256 _USDTPerToken, 287 | uint48 _initReleaseDate, 288 | uint8 _totalReleases 289 | ) 290 | BaseLimitedSale( 291 | _crodoToken, 292 | _usdtAddress, 293 | _USDTPerToken, 294 | _initReleaseDate, 295 | _totalReleases 296 | ) 297 | {} 298 | } 299 | 300 | contract CrodoPrivateSale is BaseLimitedSale { 301 | constructor( 302 | address _crodoToken, 303 | address _usdtAddress, 304 | uint256 _USDTPerToken, 305 | uint48 _initReleaseDate, 306 | uint8 _totalReleases 307 | ) 308 | BaseLimitedSale( 309 | _crodoToken, 310 | _usdtAddress, 311 | _USDTPerToken, 312 | _initReleaseDate, 313 | _totalReleases 314 | ) 315 | {} 316 | } 317 | 318 | contract CrodoStrategicSale is BaseLimitedSale { 319 | constructor( 320 | address _crodoToken, 321 | address _usdtAddress, 322 | uint256 _USDTPerToken, 323 | uint48 _initReleaseDate, 324 | uint8 _totalReleases 325 | ) 326 | BaseLimitedSale( 327 | _crodoToken, 328 | _usdtAddress, 329 | _USDTPerToken, 330 | _initReleaseDate, 331 | _totalReleases 332 | ) 333 | {} 334 | } 335 | 336 | contract CrodoPublicSale is BaseLimitedSale { 337 | constructor( 338 | address _crodoToken, 339 | address _usdtAddress, 340 | uint256 _USDTPerToken, 341 | uint48 _initReleaseDate, 342 | uint8 _totalReleases 343 | ) 344 | BaseLimitedSale( 345 | _crodoToken, 346 | _usdtAddress, 347 | _USDTPerToken, 348 | _initReleaseDate, 349 | _totalReleases 350 | ) 351 | {} 352 | } 353 | -------------------------------------------------------------------------------- /contracts/rewardToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | 7 | contract RewardToken is ERC20 { 8 | constructor() ERC20("CRODO Lottery Reward Token", "CRODORT") { 9 | // actually we do not need/want an initial supply .. just for testing here 10 | uint256 initialSupply = 1000 * (uint256(10)**decimals()); // decimals = 18 by default 11 | _mint(msg.sender, initialSupply); // mint an initial supply 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /contracts/sampleSwap.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/utils/math/SafeMath.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | import "@openzeppelin/contracts/access/Ownable.sol"; 7 | import "@openzeppelin/contracts/security/Pausable.sol"; 8 | 9 | contract Whitelist is Ownable { 10 | mapping(address => bool) public whitelist; 11 | address[] public whitelistedAddresses; 12 | bool public hasWhitelisting = false; 13 | 14 | event AddedToWhitelist(address[] indexed accounts); 15 | event RemovedFromWhitelist(address indexed account); 16 | 17 | modifier onlyWhitelisted() { 18 | if (hasWhitelisting) { 19 | require(isWhitelisted(msg.sender)); 20 | } 21 | _; 22 | } 23 | 24 | constructor(bool _hasWhitelisting) { 25 | hasWhitelisting = _hasWhitelisting; 26 | } 27 | 28 | function add(address[] memory _addresses) public onlyOwner { 29 | for (uint256 i = 0; i < _addresses.length; i++) { 30 | require(whitelist[_addresses[i]] != true); 31 | whitelist[_addresses[i]] = true; 32 | whitelistedAddresses.push(_addresses[i]); 33 | } 34 | emit AddedToWhitelist(_addresses); 35 | } 36 | 37 | function remove(address _address, uint256 _index) public onlyOwner { 38 | require(_address == whitelistedAddresses[_index]); 39 | whitelist[_address] = false; 40 | delete whitelistedAddresses[_index]; 41 | emit RemovedFromWhitelist(_address); 42 | } 43 | 44 | function getWhitelistedAddresses() public view returns (address[] memory) { 45 | return whitelistedAddresses; 46 | } 47 | 48 | function isWhitelisted(address _address) public view returns (bool) { 49 | return whitelist[_address]; 50 | } 51 | } 52 | 53 | contract FixedSwap is Pausable, Whitelist { 54 | using SafeMath for uint256; 55 | uint256 increment = 0; 56 | 57 | mapping(uint256 => Purchase) public purchases; /* Purchasers mapping */ 58 | address[] public buyers; /* Current Buyers Addresses */ 59 | uint256[] public purchaseIds; /* All purchaseIds */ 60 | mapping(address => uint256[]) public myPurchases; /* Purchasers mapping */ 61 | 62 | ERC20 public bidToken; 63 | ERC20 public askToken; 64 | bool public isSaleFunded = false; 65 | uint256 public decimals; 66 | bool public unsoldTokensReedemed = false; 67 | uint256 public tradeValue; /* Price in askToken for single base unit of bidToken */ 68 | uint256 public startDate; /* Start Date */ 69 | uint256 public endDate; /* End Date */ 70 | uint256 public individualMinimumAmount; /* Minimum Amount Per Address */ 71 | uint256 public individualMaximumAmount; /* Maximum Amount Per Address */ 72 | uint256 public minimumRaise; /* Minimum Amount of Tokens that have to be sold */ 73 | uint256 public tokensAllocated; /* Tokens Available for Allocation - Dynamic */ 74 | uint256 public tokensForSale; /* Tokens Available for Sale */ 75 | bool public isTokenSwapAtomic; /* Make token release atomic or not */ 76 | address public FEE_ADDRESS; /* Default Address for Fee Percentage */ 77 | uint8 public feePercentage; /* Measured in single decimal points (i.e. 5 = 5%) */ 78 | 79 | struct Purchase { 80 | uint256 amount; 81 | address purchaser; 82 | uint256 ethAmount; 83 | uint256 timestamp; 84 | bool wasFinalized; /* Confirm the tokens were sent already */ 85 | bool reverted; /* Confirm the tokens were sent already */ 86 | } 87 | 88 | event PurchaseEvent( 89 | uint256 amount, 90 | address indexed purchaser, 91 | uint256 timestamp 92 | ); 93 | 94 | constructor( 95 | address _askTokenAddress, 96 | address _bidTokenAddress, 97 | uint256 _tradeValue, 98 | uint256 _tokensForSale, 99 | uint256 _startDate, 100 | uint256 _endDate, 101 | uint256 _individualMinimumAmount, 102 | uint256 _individualMaximumAmount, 103 | bool _isTokenSwapAtomic, 104 | uint256 _minimumRaise, 105 | address _feeAddress, 106 | bool _hasWhitelisting 107 | ) Whitelist(_hasWhitelisting) { 108 | /* Confirmations */ 109 | require( 110 | block.timestamp < _endDate, 111 | "End Date should be further than current date" 112 | ); 113 | require( 114 | block.timestamp < _startDate, 115 | "End Date should be further than current date" 116 | ); 117 | require(_startDate < _endDate, "End Date higher than Start Date"); 118 | require(_tokensForSale > 0, "Tokens for Sale should be > 0"); 119 | require( 120 | _tokensForSale > _individualMinimumAmount, 121 | "Tokens for Sale should be > Individual Minimum Amount" 122 | ); 123 | require( 124 | _individualMaximumAmount >= _individualMinimumAmount, 125 | "Individual Maximim AMount should be > Individual Minimum Amount" 126 | ); 127 | require( 128 | _minimumRaise <= _tokensForSale, 129 | "Minimum Raise should be < Tokens For Sale" 130 | ); 131 | 132 | askToken = ERC20(_askTokenAddress); 133 | bidToken = ERC20(_bidTokenAddress); 134 | tradeValue = _tradeValue; 135 | tokensForSale = _tokensForSale; 136 | startDate = _startDate; 137 | endDate = _endDate; 138 | individualMinimumAmount = _individualMinimumAmount; 139 | individualMaximumAmount = _individualMaximumAmount; 140 | isTokenSwapAtomic = _isTokenSwapAtomic; 141 | 142 | if (!_isTokenSwapAtomic) { 143 | /* If raise is not atomic swap */ 144 | minimumRaise = _minimumRaise; 145 | } 146 | 147 | FEE_ADDRESS = _feeAddress; 148 | decimals = bidToken.decimals(); 149 | } 150 | 151 | /** 152 | * Modifier to make a function callable only when the contract has Atomic Swaps not available. 153 | */ 154 | modifier isNotAtomicSwap() { 155 | require(!isTokenSwapAtomic, "Has to be non Atomic swap"); 156 | _; 157 | } 158 | 159 | /** 160 | * Modifier to make a function callable only when the contract has Atomic Swaps not available. 161 | */ 162 | modifier isSaleFinalized() { 163 | require(hasFinalized(), "Has to be finalized"); 164 | _; 165 | } 166 | 167 | /** 168 | * Modifier to make a function callable only when the swap time is open. 169 | */ 170 | modifier isSaleOpen() { 171 | require(isOpen(), "Has to be open"); 172 | _; 173 | } 174 | 175 | /** 176 | * Modifier to make a function callable only when the contract has Atomic Swaps not available. 177 | */ 178 | modifier isSalePreStarted() { 179 | require(isPreStart(), "Has to be pre-started"); 180 | _; 181 | } 182 | 183 | /** 184 | * Modifier to make a function callable only when the contract has Atomic Swaps not available. 185 | */ 186 | modifier isFunded() { 187 | require(isSaleFunded, "Has to be funded"); 188 | _; 189 | } 190 | 191 | /* Get Functions */ 192 | function isBuyer(uint256 purchase_id) public view returns (bool) { 193 | return (msg.sender == purchases[purchase_id].purchaser); 194 | } 195 | 196 | /* Get Functions */ 197 | function totalRaiseCost() public view returns (uint256) { 198 | return (cost(tokensForSale)); 199 | } 200 | 201 | function availableTokens() public view returns (uint256) { 202 | return bidToken.balanceOf(address(this)); 203 | } 204 | 205 | function tokensLeft() public view returns (uint256) { 206 | return tokensForSale - tokensAllocated; 207 | } 208 | 209 | function hasMinimumRaise() public view returns (bool) { 210 | return (minimumRaise != 0); 211 | } 212 | 213 | /* Verify if minimum raise was not achieved */ 214 | function minimumRaiseNotAchieved() public view returns (bool) { 215 | require( 216 | cost(tokensAllocated) < cost(minimumRaise), 217 | "TotalRaise is bigger than minimum raise amount" 218 | ); 219 | return true; 220 | } 221 | 222 | /* Verify if minimum raise was achieved */ 223 | function minimumRaiseAchieved() public view returns (bool) { 224 | if (hasMinimumRaise()) { 225 | require( 226 | cost(tokensAllocated) >= cost(minimumRaise), 227 | "TotalRaise is less than minimum raise amount" 228 | ); 229 | } 230 | return true; 231 | } 232 | 233 | function hasFinalized() public view returns (bool) { 234 | return block.timestamp > endDate; 235 | } 236 | 237 | function hasStarted() public view returns (bool) { 238 | return block.timestamp >= startDate; 239 | } 240 | 241 | function isPreStart() public view returns (bool) { 242 | return block.timestamp < startDate; 243 | } 244 | 245 | function isOpen() public view returns (bool) { 246 | return hasStarted() && !hasFinalized(); 247 | } 248 | 249 | function hasMinimumAmount() public view returns (bool) { 250 | return (individualMinimumAmount != 0); 251 | } 252 | 253 | function cost(uint256 _amount) public view returns (uint256) { 254 | return _amount.mul(tradeValue).div(10**decimals); 255 | } 256 | 257 | function boughtByAddress(address _buyer) public view returns (uint256) { 258 | uint256[] memory _purchases = getMyPurchases(_buyer); 259 | uint256 purchaserTotalAmountPurchased = 0; 260 | for (uint256 i = 0; i < _purchases.length; i++) { 261 | Purchase memory _purchase = purchases[_purchases[i]]; 262 | purchaserTotalAmountPurchased = purchaserTotalAmountPurchased.add( 263 | _purchase.amount 264 | ); 265 | } 266 | return purchaserTotalAmountPurchased; 267 | } 268 | 269 | function getPurchase(uint256 _purchase_id) 270 | external 271 | view 272 | returns ( 273 | uint256, 274 | address, 275 | uint256, 276 | uint256, 277 | bool, 278 | bool 279 | ) 280 | { 281 | Purchase memory purchase = purchases[_purchase_id]; 282 | return ( 283 | purchase.amount, 284 | purchase.purchaser, 285 | purchase.ethAmount, 286 | purchase.timestamp, 287 | purchase.wasFinalized, 288 | purchase.reverted 289 | ); 290 | } 291 | 292 | function getPurchaseIds() public view returns (uint256[] memory) { 293 | return purchaseIds; 294 | } 295 | 296 | function getBuyers() public view returns (address[] memory) { 297 | return buyers; 298 | } 299 | 300 | function getMyPurchases(address _address) 301 | public 302 | view 303 | returns (uint256[] memory) 304 | { 305 | return myPurchases[_address]; 306 | } 307 | 308 | function setFeePercentage(uint8 _feePercentage) public onlyOwner { 309 | require( 310 | feePercentage == 0, 311 | "Fee Percentage can not be modyfied once set" 312 | ); 313 | require(_feePercentage <= 99, "Fee Percentage has to be < 100"); 314 | feePercentage = _feePercentage; 315 | } 316 | 317 | /* Fund - Pre Sale Start */ 318 | function fund(uint256 _amount) public isSalePreStarted { 319 | /* Confirm transfered tokens is no more than needed */ 320 | require( 321 | availableTokens().add(_amount) <= tokensForSale, 322 | "Transfered tokens have to be equal or less than proposed" 323 | ); 324 | 325 | /* Transfer Funds */ 326 | require( 327 | bidToken.transferFrom(msg.sender, address(this), _amount), 328 | "Failed ERC20 token transfer" 329 | ); 330 | 331 | /* If Amount is equal to needed - sale is ready */ 332 | if (availableTokens() == tokensForSale) { 333 | isSaleFunded = true; 334 | } 335 | } 336 | 337 | /* Action Functions */ 338 | function swap(uint256 _amount) 339 | external 340 | payable 341 | whenNotPaused 342 | isFunded 343 | isSaleOpen 344 | onlyWhitelisted 345 | { 346 | /* Confirm Amount is positive */ 347 | require(_amount > 0, "Amount has to be positive"); 348 | 349 | /* Confirm Amount is less than tokens available */ 350 | require( 351 | _amount <= tokensLeft(), 352 | "Amount is less than tokens available" 353 | ); 354 | 355 | uint256 purchaseCost = cost(_amount); 356 | 357 | /* Confirm the user has funds for the transfer, confirm the value is equal */ 358 | require( 359 | askToken.balanceOf(msg.sender) >= purchaseCost, 360 | "User doesn't have enough askToken for purchase" 361 | ); 362 | 363 | /* Confirm Amount is bigger than minimum Amount */ 364 | require( 365 | _amount >= individualMinimumAmount, 366 | "Amount is bigger than minimum amount" 367 | ); 368 | 369 | /* Confirm Amount is smaller than maximum Amount */ 370 | require( 371 | _amount <= individualMaximumAmount, 372 | "Amount is smaller than maximum amount" 373 | ); 374 | 375 | /* Verify all user purchases, loop thru them */ 376 | uint256 purchaserTotalAmountPurchased = boughtByAddress(msg.sender); 377 | require( 378 | purchaserTotalAmountPurchased.add(_amount) <= 379 | individualMaximumAmount, 380 | "Address has already passed the max amount of swap" 381 | ); 382 | 383 | if (isTokenSwapAtomic) { 384 | /* Confirm transfer */ 385 | require( 386 | bidToken.transfer(msg.sender, _amount), 387 | "ERC20 transfer didn't work" 388 | ); 389 | } 390 | 391 | uint256 purchase_id = increment; 392 | increment = increment.add(1); 393 | 394 | askToken.transferFrom(msg.sender, address(this), purchaseCost); 395 | /* Create new purchase */ 396 | Purchase memory purchase = Purchase( 397 | _amount, 398 | msg.sender, 399 | purchaseCost, 400 | block.timestamp, 401 | isTokenSwapAtomic, /* If Atomic Swap */ 402 | false 403 | ); 404 | purchases[purchase_id] = purchase; 405 | purchaseIds.push(purchase_id); 406 | myPurchases[msg.sender].push(purchase_id); 407 | buyers.push(msg.sender); 408 | tokensAllocated = tokensAllocated.add(_amount); 409 | emit PurchaseEvent(_amount, msg.sender, block.timestamp); 410 | } 411 | 412 | /* Redeem tokens when the sale was finalized */ 413 | function redeemTokens(uint256 purchase_id) 414 | external 415 | isNotAtomicSwap 416 | isSaleFinalized 417 | whenNotPaused 418 | { 419 | /* Confirm it exists and was not finalized */ 420 | require( 421 | (purchases[purchase_id].amount != 0) && 422 | !purchases[purchase_id].wasFinalized, 423 | "Purchase is either 0 or finalized" 424 | ); 425 | require(isBuyer(purchase_id), "Address is not buyer"); 426 | purchases[purchase_id].wasFinalized = true; 427 | require( 428 | bidToken.transfer(msg.sender, purchases[purchase_id].amount), 429 | "ERC20 transfer failed" 430 | ); 431 | } 432 | 433 | /* Retrieve Minumum Amount */ 434 | function redeemGivenMinimumGoalNotAchieved(uint256 purchase_id) 435 | external 436 | isSaleFinalized 437 | isNotAtomicSwap 438 | { 439 | require(hasMinimumRaise(), "Minimum raise has to exist"); 440 | require(minimumRaiseNotAchieved(), "Minimum raise has to be reached"); 441 | /* Confirm it exists and was not finalized */ 442 | require( 443 | (purchases[purchase_id].amount != 0) && 444 | !purchases[purchase_id].wasFinalized, 445 | "Purchase is either 0 or finalized" 446 | ); 447 | require(isBuyer(purchase_id), "Address is not buyer"); 448 | purchases[purchase_id].wasFinalized = true; 449 | purchases[purchase_id].reverted = true; 450 | askToken.transfer(msg.sender, purchases[purchase_id].ethAmount); 451 | } 452 | 453 | /* Admin Functions */ 454 | function withdrawFunds(address tokensReceiver) 455 | external 456 | onlyOwner 457 | whenNotPaused 458 | isSaleFinalized 459 | { 460 | require(minimumRaiseAchieved(), "Minimum raise has to be reached"); 461 | uint256 contractBalance = askToken.balanceOf(address(this)); 462 | uint256 feeAmount = contractBalance.mul(feePercentage).div(100); 463 | askToken.transfer(FEE_ADDRESS, feeAmount); 464 | askToken.transfer(tokensReceiver, contractBalance - feeAmount); 465 | } 466 | 467 | function withdrawUnsoldTokens() external onlyOwner isSaleFinalized { 468 | require(!unsoldTokensReedemed); 469 | uint256 unsoldTokens; 470 | if (hasMinimumRaise() && (cost(tokensAllocated) < cost(minimumRaise))) { 471 | /* Minimum Raise not reached */ 472 | unsoldTokens = tokensForSale; 473 | } else { 474 | /* If minimum Raise Achieved Redeem All Tokens minus the ones */ 475 | unsoldTokens = tokensForSale.sub(tokensAllocated); 476 | } 477 | 478 | if (unsoldTokens > 0) { 479 | unsoldTokensReedemed = true; 480 | require( 481 | bidToken.transfer(msg.sender, unsoldTokens), 482 | "ERC20 transfer failed" 483 | ); 484 | } 485 | } 486 | 487 | function removeOtherERC20Tokens(address _tokenAddress, address _to) 488 | external 489 | onlyOwner 490 | isSaleFinalized 491 | { 492 | require( 493 | _tokenAddress != address(bidToken), 494 | "Token Address has to be diff than the bidToken subject to sale" 495 | ); // Confirm tokens addresses are different from main sale one 496 | ERC20 erc20Token = ERC20(_tokenAddress); 497 | require( 498 | erc20Token.transfer(_to, erc20Token.balanceOf(address(this))), 499 | "ERC20 Token transfer failed" 500 | ); 501 | } 502 | 503 | /* Safe Pull function */ 504 | function safePull() external onlyOwner whenPaused { 505 | address payable seller = payable(msg.sender); 506 | seller.transfer(address(this).balance); 507 | bidToken.transfer(msg.sender, bidToken.balanceOf(address(this))); 508 | askToken.transfer(msg.sender, askToken.balanceOf(address(this))); 509 | } 510 | } 511 | -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | const Migrations = artifacts.require("Migrations") 2 | 3 | module.exports = async function (deployer) { 4 | await deployer.deploy(Migrations) 5 | } 6 | -------------------------------------------------------------------------------- /migrations/2_crodo_token.js: -------------------------------------------------------------------------------- 1 | const CrodoDistributionContract = artifacts.require("CrodoDistributionContract") 2 | const CrodoToken = artifacts.require("CrodoToken") 3 | const { getUnixDate } = require("./utils.js") 4 | 5 | const TGEDate = getUnixDate(2022, 1, 30) // TODO: Set correct TGEDate 6 | 7 | // TODO: Set correct addresses 8 | const seedWallet = ["0x72245A3E23E7F73e5eaD2857b990b74a27FB95d4"] 9 | const privSaleWallet = ["0xC1A14B3CC70d3a1FD4f8e45FeA6B0c755f5a3D4A"] 10 | const strategicSaleWallet = ["0x567C09825dd2678fc8BE92F7504823A09C638555"] 11 | const pubSaleWallet = ["0xC6F8fa17836fEebBD836f7F5986942e8d102B683"] 12 | const advisorsWallet = ["0x4B49f5c28469F1445D5d591078bde5B976a2a28B"] 13 | const liquidityWallet = ["0xd2656F956Ee90Bb6A564C35AA886405075D97E0E"] 14 | const strategicWallet = ["0x0c1b057E1726A26D5A47FaBA8770263bF54bE4a1"] 15 | const communityWallet = ["0x3Be5244F6c0769384B8AB3bC1EE8667C19CF4D68"] 16 | 17 | module.exports = async function (deployer) { 18 | await deployer.deploy( 19 | CrodoDistributionContract, 20 | seedWallet, 21 | privSaleWallet, 22 | strategicSaleWallet, 23 | pubSaleWallet, 24 | advisorsWallet, 25 | liquidityWallet, 26 | strategicWallet, 27 | communityWallet 28 | ) 29 | const dist = await CrodoDistributionContract.deployed() 30 | await deployer.deploy(CrodoToken, dist.address) 31 | const token = await CrodoToken.deployed() 32 | await dist.setTokenAddress(token.address) 33 | await dist.setTGEDate(TGEDate) 34 | 35 | await dist.setSeedRound() 36 | await dist.setPrivateRound() 37 | await dist.setStrategicSaleRound() 38 | await dist.setPublicRound() 39 | await dist.setTeamRound() 40 | await dist.setAdvisorsRound() 41 | await dist.setLiquidityRound() 42 | await dist.setStrategicRound() 43 | await dist.setCommunityRound() 44 | } 45 | -------------------------------------------------------------------------------- /migrations/3_crodo_stake.js: -------------------------------------------------------------------------------- 1 | const CrodoToken = artifacts.require("CrodoToken") 2 | const CRDStake = artifacts.require("CRDStake") 3 | 4 | const minute = 60 5 | const hour = minute * 60 6 | const day = hour * 24 7 | const month = day * 30 8 | const year = month * 12 9 | 10 | const lockTimePeriodMin = month * 6 11 | const lockTimePeriodMax = year * 4 12 | 13 | module.exports = async function (deployer) { 14 | const token = await CrodoToken.deployed() 15 | await deployer.deploy(CRDStake, token.address, lockTimePeriodMin, lockTimePeriodMax) 16 | } 17 | -------------------------------------------------------------------------------- /migrations/4_crodo_sales.js: -------------------------------------------------------------------------------- 1 | // Before any of the contract sales can be used for locking tokens, we need to fund by sending 2 | // regular ERC20 `transfer` transaction on their address. 3 | 4 | const CrodoToken = artifacts.require("CrodoToken") 5 | const CrodoSeedSale = artifacts.require("CrodoSeedSale") 6 | const CrodoPrivateSale = artifacts.require("CrodoPrivateSale") 7 | const CrodoStrategicSale = artifacts.require("CrodoStrategicSale") 8 | const CrodoPublicSale = artifacts.require("CrodoPublicSale") 9 | const { getUnixDate, amountToLamports } = require("./utils.js") 10 | 11 | // TODO: Set correct dates & USDT address 12 | const USDTAddress = "0x66e428c3f67a68878562e79A0234c1F83c208770" 13 | const USDTDecimals = 6 14 | const seedFirstRelease = getUnixDate(2022, 4, 1) 15 | const privateFirstRelease = getUnixDate(2022, 4, 1) 16 | const strategicFirstRelease = getUnixDate(2022, 4, 1) 17 | const publicFirstRelease = getUnixDate(2022, 4, 1) 18 | const seedReleases = 27 19 | const privateReleases = 26 20 | const strategicReleases = 24 21 | const publicReleases = 4 22 | const seedPrice = amountToLamports(0.10, USDTDecimals) 23 | const privatePrice = amountToLamports(0.14, USDTDecimals) 24 | const strategicPrice = amountToLamports(0.16, USDTDecimals) 25 | const publicPrice = amountToLamports(0.18, USDTDecimals) 26 | 27 | module.exports = async function (deployer) { 28 | const token = await CrodoToken.deployed() 29 | 30 | await deployer.deploy( 31 | CrodoSeedSale, token.address, USDTAddress, seedPrice, 32 | seedFirstRelease, seedReleases 33 | ) 34 | 35 | await deployer.deploy( 36 | CrodoPrivateSale, token.address, USDTAddress, privatePrice, 37 | privateFirstRelease, privateReleases 38 | ) 39 | 40 | await deployer.deploy( 41 | CrodoStrategicSale, token.address, USDTAddress, strategicPrice, 42 | strategicFirstRelease, strategicReleases 43 | ) 44 | 45 | await deployer.deploy( 46 | CrodoPublicSale, token.address, USDTAddress, publicPrice, 47 | publicFirstRelease, publicReleases 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /migrations/5_crodo_rewards.js: -------------------------------------------------------------------------------- 1 | const RewardToken = artifacts.require("RewardToken") 2 | 3 | module.exports = function (deployer) { 4 | deployer.deploy(RewardToken) 5 | } 6 | -------------------------------------------------------------------------------- /migrations/utils.js: -------------------------------------------------------------------------------- 1 | const BigNumber = require("bignumber.js") 2 | // Arguments are expected to be date identifiers in the following order: 3 | // year, month, day, hour, minute, second 4 | function getUnixDate (...args) { 5 | return Math.floor(new Date(...args).getTime() / 1000) 6 | } 7 | 8 | function amountToLamports (amount, decimals) { 9 | return new BigNumber(amount).multipliedBy(10 ** decimals).integerValue() 10 | } 11 | 12 | module.exports = { getUnixDate, amountToLamports } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Contracts", 3 | "version": "1.0.0", 4 | "description": "Smart contracts for Crodo-io project", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "truffle compile", 8 | "deploy": "truffle deploy --reset", 9 | "deploy:testnet": "truffle deploy --network testnet --reset", 10 | "deploy:local_testnet": "truffle deploy --network local_testnet --reset", 11 | "deploy:mainnet": "truffle deploy --network mainnet --reset", 12 | "coverage": "truffle run coverage", 13 | "eslint": "eslint --fix test/ migrations/", 14 | "lint": "npm run eslint && npm run prettier", 15 | "solhint": "npx solhint -f stylish ./contracts/**/*.sol", 16 | "prettier": "npx prettier --write ./contracts/**/*.sol", 17 | "test": "truffle test", 18 | "truffle": "truffle" 19 | }, 20 | "dependencies": { 21 | "@openzeppelin/contracts": "^4.4.2", 22 | "@truffle/hdwallet-provider": "^2.0.0", 23 | "bignumber.js": "^9.0.2", 24 | "ganache-cli": "^6.12.2", 25 | "ganache-time-traveler": "^1.0.16", 26 | "truffle": "^5.4.29" 27 | }, 28 | "keywords": [], 29 | "author": "", 30 | "license": "ISC", 31 | "devDependencies": { 32 | "config": "^3.3.7", 33 | "eslint": "^7.32.0", 34 | "eslint-config-standard": "^16.0.3", 35 | "eslint-plugin-import": "^2.25.4", 36 | "eslint-plugin-node": "^11.1.0", 37 | "eslint-plugin-promise": "^5.2.0", 38 | "prettier": "^2.5.1", 39 | "prettier-plugin-solidity": "^1.0.0-beta.19", 40 | "solhint": "^3.3.6", 41 | "solidity-coverage": "^0.7.20" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/crodo_token.js: -------------------------------------------------------------------------------- 1 | const DistributionContract = artifacts.require("CrodoDistributionContract") 2 | const CrodoToken = artifacts.require("CrodoToken") 3 | const BigNumber = require("bignumber.js") 4 | const timeMachine = require("ganache-time-traveler") 5 | 6 | function amountToLamports (amount, decimals) { 7 | return new BigNumber(amount).multipliedBy(10 ** decimals).integerValue() 8 | } 9 | 10 | function getTimestamp () { 11 | return Math.floor(Date.now() / 1000) 12 | } 13 | 14 | contract("CrodoToken", (accounts) => { 15 | let token 16 | let dist 17 | 18 | const seedWallet = accounts[0] 19 | const privSaleWallet = accounts[1] 20 | const strategicSaleWallet = accounts[2] 21 | const pubSaleWallet = accounts[3] 22 | const advisorsWallet = accounts[5] 23 | const liquidityWallet = accounts[6] 24 | const strategicWallet = accounts[7] 25 | const communityWallet = accounts[8] 26 | 27 | const teamWallets = [ 28 | "0xcF528152C7619E23d0c6A16de75E6B30A45Bf502", 29 | "0x72245A3E23E7F73e5eaD2857b990b74a27FB95d4", 30 | "0xC1A14B3CC70d3a1FD4f8e45FeA6B0c755f5a3D4A", 31 | "0xC6F8fa17836fEebBD836f7F5986942e8d102B683" 32 | ] 33 | 34 | const day = 60 * 60 * 24 35 | const month = day * 30 36 | const TGEDate = getTimestamp() + month 37 | let crodoDecimals 38 | 39 | const releaseWallets = [ 40 | seedWallet, privSaleWallet, strategicSaleWallet, pubSaleWallet, teamWallets, 41 | advisorsWallet, liquidityWallet, strategicWallet, communityWallet 42 | ] 43 | /* 44 | * Precomputed releases table, each row is balance on nth month after TGEDate, 45 | * each category is rounded to 100% (some categories aren't precise, 46 | * e.g. Strategic reserve: 2.85% * 35 = 99.75%). 47 | * 48 | * Columns: 49 | * Seed, Private sale, Strategic, Public sale, Team, Advisors, Liquidity, Strategic Reserve, Community / Ecosystem 50 | */ 51 | const releasesTable = [ 52 | [6000000, 8000000, 8000000, 4000000, 0, 0, 12000000, 0, 1050000], 53 | [6000000, 8000000, 8000000, 4000000, 0, 0, 12000000, 0, 1673700], 54 | [6000000, 8000000, 8000000, 4000000, 0, 0, 12000000, 0, 2297400], 55 | [6000000, 8000000, 8000000, 4000000, 0, 0, 12000000, 0, 2921100], 56 | [6000000, 8000000, 8000000, 4000000, 0, 0, 12000000, 0, 3544800], 57 | [6000000, 8000000, 8000000, 4000000, 0, 0, 12000000, 0, 4168500], 58 | [6000000, 8000000, 8000000, 4000000, 0, 0, 12000000, 570000, 4792200], 59 | [6000000, 8000000, 8000000, 4000000, 0, 0, 12000000, 1140000, 5415900], 60 | [6000000, 8000000, 8000000, 4000000, 0, 0, 12000000, 1710000, 6039600], 61 | [6000000, 8000000, 8000000, 4000000, 0, 0, 12000000, 2280000, 6663300], 62 | [6000000, 8000000, 8000000, 4000000, 0, 0, 12000000, 2850000, 7287000], 63 | [6000000, 8000000, 8000000, 4000000, 0, 0, 12000000, 3420000, 7910700], 64 | [6000000, 8000000, 8000000, 4000000, 0, 0, 12000000, 3990000, 8534400], 65 | [6000000, 8000000, 8000000, 4000000, 0, 0, 12000000, 4560000, 9158100], 66 | [6000000, 8000000, 8000000, 4000000, 0, 0, 12000000, 5130000, 9781800], 67 | [6000000, 8000000, 8000000, 4000000, 0, 0, 12000000, 5700000, 10405500], 68 | [6000000, 8000000, 8000000, 4000000, 0, 0, 12000000, 6270000, 11029200], 69 | [6000000, 8000000, 8000000, 4000000, 600000 / teamWallets.length, 240000, 12000000, 6840000, 11652900], 70 | [6000000, 8000000, 8000000, 4000000, 1200000 / teamWallets.length, 480000, 12000000, 7410000, 12276600], 71 | [6000000, 8000000, 8000000, 4000000, 1800000 / teamWallets.length, 720000, 12000000, 7980000, 12900300], 72 | [6000000, 8000000, 8000000, 4000000, 2400000 / teamWallets.length, 960000, 12000000, 8550000, 13524000], 73 | [6000000, 8000000, 8000000, 4000000, 3000000 / teamWallets.length, 1200000, 12000000, 9120000, 14147700], 74 | [6000000, 8000000, 8000000, 4000000, 3600000 / teamWallets.length, 1440000, 12000000, 9690000, 14771400], 75 | [6000000, 8000000, 8000000, 4000000, 4200000 / teamWallets.length, 1680000, 12000000, 10260000, 15395100], 76 | [6000000, 8000000, 8000000, 4000000, 4800000 / teamWallets.length, 1920000, 12000000, 10830000, 16018800], 77 | [6000000, 8000000, 8000000, 4000000, 5400000 / teamWallets.length, 2160000, 12000000, 11400000, 16642500], 78 | [6000000, 8000000, 8000000, 4000000, 6000000 / teamWallets.length, 2400000, 12000000, 11970000, 17266200], 79 | [6000000, 8000000, 8000000, 4000000, 6600000 / teamWallets.length, 2640000, 12000000, 12540000, 17889900], 80 | [6000000, 8000000, 8000000, 4000000, 7200000 / teamWallets.length, 2880000, 12000000, 13110000, 18513600], 81 | [6000000, 8000000, 8000000, 4000000, 7800000 / teamWallets.length, 3120000, 12000000, 13680000, 19137300], 82 | [6000000, 8000000, 8000000, 4000000, 8400000 / teamWallets.length, 3360000, 12000000, 14250000, 19761000], 83 | [6000000, 8000000, 8000000, 4000000, 9000000 / teamWallets.length, 3600000, 12000000, 14820000, 20384700], 84 | [6000000, 8000000, 8000000, 4000000, 9600000 / teamWallets.length, 3840000, 12000000, 15390000, 21000000], 85 | [6000000, 8000000, 8000000, 4000000, 10200000 / teamWallets.length, 4080000, 12000000, 15960000, 21000000], 86 | [6000000, 8000000, 8000000, 4000000, 10800000 / teamWallets.length, 4320000, 12000000, 16530000, 21000000], 87 | [6000000, 8000000, 8000000, 4000000, 11400000 / teamWallets.length, 4560000, 12000000, 17100000, 21000000], 88 | [6000000, 8000000, 8000000, 4000000, 12000000 / teamWallets.length, 4800000, 12000000, 17670000, 21000000], 89 | [6000000, 8000000, 8000000, 4000000, 12600000 / teamWallets.length, 5040000, 12000000, 18240000, 21000000], 90 | [6000000, 8000000, 8000000, 4000000, 13200000 / teamWallets.length, 5280000, 12000000, 18810000, 21000000], 91 | [6000000, 8000000, 8000000, 4000000, 13800000 / teamWallets.length, 5520000, 12000000, 19380000, 21000000], 92 | [6000000, 8000000, 8000000, 4000000, 14400000 / teamWallets.length, 5760000, 12000000, 20000000, 21000000], 93 | [6000000, 8000000, 8000000, 4000000, 15000000 / teamWallets.length, 6000000, 12000000, 20000000, 21000000], 94 | [6000000, 8000000, 8000000, 4000000, 15000000 / teamWallets.length, 6000000, 12000000, 20000000, 21000000] 95 | ] 96 | 97 | // Not dependent on actual tests, just make sure test setup is correct 98 | assert.equal(releaseWallets.length, releasesTable[0].length) 99 | 100 | beforeEach(async () => { 101 | const snapshot = await timeMachine.takeSnapshot() 102 | snapshotId = snapshot.result 103 | 104 | dist = await DistributionContract.new( 105 | [seedWallet], 106 | [privSaleWallet], 107 | [strategicSaleWallet], 108 | [pubSaleWallet], 109 | [advisorsWallet], 110 | [liquidityWallet], 111 | [strategicWallet], 112 | [communityWallet] 113 | ) 114 | // await dist.initAllRounds() 115 | token = await CrodoToken.new(dist.address) 116 | await dist.setTokenAddress(token.address) 117 | await dist.setSeedRound() 118 | await dist.setPrivateRound() 119 | await dist.setStrategicSaleRound() 120 | await dist.setPublicRound() 121 | await dist.setTeamRound() 122 | await dist.setAdvisorsRound() 123 | await dist.setLiquidityRound() 124 | await dist.setStrategicRound() 125 | await dist.setCommunityRound() 126 | crodoDecimals = await token.decimals() 127 | await dist.setTGEDate(TGEDate) 128 | }) 129 | 130 | afterEach(async () => { 131 | await timeMachine.revertToSnapshot(snapshotId) 132 | }) 133 | 134 | it("test basic attributes of the Crodo token", async () => { 135 | const tokenCap = new BigNumber(100000000).multipliedBy(1e+18) 136 | assert.equal(await token.name(), "CrodoToken") 137 | assert.equal(await token.symbol(), "CROD") 138 | assert.equal(await token.decimals(), 18) 139 | assert.equal( 140 | new BigNumber(await token.cap()).toString(), 141 | tokenCap.toString() 142 | ) 143 | }) 144 | 145 | it("test the whole distribution", async () => { 146 | await timeMachine.advanceBlockAndSetTime(TGEDate + day) 147 | for (let i = 0; i < releasesTable.length; ++i) { 148 | await dist.triggerTokenSend() 149 | for (let j = 0; j < releaseWallets.length; ++j) { 150 | const wallet = releaseWallets[j] 151 | const targetBalance = releasesTable[i][j] 152 | 153 | if (typeof wallet === "object") { 154 | for (let t = 0; t < wallet.length; ++t) { 155 | assert.equal( 156 | Number(await token.balanceOf(wallet[t])), 157 | amountToLamports(targetBalance, crodoDecimals) 158 | ) 159 | } 160 | } else { 161 | assert.equal( 162 | Number(await token.balanceOf(wallet)), 163 | amountToLamports(targetBalance, crodoDecimals) 164 | ) 165 | } 166 | } 167 | await timeMachine.advanceTimeAndBlock(month) 168 | } 169 | }) 170 | }) 171 | -------------------------------------------------------------------------------- /test/private_sale.js: -------------------------------------------------------------------------------- 1 | const CrodoPrivateSale = artifacts.require("CrodoPrivateSale") 2 | const TestToken = artifacts.require("TestToken") 3 | const BigNumber = require("bignumber.js") 4 | const timeMachine = require("ganache-time-traveler") 5 | 6 | function amountToLamports (amount, decimals) { 7 | return new BigNumber(amount).multipliedBy(10 ** decimals).integerValue() 8 | } 9 | 10 | function getTimestamp () { 11 | return Math.floor(Date.now() / 1000) 12 | } 13 | 14 | function cmpRanged (n, n1, range) { 15 | return Math.abs(n - n1) <= range 16 | } 17 | 18 | contract("PrivateSale", (accounts) => { 19 | let crodoToken 20 | let usdtToken 21 | let privateSale 22 | let usdtPrice 23 | let tokensForSale 24 | const owner = accounts[0] 25 | const user1 = accounts[1] 26 | const crodoDecimals = 18 27 | const usdtDecimals = 6 28 | 29 | const day = 60 * 60 * 24 30 | const month = day * 30 31 | 32 | const releaseInterval = 2 * month 33 | const initRelease = getTimestamp() + 3 * month 34 | const totalReleases = 23 35 | 36 | beforeEach(async () => { 37 | const snapshot = await timeMachine.takeSnapshot() 38 | snapshotId = snapshot.result 39 | 40 | crodoToken = await TestToken.new(crodoDecimals, owner, 0) 41 | usdtToken = await TestToken.new(usdtDecimals, owner, 0) 42 | usdtPrice = amountToLamports(0.15, usdtDecimals) 43 | tokensForSale = 100000 44 | 45 | privateSale = await CrodoPrivateSale.new( 46 | crodoToken.address, 47 | usdtToken.address, 48 | usdtPrice, 49 | initRelease, 50 | totalReleases 51 | ) 52 | await privateSale.setReleaseInterval(releaseInterval) 53 | await crodoToken.mint(privateSale.address, amountToLamports(tokensForSale, crodoDecimals)) 54 | }) 55 | 56 | afterEach(async () => { 57 | await timeMachine.revertToSnapshot(snapshotId) 58 | }) 59 | 60 | it("user exceeded their buy limit", async () => { 61 | const usdtPrice = amountToLamports(0.15 * 50, usdtDecimals) 62 | await usdtToken.mint(owner, usdtPrice) 63 | await privateSale.addParticipant(owner, 1, 49) 64 | await usdtToken.approve(privateSale.address, usdtPrice) 65 | 66 | await privateSale.lockTokens(50).then(res => { 67 | assert.fail("This shouldn't happen") 68 | }).catch(desc => { 69 | assert.equal(desc.reason, "User tried to exceed their buy-high limit") 70 | // assert.equal(desc.code, -32000) 71 | // assert.equal(desc.message, "rpc error: code = InvalidArgument desc = execution reverted: User tried to exceed their buy-high limit: invalid request") 72 | }) 73 | }) 74 | 75 | it("user doesn't have enough USDT", async () => { 76 | const usdtPrice = amountToLamports(0.15 * 10, usdtDecimals) 77 | // await usdtToken.mint(owner, usdtPrice) 78 | await privateSale.addParticipant(owner, 1, 100) 79 | await usdtToken.approve(privateSale.address, usdtPrice) 80 | 81 | await privateSale.lockTokens(10).then(res => { 82 | assert.fail("This shouldn't happen") 83 | }).catch(desc => { 84 | assert.equal(desc.reason, "User doesn't have enough USDT to buy requested tokens") 85 | // assert.equal(desc.code, -32000) 86 | // assert.equal(desc.message, "rpc error: code = InvalidArgument desc = execution reverted: User doesn't have enough USDT to buy requested tokens: invalid request") 87 | }) 88 | }) 89 | 90 | it("reserve total of 46 tokens spread by 2 users", async () => { 91 | const lockingAmount = 23 92 | const usdtPrice = amountToLamports(0.15 * lockingAmount, usdtDecimals) 93 | await usdtToken.mint(owner, usdtPrice) 94 | await privateSale.addParticipant(owner, 1, 100) 95 | await usdtToken.approve(privateSale.address, usdtPrice) 96 | 97 | await usdtToken.mint(user1, usdtPrice) 98 | await privateSale.addParticipant(user1, 1, 100) 99 | await usdtToken.approve(privateSale.address, usdtPrice, { from: user1 }) 100 | 101 | let userUSDTBefore = Number(await usdtToken.balanceOf(owner)) 102 | await privateSale.lockTokens(lockingAmount) 103 | assert.equal( 104 | Number(amountToLamports(lockingAmount, crodoDecimals)), 105 | Number(await privateSale.reservedBy(owner)) 106 | ) 107 | assert.equal( 108 | userUSDTBefore - usdtPrice, 109 | Number(await usdtToken.balanceOf(owner)) 110 | ) 111 | 112 | userUSDTBefore = Number(await usdtToken.balanceOf(user1)) 113 | await privateSale.lockTokens(lockingAmount, { from: user1 }) 114 | assert.equal( 115 | Number(amountToLamports(lockingAmount, crodoDecimals)), 116 | Number(await privateSale.reservedBy(user1)) 117 | ) 118 | assert.equal( 119 | userUSDTBefore - usdtPrice, 120 | Number(await usdtToken.balanceOf(user1)) 121 | ) 122 | }) 123 | 124 | it("Perform full cycle of test-private-sale among 2 users", async () => { 125 | const lockingAmount = { } 126 | lockingAmount[owner] = tokensForSale * 0.75 127 | lockingAmount[user1] = tokensForSale * 0.25 128 | 129 | let usdtPrice = amountToLamports(0.15 * lockingAmount[owner], usdtDecimals) 130 | await usdtToken.mint(owner, usdtPrice) 131 | await privateSale.addParticipant(owner, 1, lockingAmount[owner] + 1) 132 | await usdtToken.approve(privateSale.address, usdtPrice) 133 | 134 | usdtPrice = amountToLamports(0.15 * lockingAmount[user1], usdtDecimals) 135 | await usdtToken.mint(user1, usdtPrice) 136 | await privateSale.addParticipant(user1, 1, lockingAmount[user1] + 1) 137 | await usdtToken.approve(privateSale.address, usdtPrice, { from: user1 }) 138 | 139 | let firstLock = lockingAmount[owner] - 100 140 | await privateSale.lockTokens(firstLock) 141 | await privateSale.lockTokens(lockingAmount[owner] - firstLock) 142 | firstLock = lockingAmount[user1] - 100 143 | await privateSale.lockTokens(firstLock, { from: user1 }) 144 | await privateSale.lockTokens(lockingAmount[user1] - firstLock, { from: user1 }) 145 | 146 | await timeMachine.advanceBlockAndSetTime(initRelease + day) 147 | 148 | for (let i = 1; i < totalReleases; ++i) { 149 | await privateSale.releaseTokens() 150 | 151 | Object.keys(lockingAmount).forEach(async addr => { 152 | const target = Number(amountToLamports(lockingAmount[addr], crodoDecimals)) * (i / totalReleases) 153 | const balance = Number(await crodoToken.balanceOf(addr)) 154 | // Due to division on types >8 bytes, either in contract or in javascript, 155 | // small inpercisions are allowed, the only important thing, is that after the last 156 | // release numbers must be exact. 157 | if (!cmpRanged(target, balance, target * 0.001)) { 158 | assert.equal( 159 | target, 160 | balance 161 | ) 162 | } 163 | }) 164 | await timeMachine.advanceTimeAndBlock(releaseInterval) 165 | } 166 | 167 | await privateSale.releaseTokens() 168 | 169 | let target = Number(amountToLamports(lockingAmount[owner], crodoDecimals)) 170 | let balance = Number(await crodoToken.balanceOf(owner)) 171 | assert.equal( 172 | target, 173 | balance 174 | ) 175 | 176 | target = Number(amountToLamports(lockingAmount[user1], crodoDecimals)) 177 | balance = Number(await crodoToken.balanceOf(user1)) 178 | assert.equal( 179 | target, 180 | balance 181 | ) 182 | 183 | // Take USDT from contract 184 | const balanceBefore = Number(await usdtToken.balanceOf(owner)) 185 | const contractUSDT = Number(await usdtToken.balanceOf(privateSale.address)) 186 | await privateSale.pullUSDT(owner, contractUSDT) 187 | const balanceAfter = Number(await usdtToken.balanceOf(owner)) 188 | 189 | assert.equal( 190 | balanceAfter, 191 | balanceBefore + contractUSDT 192 | ) 193 | assert.equal( 194 | Number(await usdtToken.balanceOf(privateSale.address)), 195 | 0 196 | ) 197 | }) 198 | 199 | it("Lock tokens after sale has started", async () => { 200 | const lockingAmount = tokensForSale * 0.63 201 | 202 | const usdtPrice = amountToLamports(0.15 * lockingAmount, usdtDecimals) 203 | await usdtToken.mint(owner, usdtPrice) 204 | await privateSale.addParticipant(owner, 1, lockingAmount + 1) 205 | await usdtToken.approve(privateSale.address, usdtPrice) 206 | 207 | await timeMachine.advanceBlockAndSetTime(initRelease + day) 208 | 209 | // Skip 2 releases 210 | await privateSale.releaseTokens() 211 | await timeMachine.advanceTimeAndBlock(releaseInterval) 212 | await privateSale.releaseTokens() 213 | 214 | const firstLock = lockingAmount - 100 215 | await privateSale.lockTokens(firstLock) 216 | await privateSale.lockTokens(lockingAmount - firstLock) 217 | 218 | for (let i = 1; i < totalReleases - 2; ++i) { 219 | await timeMachine.advanceTimeAndBlock(releaseInterval) 220 | await privateSale.releaseTokens() 221 | 222 | const target = Number(amountToLamports(lockingAmount, crodoDecimals)) * (i / totalReleases) 223 | const balance = Number(await crodoToken.balanceOf(owner)) 224 | // Due to division on types >8 bytes, either in contract or in javascript, 225 | // small inpercisions are allowed, the only important thing, is that after the last 226 | // release numbers must be exact. 227 | if (!cmpRanged(target, balance, target * 0.001)) { 228 | assert.equal( 229 | target, 230 | balance 231 | ) 232 | } 233 | } 234 | 235 | await timeMachine.advanceTimeAndBlock(releaseInterval) 236 | await privateSale.releaseTokens() 237 | 238 | const target = Number(amountToLamports(lockingAmount, crodoDecimals)) 239 | const balance = Number(await crodoToken.balanceOf(owner)) 240 | assert.equal( 241 | target, 242 | balance 243 | ) 244 | 245 | // Take USDT from contract 246 | const balanceBefore = Number(await usdtToken.balanceOf(owner)) 247 | const contractUSDT = Number(await usdtToken.balanceOf(privateSale.address)) 248 | await privateSale.pullUSDT(owner, contractUSDT) 249 | const balanceAfter = Number(await usdtToken.balanceOf(owner)) 250 | 251 | assert.equal( 252 | balanceAfter, 253 | balanceBefore + contractUSDT 254 | ) 255 | assert.equal( 256 | Number(await usdtToken.balanceOf(privateSale.address)), 257 | 0 258 | ) 259 | }) 260 | 261 | // TODO: REWRITE THIS TEST, IT WASN'T FINISHED, JUST COPIED 262 | it("Stake before and after sale has started", async () => { 263 | const lockingAmount = tokensForSale * 0.63 264 | 265 | const usdtPrice = amountToLamports(0.15 * lockingAmount, usdtDecimals) 266 | await usdtToken.mint(owner, usdtPrice) 267 | await privateSale.addParticipant(owner, 1, lockingAmount + 1) 268 | await usdtToken.approve(privateSale.address, usdtPrice) 269 | 270 | await timeMachine.advanceBlockAndSetTime(initRelease + day) 271 | 272 | let currLocked = lockingAmount / 3 273 | await privateSale.lockTokens(currLocked) 274 | 275 | // Wait 2 releases 276 | await privateSale.releaseTokens() 277 | await timeMachine.advanceTimeAndBlock(releaseInterval) 278 | await privateSale.releaseTokens() 279 | 280 | let target = amountToLamports(currLocked * (2 / totalReleases), crodoDecimals) 281 | let balance = Number(await crodoToken.balanceOf(owner)) 282 | if (!cmpRanged(target, balance, target * 0.001)) { 283 | assert.equal( 284 | target, 285 | balance 286 | ) 287 | } 288 | 289 | // Lock another batch of tokens and wait another 2 releases 290 | await privateSale.lockTokens(currLocked) 291 | await timeMachine.advanceTimeAndBlock(releaseInterval) 292 | await privateSale.releaseTokens() 293 | await timeMachine.advanceTimeAndBlock(releaseInterval) 294 | await privateSale.releaseTokens() 295 | 296 | currLocked *= 2 297 | target = balance + Number(amountToLamports(currLocked * (2 / totalReleases), crodoDecimals)) 298 | balance = Number(await crodoToken.balanceOf(owner)) 299 | if (!cmpRanged(target, balance, target * 0.001)) { 300 | assert.equal( 301 | target, 302 | balance 303 | ) 304 | } 305 | 306 | privateSale.lockTokens(lockingAmount - currLocked) 307 | currLocked = lockingAmount 308 | const alreadyReleased = balance 309 | 310 | for (let i = 1; i < totalReleases - 4; ++i) { 311 | await timeMachine.advanceTimeAndBlock(releaseInterval) 312 | await privateSale.releaseTokens() 313 | 314 | target = alreadyReleased + 315 | Number(amountToLamports(lockingAmount, crodoDecimals)) * (i / totalReleases) 316 | balance = Number(await crodoToken.balanceOf(owner)) 317 | // Due to division on types >8 bytes, either in contract or in javascript, 318 | // small inpercisions are allowed, the only important thing, is that after the last 319 | // release numbers must be exact. 320 | if (!cmpRanged(target, balance, target * 0.001)) { 321 | assert.equal( 322 | target, 323 | balance 324 | ) 325 | } 326 | } 327 | 328 | await timeMachine.advanceTimeAndBlock(releaseInterval) 329 | await privateSale.releaseTokens() 330 | 331 | target = Number(amountToLamports(lockingAmount, crodoDecimals)) 332 | balance = Number(await crodoToken.balanceOf(owner)) 333 | assert.equal( 334 | target, 335 | balance 336 | ) 337 | 338 | // Take USDT from contract 339 | const balanceBefore = Number(await usdtToken.balanceOf(owner)) 340 | const contractUSDT = Number(await usdtToken.balanceOf(privateSale.address)) 341 | await privateSale.pullUSDT(owner, contractUSDT) 342 | const balanceAfter = Number(await usdtToken.balanceOf(owner)) 343 | 344 | assert.equal( 345 | balanceAfter, 346 | balanceBefore + contractUSDT 347 | ) 348 | assert.equal( 349 | Number(await usdtToken.balanceOf(privateSale.address)), 350 | 0 351 | ) 352 | }) 353 | 354 | it("test admin functions", async () => { 355 | const userReserve = 30 356 | await privateSale.addParticipant(user1, 1, 49) 357 | const firstReserve = userReserve - 15 358 | await privateSale.lockForParticipant(user1, firstReserve) 359 | await privateSale.lockForParticipant(user1, userReserve - firstReserve) 360 | 361 | assert.equal( 362 | Number(await privateSale.reservedBy(user1)), 363 | amountToLamports(userReserve, crodoDecimals) 364 | ) 365 | }) 366 | }) 367 | -------------------------------------------------------------------------------- /test/stake.js: -------------------------------------------------------------------------------- 1 | const TestToken = artifacts.require("TestToken") 2 | const CRDStake = artifacts.require("CRDStake") 3 | const BigNumber = require("bignumber.js") 4 | const timeMachine = require("ganache-time-traveler") 5 | 6 | function amountToLamports (amount, decimals) { 7 | return new BigNumber(amount).multipliedBy(10 ** decimals).integerValue() 8 | } 9 | 10 | function cmpRanged (n, n1, range) { 11 | return Math.abs(n - n1) <= range 12 | } 13 | 14 | contract("CrodoToken", (accounts) => { 15 | let stakeToken 16 | let stake 17 | const owner = accounts[0] 18 | const user1 = accounts[1] 19 | 20 | const day = 60 * 60 * 24 21 | const month = day * 30 22 | const year = month * 12 23 | 24 | const lockTimePeriodMin = month 25 | const lockTimePeriodMax = 2 * year 26 | const rewardFactor = 1000 * day // Stake 1000 tokens to get reward of 1 token a day 27 | const stakeDecimals = 12 28 | 29 | beforeEach(async () => { 30 | const snapshot = await timeMachine.takeSnapshot() 31 | snapshotId = snapshot.result 32 | 33 | stakeToken = await TestToken.new(stakeDecimals, owner, 0) 34 | stake = await CRDStake.new(stakeToken.address, lockTimePeriodMin, lockTimePeriodMax) 35 | await stake.setRewardToken(stakeToken.address) 36 | await stake.setStakeRewardFactor(rewardFactor) 37 | await stakeToken.mint(stake.address, amountToLamports(100000, stakeDecimals)) 38 | }) 39 | 40 | afterEach(async () => { 41 | await timeMachine.revertToSnapshot(snapshotId) 42 | }) 43 | 44 | it("stake 20 tokens between 2 users for 6 months & withdraw them", async () => { 45 | const stakeAmount = amountToLamports(15, stakeDecimals) 46 | const userStake = amountToLamports(5, stakeDecimals) 47 | const lockTime = 6 * month 48 | 49 | await stakeToken.mint(owner, stakeAmount) 50 | await stakeToken.approve(stake.address, stakeAmount) 51 | await stake.stake(stakeAmount, lockTime) 52 | 53 | await stakeToken.mint(user1, userStake) 54 | await stakeToken.approve(stake.address, userStake, { from: user1 }) 55 | await stake.stake(userStake, lockTime, { from: user1 }) 56 | 57 | assert.equal( 58 | lockTime, 59 | Number(await stake.getLockTime(owner)) 60 | ) 61 | assert.equal( 62 | stakeAmount, 63 | Number(await stake.stakeAmount(owner)) 64 | ) 65 | 66 | const firstAwait = lockTime / 2 67 | await timeMachine.advanceTimeAndBlock(firstAwait) 68 | let target = stakeAmount * firstAwait / rewardFactor 69 | let current = Number(await stake.getEarnedRewardTokens(owner)) 70 | if (!cmpRanged(target, current, target * 0.001)) { 71 | assert.equal( 72 | target, 73 | current 74 | ) 75 | } 76 | 77 | await timeMachine.advanceTimeAndBlock(lockTime - firstAwait) 78 | target = current + stakeAmount * (lockTime - firstAwait) / rewardFactor 79 | current = Number(await stake.getEarnedRewardTokens(owner)) 80 | if (!cmpRanged(target, current, target * 0.001)) { 81 | assert.equal( 82 | target, 83 | current 84 | ) 85 | } 86 | 87 | target = current + Number(await stakeToken.balanceOf(owner)) 88 | await stake.claim() 89 | current = Number(await stakeToken.balanceOf(owner)) 90 | if (!cmpRanged(target, current, target * 0.001)) { 91 | assert.equal( 92 | target, 93 | current 94 | ) 95 | } 96 | 97 | current = Number(await stake.getEarnedRewardTokens(user1)) 98 | target = current + Number(await stakeToken.balanceOf(user1)) 99 | await stake.claim({ from: user1 }) 100 | current = Number(await stakeToken.balanceOf(user1)) 101 | if (!cmpRanged(target, current, target * 0.001)) { 102 | assert.equal( 103 | target, 104 | current 105 | ) 106 | } 107 | }) 108 | 109 | it("stake 10 tokens for 6 months and restake half way in", async () => { 110 | const stakeAmount = amountToLamports(10, stakeDecimals) 111 | const lockTime = 6 * month 112 | 113 | await stakeToken.mint(owner, stakeAmount) 114 | await stakeToken.approve(stake.address, stakeAmount) 115 | await stake.stake(stakeAmount, lockTime) 116 | 117 | const firstAwait = lockTime / 2 118 | await timeMachine.advanceTimeAndBlock(firstAwait) 119 | 120 | const earnedBeforeRestake = Number(await stake.getEarnedRewardTokens(owner)) 121 | const newStakeAmount = Number(stakeAmount) + earnedBeforeRestake 122 | await stake.restakeRewards() 123 | await timeMachine.advanceTimeAndBlock(lockTime - firstAwait) 124 | 125 | target = newStakeAmount * (lockTime - firstAwait) / rewardFactor 126 | current = Number(await stake.getEarnedRewardTokens(owner)) 127 | if (!cmpRanged(target, current, target * 0.001)) { 128 | assert.equal( 129 | target, 130 | current 131 | ) 132 | } 133 | 134 | target = current + Number(await stakeToken.balanceOf(owner)) 135 | await stake.claim() 136 | current = Number(await stakeToken.balanceOf(owner)) 137 | if (!cmpRanged(target, current, target * 0.001)) { 138 | assert.equal( 139 | target, 140 | current 141 | ) 142 | } 143 | }) 144 | 145 | it("user tried to exceed the max stake time limit", async () => { 146 | const stakeAmount = amountToLamports(10, stakeDecimals) 147 | const lockTime = lockTimePeriodMax + month 148 | 149 | await stakeToken.mint(owner, stakeAmount) 150 | await stakeToken.approve(stake.address, stakeAmount) 151 | await stake.stake(stakeAmount, lockTime).then(res => { 152 | assert.fail("This shouldn't happen") 153 | }).catch(desc => { 154 | assert.equal(desc.reason, "lockTime must by < lockTimePeriodMax") 155 | // assert.equal(desc.code, -32000) 156 | // assert.equal(desc.message, "rpc error: code = InvalidArgument desc = execution reverted: : invalid request") 157 | }) 158 | }) 159 | }) 160 | -------------------------------------------------------------------------------- /test/swap.js: -------------------------------------------------------------------------------- 1 | const FixedSwap = artifacts.require("FixedSwap") 2 | const TestToken = artifacts.require("TestToken") 3 | const BigNumber = require("bignumber.js") 4 | const timeMachine = require("ganache-time-traveler") 5 | 6 | function amountToLamports (amount, decimals) { 7 | return new BigNumber(amount).multipliedBy(10 ** decimals).integerValue() 8 | } 9 | 10 | function getTimestamp () { 11 | return Math.floor(Date.now() / 1000) 12 | } 13 | 14 | contract("FixedSwap", (accounts) => { 15 | let fixedSwap 16 | let askToken 17 | let bidToken 18 | const owner = accounts[0] 19 | const feeAddress = accounts[1] 20 | 21 | const day = 60 * 60 * 24 22 | const month = day * 30 23 | const askDecimals = 12 24 | const bidDecimals = 12 25 | const feePercentage = 1 26 | const tradeValue = amountToLamports(0.15, askDecimals) 27 | const tokensForSale = amountToLamports(100000, bidDecimals) 28 | const minAmount = amountToLamports(1, bidDecimals) 29 | const maxAmount = amountToLamports(100, bidDecimals) 30 | 31 | beforeEach(async () => { 32 | const snapshot = await timeMachine.takeSnapshot() 33 | snapshotId = snapshot.result 34 | 35 | askToken = await TestToken.new(askDecimals, owner, 0) 36 | bidToken = await TestToken.new(bidDecimals, owner, 0) 37 | fixedSwap = await FixedSwap.new( 38 | askToken.address, 39 | bidToken.address, 40 | tradeValue, 41 | tokensForSale, 42 | getTimestamp() + day, 43 | getTimestamp() + 6 * month, 44 | minAmount, 45 | maxAmount, 46 | false, 47 | amountToLamports(10000, bidDecimals), 48 | feeAddress, 49 | false 50 | ) 51 | await fixedSwap.setFeePercentage(feePercentage) 52 | }) 53 | 54 | afterEach(async () => { 55 | await timeMachine.revertToSnapshot(snapshotId) 56 | }) 57 | 58 | it("basic swap", async () => { 59 | const swapAmount = amountToLamports(25, bidDecimals) 60 | const swapCost = swapAmount * tradeValue / (10 ** bidDecimals) 61 | await askToken.mint(owner, swapCost) 62 | await askToken.approve(fixedSwap.address, swapCost) 63 | 64 | await bidToken.mint(owner, tokensForSale) 65 | await bidToken.approve(fixedSwap.address, tokensForSale) 66 | await fixedSwap.fund(tokensForSale) 67 | 68 | assert.equal( 69 | Number(await bidToken.balanceOf(fixedSwap.address)), 70 | tokensForSale 71 | ) 72 | assert.ok(await fixedSwap.isPreStart()) 73 | 74 | // Skip 2 days to wait for the start date of the swap pool 75 | await timeMachine.advanceTimeAndBlock(day * 2) 76 | 77 | assert.ok(await fixedSwap.hasStarted()) 78 | assert.ok(await fixedSwap.isOpen()) 79 | 80 | const askBalanceBefore = await askToken.balanceOf(fixedSwap.address) 81 | await fixedSwap.swap(swapAmount) 82 | assert.equal( 83 | Number(await fixedSwap.boughtByAddress(owner)), 84 | swapAmount 85 | ) 86 | assert.equal( 87 | Number(askBalanceBefore) + swapCost, 88 | Number(await askToken.balanceOf(fixedSwap.address)) 89 | ) 90 | 91 | // Wait for sale to end 92 | await timeMachine.advanceTimeAndBlock(month * 6) 93 | assert.ok(await fixedSwap.hasFinalized()) 94 | await fixedSwap.redeemTokens(0) 95 | 96 | assert.equal( 97 | Number(await bidToken.balanceOf(owner)), 98 | Number(swapAmount) 99 | ) 100 | }) 101 | }) 102 | -------------------------------------------------------------------------------- /truffle-config.js: -------------------------------------------------------------------------------- 1 | const HDWalletProvider = require('@truffle/hdwallet-provider') 2 | const NonceTrackerSubprovider = require('web3-provider-engine/subproviders/nonce-tracker') 3 | const config = require('config') 4 | const TestRPC = require('ganache-cli') 5 | 6 | module.exports = { 7 | networks: { 8 | development: { 9 | provider: TestRPC.provider( { gasLimit: 0xff6691b7 }), 10 | network_id: '*' 11 | }, 12 | testnet: { 13 | provider: function () { 14 | let w = new HDWalletProvider(config.get('testnet.truffle.privateKey'), config.get('testnet.blockchain.rpc')) 15 | let nonceTracker = new NonceTrackerSubprovider() 16 | w.engine._providers.unshift(nonceTracker) 17 | nonceTracker.setEngine(w.engine) 18 | return w 19 | }, 20 | network_id: config.get('testnet.blockchain.networkId'), 21 | port: 8545, 22 | gas: 20000000, 23 | gasPrice: 10000000000000 24 | }, 25 | local_testnet: { 26 | provider: function () { 27 | let w = new HDWalletProvider(config.get('testnet-local.truffle.privateKey'), config.get('testnet-local.blockchain.rpc')) 28 | let nonceTracker = new NonceTrackerSubprovider() 29 | w.engine._providers.unshift(nonceTracker) 30 | nonceTracker.setEngine(w.engine) 31 | return w 32 | }, 33 | network_id: config.get('testnet-local.blockchain.networkId'), 34 | port: 8545, 35 | gas: 20000000, 36 | gasPrice: 10000000000000, 37 | timeoutBlocks: 10000 38 | }, 39 | mainnet: { 40 | provider: function () { 41 | let w = new HDWalletProvider(config.get('mainnet.truffle.privateKey'), config.get('mainnet.blockchain.rpc')) 42 | let nonceTracker = new NonceTrackerSubprovider() 43 | w.engine._providers.unshift(nonceTracker) 44 | nonceTracker.setEngine(w.engine) 45 | return w 46 | }, 47 | network_id: config.get('mainnet.blockchain.networkId'), 48 | port: 8545, 49 | gas: 20000000, 50 | gasPrice: 60000000000 51 | }, 52 | }, 53 | compilers: { 54 | solc: { 55 | version: '^0.8.0', 56 | settings: { 57 | optimizer: { 58 | enabled: true 59 | } 60 | } 61 | } 62 | }, 63 | plugins: [ "solidity-coverage" ] 64 | } 65 | --------------------------------------------------------------------------------