generated from defi-wonderland/solidity-foundry-boilerplate
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
113 additions
and
75 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,45 +1,55 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity =0.8.19; | ||
pragma solidity 0.8.20; | ||
|
||
import {IERC20} from 'isolmate/interfaces/tokens/IERC20.sol'; | ||
import {IUnlock} from 'interfaces/IUnlock.sol'; | ||
import {Owned} from 'solmate/auth/Owned.sol'; | ||
import {Ownable2Step, Ownable} from '@openzeppelin/contracts/access/Ownable2Step.sol'; | ||
|
||
contract Unlock is Owned, IUnlock { | ||
uint256 public constant TOTAL_SUPPLY = 24_960_000 ether; | ||
contract Unlock is Ownable2Step, IUnlock { | ||
uint256 public totalAmount; | ||
|
||
uint256 public startTime; | ||
mapping(address _token => uint256 _amount) public withdrawedSupply; | ||
mapping(address _token => uint256 _amount) public withdrawnSupply; | ||
|
||
constructor(uint256 _startTime, address _owner) Owned(_owner) { | ||
constructor(uint256 _startTime, address _owner, uint256 _totalAmount) Ownable(_owner) { | ||
startTime = _startTime; | ||
totalAmount = _totalAmount; | ||
} | ||
|
||
function _unlockedSupply(address _token, uint256 _timestamp) internal view returns (uint256 _unlockedSupplyReturn) { | ||
if (_timestamp < startTime + 365 days) { | ||
_unlockedSupplyReturn = 0; | ||
} else { | ||
_unlockedSupplyReturn = | ||
TOTAL_SUPPLY / 13 + (TOTAL_SUPPLY * 12 / 13) * (_timestamp - startTime - 365 days) / 365 days; | ||
_unlockedSupplyReturn -= withdrawedSupply[_token]; | ||
} | ||
function _unlockedSupply(uint256 _timestamp) internal view returns (uint256 _unlockedSupplyReturn) { | ||
uint256 _firstMilestoneTime = startTime + 365 days; // 1st milestone is 1 year after start time | ||
|
||
if (_timestamp < _firstMilestoneTime) return _unlockedSupplyReturn; // return 0 if not reached | ||
|
||
uint256 _firstMilestoneUnlockedAmount = totalAmount / 13; // 1st milestone unlock amount | ||
uint256 _restAmount = totalAmount - _firstMilestoneUnlockedAmount; // rest amount after 1st milestone | ||
uint256 _timePassed = _timestamp - startTime - 365 days; // time passed after 1st milestone | ||
uint256 _totalTime = 365 days; // total unlock time after 1st milestone | ||
|
||
// f(x) = ax + b | ||
// b = totalAmount / 13 | ||
// a = restAmount / totalTime | ||
// x = timePassed | ||
_unlockedSupplyReturn = _firstMilestoneUnlockedAmount + (_restAmount * _timePassed) / _totalTime; | ||
} | ||
|
||
function unlockedSupply(address _token) external view returns (uint256 _unlockedSupplyReturn) { | ||
_unlockedSupplyReturn = _unlockedSupply(_token, block.timestamp); | ||
function unlockedSupply() external view returns (uint256 _unlockedSupplyReturn) { | ||
_unlockedSupplyReturn = _unlockedSupply(block.timestamp); | ||
} | ||
|
||
function unlockedAtTimestamp( | ||
address _token, | ||
uint256 _timestamp | ||
) external view returns (uint256 _unlockedSupplyReturn) { | ||
_unlockedSupplyReturn = _unlockedSupply(_token, _timestamp); | ||
function unlockedAtTimestamp(uint256 _timestamp) external view returns (uint256 _unlockedSupplyReturn) { | ||
_unlockedSupplyReturn = _unlockedSupply(_timestamp); | ||
} | ||
|
||
function withdraw(address _receiver, address _token, uint256 _amount) external { | ||
if (_amount > _unlockedSupply(_token, block.timestamp)) revert InsufficientUnlockedSupply(); | ||
if (msg.sender != owner) revert Unauthorized(); | ||
withdrawedSupply[_token] += _amount; | ||
function withdraw(address _receiver, address _token) external { | ||
if (msg.sender != owner()) revert Unauthorized(); | ||
|
||
uint256 _amount = _unlockedSupply(block.timestamp) - withdrawnSupply[_token]; | ||
uint256 _balance = IERC20(_token).balanceOf(address(this)); | ||
|
||
if (_amount > _balance) _amount = _balance; | ||
|
||
withdrawnSupply[_token] += _amount; | ||
IERC20(_token).transfer(_receiver, _amount); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,15 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity =0.8.19; | ||
pragma solidity =0.8.20; | ||
|
||
interface IUnlock { | ||
error InsufficientUnlockedSupply(); | ||
error Unauthorized(); | ||
|
||
function unlockedSupply(address _token) external view returns (uint256 _unlockedSupply); | ||
function unlockedAtTimestamp(address _token, uint256 _timestamp) external view returns (uint256 _unlockedSupply); | ||
function withdraw(address _receiver, address _token, uint256 _amount) external; | ||
function unlockedSupply() external view returns (uint256 _unlockedSupply); | ||
function unlockedAtTimestamp(uint256 _timestamp) external view returns (uint256 _unlockedSupply); | ||
function withdraw(address _receiver, address _token) external; | ||
|
||
function startTime() external view returns (uint256 _startTime); | ||
function withdrawedSupply(address _token) external view returns (uint256 _withdrawedSupply); | ||
function TOTAL_SUPPLY() external view returns (uint256 _totalSupply); | ||
function withdrawnSupply(address _token) external view returns (uint256 _withdrawedSupply); | ||
function totalAmount() external view returns (uint256 _totalAmount); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,71 +1,89 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity =0.8.19; | ||
pragma solidity =0.8.20; | ||
|
||
import {IntegrationBase} from 'test/integration/IntegrationBase.sol'; | ||
import {IOwned} from 'test/utils/IOwned.sol'; | ||
import {IOwnable2Steps} from 'test/utils/IOwnable2Steps.sol'; | ||
import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; | ||
import {IUnlock} from 'interfaces/IUnlock.sol'; | ||
import {IERC20} from 'isolmate/interfaces/tokens/IERC20.sol'; | ||
|
||
contract IntegrationUnlock is IntegrationBase { | ||
function test_Constructor() public { | ||
assertEq(IOwned(address(_unlock)).owner(), _owner); | ||
assertEq(IOwnable2Steps(address(_unlock)).owner(), _owner); | ||
assertEq(_unlock.startTime(), block.timestamp + 10 minutes); | ||
} | ||
|
||
function test_UnlockedAtTimestamp() public { | ||
assertEq(_unlock.unlockedAtTimestamp(_nextToken, _startTime), 0); | ||
assertEq(_unlock.unlockedAtTimestamp(_nextToken, _startTime + 364 days), 0 ether); | ||
assertEq(_unlock.unlockedAtTimestamp(_nextToken, _startTime + 365 days), 1_920_000 ether); | ||
assertEq(_unlock.unlockedAtTimestamp(_startTime), 0); | ||
assertEq(_unlock.unlockedAtTimestamp(_startTime + 364 days), 0 ether); | ||
assertEq(_unlock.unlockedAtTimestamp(_startTime + 365 days), 1_920_000 ether); | ||
|
||
assertEq(_unlock.unlockedAtTimestamp(_nextToken, _startTime + 365 days + 10 days) - 2_551_232 ether < 1 ether, true); | ||
assertEq( | ||
_unlock.unlockedAtTimestamp(_nextToken, _startTime + 365 days + 100 days) - 8_232_328 ether < 1 ether, true | ||
); | ||
assertEq(_unlock.unlockedAtTimestamp(_startTime + 365 days + 10 days) - 2_551_232 ether < 1 ether, true); | ||
assertEq(_unlock.unlockedAtTimestamp(_startTime + 365 days + 100 days) - 8_232_328 ether < 1 ether, true); | ||
|
||
assertEq(_unlock.unlockedAtTimestamp(_nextToken, _startTime + 365 days + 365 days), 24_960_000 ether); | ||
assertEq(_unlock.unlockedAtTimestamp(_startTime + 365 days + 365 days), 24_960_000 ether); | ||
} | ||
|
||
function test_UnlockedAmount() public { | ||
assertEq(_unlock.unlockedSupply(_nextToken), 0); | ||
assertEq(_unlock.unlockedSupply(), 0); | ||
vm.warp(_startTime + 364 days); | ||
assertEq(_unlock.unlockedSupply(_nextToken), 0); | ||
assertEq(_unlock.unlockedSupply(), 0); | ||
vm.warp(_startTime + 365 days); | ||
assertEq(_unlock.unlockedSupply(_nextToken), 1_920_000 ether); | ||
assertEq(_unlock.unlockedSupply(), 1_920_000 ether); | ||
|
||
vm.warp(_startTime + 365 days + 10 days); | ||
assertEq(_unlock.unlockedSupply(_nextToken) - 2_551_232 ether < 1 ether, true); | ||
assertEq(_unlock.unlockedSupply() - 2_551_232 ether < 1 ether, true); | ||
vm.warp(_startTime + 365 days + 100 days); | ||
assertEq(_unlock.unlockedSupply(_nextToken) - 8_232_328 ether < 1 ether, true); | ||
assertEq(_unlock.unlockedSupply() - 8_232_328 ether < 1 ether, true); | ||
|
||
vm.warp(_startTime + 365 days + 365 days); | ||
assertEq(_unlock.unlockedSupply(_nextToken), 24_960_000 ether); | ||
assertEq(_unlock.unlockedSupply(), 24_960_000 ether); | ||
} | ||
|
||
function test_WithdrawNoSupply() public { | ||
vm.warp(_startTime + 364 days); | ||
vm.prank(_owner); | ||
vm.expectRevert(abi.encodeWithSelector(IUnlock.InsufficientUnlockedSupply.selector)); | ||
_unlock.withdraw(_alice, _nextToken, 1); | ||
_unlock.withdraw(_alice, _nextToken); | ||
assertEq(_unlock.withdrawnSupply(_nextToken), 0); | ||
assertEq(IERC20(_nextToken).balanceOf(_alice), 0); | ||
} | ||
|
||
function test_WithdrawUnauthorized() public { | ||
vm.warp(_startTime + 365 days); | ||
vm.expectRevert(abi.encodeWithSelector(IUnlock.Unauthorized.selector)); | ||
_unlock.withdraw(_alice, _nextToken, 1); | ||
_unlock.withdraw(_alice, _nextToken); | ||
} | ||
|
||
function test_WithdrawLegit() public { | ||
deal(_nextToken, address(_unlock), 1_920_000 ether); | ||
deal(_nextToken, address(_unlock), 2_000_000 ether); // deal more than withrawable | ||
vm.warp(_startTime + 365 days); | ||
vm.startPrank(_owner); | ||
|
||
_unlock.withdraw(_alice, _nextToken, 1_920_000 ether); | ||
assertEq(_unlock.withdrawedSupply(_nextToken), 1_920_000 ether); | ||
_unlock.withdraw(_alice, _nextToken); | ||
assertEq(_unlock.withdrawnSupply(_nextToken), 1_920_000 ether); | ||
assertEq(IERC20(_nextToken).balanceOf(_alice), 1_920_000 ether); | ||
|
||
vm.expectRevert(abi.encodeWithSelector(IUnlock.InsufficientUnlockedSupply.selector)); | ||
_unlock.withdraw(_alice, _nextToken, 1); | ||
// try again and expect no changes | ||
_unlock.withdraw(_alice, _nextToken); | ||
assertEq(_unlock.withdrawnSupply(_nextToken), 1_920_000 ether); | ||
assertEq(IERC20(_nextToken).balanceOf(_alice), 1_920_000 ether); | ||
|
||
vm.stopPrank(); | ||
} | ||
|
||
function test_transferOwnership() public { | ||
vm.prank(_owner); | ||
IOwnable2Steps(address(_unlock)).transferOwnership(_alice); | ||
assertEq(IOwnable2Steps(address(_unlock)).pendingOwner(), _alice); | ||
assertEq(IOwnable2Steps(address(_unlock)).owner(), _owner); | ||
|
||
address _bob = makeAddr('bob'); | ||
vm.prank(_bob); | ||
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, _bob)); | ||
IOwnable2Steps(address(_unlock)).acceptOwnership(); | ||
|
||
vm.prank(_alice); | ||
IOwnable2Steps(address(_unlock)).acceptOwnership(); | ||
assertEq(IOwnable2Steps(address(_unlock)).owner(), _alice); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity =0.8.20; | ||
|
||
interface IOwnable2Steps { | ||
function transferOwnership(address _newOwner) external; | ||
function acceptOwnership() external; | ||
|
||
function pendingOwner() external view returns (address _pendingOwner); | ||
function owner() external view returns (address _owner); | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters