Skip to content

Commit 12cc9b2

Browse files
author
valentinpollart
committed
feat: prepare sale v4 deployment
1 parent 4801b6c commit 12cc9b2

22 files changed

+1840
-157
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ are considered as a security standard.
1414
| ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- |
1515
| [DeepSquare](./contracts/DeepSquare.sol) (ERC20) | [0xf192caE2e7Cd4048Bea307368015E3647c49338e](https://snowtrace.io/token/0xf192caE2e7Cd4048Bea307368015E3647c49338e) |
1616
| [Sale](./contracts/Sale.sol) | [0xb4A981d2663455aEE53193Da8e7c61c3579301cb](https://snowtrace.io/address/0xb4A981d2663455aEE53193Da8e7c61c3579301cb#code) |
17-
| [Eligibility](./contracts/Eligibility.sol) | [0x52088e60AfB56E83cA0B6340B49F709e57973869](https://snowtrace.io/address/0x52088e60AfB56E83cA0B6340B49F709e57973869#code) |
17+
| [Eligibility](contracts/legacy/v1.2/Eligibility.sol) | [0x52088e60AfB56E83cA0B6340B49F709e57973869](https://snowtrace.io/address/0x52088e60AfB56E83cA0B6340B49F709e57973869#code) |
1818

1919
## Maintainers
2020

contracts/Sale.sol

-20
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ pragma solidity ^0.8.0;
44
import "@openzeppelin/contracts/access/Ownable.sol";
55
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
66
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
7-
import "./Eligibility.sol";
87

98
/**
109
* @title Token sale.
@@ -18,9 +17,6 @@ contract Sale is Ownable {
1817
/// @notice The stablecoin ERC20 contract.
1918
IERC20Metadata public immutable STC;
2019

21-
// @notice The eligibility contract.
22-
IEligibility public immutable eligibility;
23-
2420
/// @notice The Chainlink AVAX/USD pair aggregator.
2521
AggregatorV3Interface public aggregator;
2622

@@ -45,29 +41,25 @@ contract Sale is Ownable {
4541
/**
4642
* @param _DPS The DPS contract address.
4743
* @param _STC The ERC20 stablecoin contract address (e.g, USDT, USDC, etc.).
48-
* @param _eligibility The eligibility contract.
4944
* @param _aggregator The Chainlink AVAX/USD pair aggregator contract address.
5045
* @param _rate The DPS/STC rate in STC cents.
5146
* @param _initialSold How many DPS tokens were already sold.
5247
*/
5348
constructor(
5449
IERC20Metadata _DPS,
5550
IERC20Metadata _STC,
56-
Eligibility _eligibility,
5751
AggregatorV3Interface _aggregator,
5852
uint8 _rate,
5953
uint256 _minimumPurchaseSTC,
6054
uint256 _initialSold
6155
) {
6256
require(address(_DPS) != address(0), "Sale: token is zero");
6357
require(address(_STC) != address(0), "Sale: stablecoin is zero");
64-
require(address(_eligibility) != address(0), "Sale: eligibility is zero");
6558
require(address(_aggregator) != address(0), "Sale: aggregator is zero");
6659
require(_rate > 0, "Sale: rate is not positive");
6760

6861
DPS = _DPS;
6962
STC = _STC;
70-
eligibility = _eligibility;
7163
aggregator = _aggregator;
7264
rate = _rate;
7365
minimumPurchaseSTC = _minimumPurchaseSTC;
@@ -146,18 +138,6 @@ contract Sale is Ownable {
146138
function _validate(address account, uint256 amountSTC) internal returns (uint256) {
147139
require(account != owner(), "Sale: investor is the sale owner");
148140

149-
(uint8 tier, uint256 limit) = eligibility.lookup(account);
150-
151-
require(tier > 0, "Sale: account is not eligible");
152-
153-
uint256 investmentSTC = convertDPStoSTC(DPS.balanceOf(account)) + amountSTC;
154-
uint256 limitSTC = limit * (10**STC.decimals());
155-
156-
if (limitSTC != 0) {
157-
// zero limit means that the tier has no restrictions
158-
require(investmentSTC <= limitSTC, "Sale: exceeds tier limit");
159-
}
160-
161141
uint256 amountDPS = convertSTCtoDPS(amountSTC);
162142
require(DPS.balanceOf(address(this)) >= amountDPS, "Sale: no enough tokens remaining");
163143

contracts/legacy/v1.0/Initializer.sol

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ pragma solidity ^0.8.0;
33

44
import "@openzeppelin/contracts/access/Ownable.sol";
55
import "../../DeepSquare.sol";
6-
import "../../Eligibility.sol";
6+
import "../v1.2/Eligibility.sol";
77

88
contract Initializer is Ownable {
99
DeepSquare public DPS;

contracts/legacy/v1.0/SaleV1.sol

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ pragma solidity ^0.8.0;
33

44
import "@openzeppelin/contracts/access/Ownable.sol";
55
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
6-
import "../../Eligibility.sol";
6+
import "../v1.2/Eligibility.sol";
77

88
/**
99
* @title Token sale.

contracts/legacy/v1.1/SaleV2.sol

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ pragma solidity ^0.8.0;
44
import "@openzeppelin/contracts/access/Ownable.sol";
55
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
66
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
7-
import "../../Eligibility.sol";
7+
import "../v1.2/Eligibility.sol";
88

99
/**
1010
* @title Token sale.

contracts/Eligibility.sol renamed to contracts/legacy/v1.2/Eligibility.sol

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ pragma solidity ^0.8.0;
33

44
import "@openzeppelin/contracts/access/AccessControl.sol";
55
import "@openzeppelin/contracts/utils/Context.sol";
6-
import "./interfaces/IEligibility.sol";
6+
import "../../interfaces/IEligibility.sol";
77

88
struct Result {
99
uint8 tier; // The KYC tier.

contracts/legacy/v1.2/SaleV3.sol

+235
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
import "@openzeppelin/contracts/access/Ownable.sol";
5+
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
6+
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
7+
import "./Eligibility.sol";
8+
9+
/**
10+
* @title Token sale.
11+
* @author Mathieu Bour, Julien Schneider, Charly Mancel, Valentin Pollart and Clarisse Tarrou for the DeepSquare Association.
12+
* @notice Conduct a token sale in exchange for a stablecoin (STC), e.g. USDC.
13+
*/
14+
contract SaleV3 is Ownable {
15+
/// @notice The DPS token contract being sold. It must have an owner() function in order to let the sale be closed.
16+
IERC20Metadata public immutable DPS;
17+
18+
/// @notice The stablecoin ERC20 contract.
19+
IERC20Metadata public immutable STC;
20+
21+
// @notice The eligibility contract.
22+
IEligibility public immutable eligibility;
23+
24+
/// @notice The Chainlink AVAX/USD pair aggregator.
25+
AggregatorV3Interface public aggregator;
26+
27+
/// @notice How many cents costs a DPS (e.g., 40 means a single DPS token costs 0.40 STC).
28+
uint8 public immutable rate;
29+
30+
/// @notice The minimum DPS purchase amount in stablecoin.
31+
uint256 public immutable minimumPurchaseSTC;
32+
33+
/// @notice How many DPS tokens were sold during the sale.
34+
uint256 public sold;
35+
36+
bool public isPaused;
37+
38+
/**
39+
* Token purchase event.
40+
* @param investor The investor address.
41+
* @param amountDPS Amount of DPS tokens purchased.
42+
*/
43+
event Purchase(address indexed investor, uint256 amountDPS);
44+
45+
/**
46+
* @param _DPS The DPS contract address.
47+
* @param _STC The ERC20 stablecoin contract address (e.g, USDT, USDC, etc.).
48+
* @param _eligibility The eligibility contract.
49+
* @param _aggregator The Chainlink AVAX/USD pair aggregator contract address.
50+
* @param _rate The DPS/STC rate in STC cents.
51+
* @param _initialSold How many DPS tokens were already sold.
52+
*/
53+
constructor(
54+
IERC20Metadata _DPS,
55+
IERC20Metadata _STC,
56+
Eligibility _eligibility,
57+
AggregatorV3Interface _aggregator,
58+
uint8 _rate,
59+
uint256 _minimumPurchaseSTC,
60+
uint256 _initialSold
61+
) {
62+
require(address(_DPS) != address(0), "Sale: token is zero");
63+
require(address(_STC) != address(0), "Sale: stablecoin is zero");
64+
require(address(_eligibility) != address(0), "Sale: eligibility is zero");
65+
require(address(_aggregator) != address(0), "Sale: aggregator is zero");
66+
require(_rate > 0, "Sale: rate is not positive");
67+
68+
DPS = _DPS;
69+
STC = _STC;
70+
eligibility = _eligibility;
71+
aggregator = _aggregator;
72+
rate = _rate;
73+
minimumPurchaseSTC = _minimumPurchaseSTC;
74+
sold = _initialSold;
75+
isPaused = false;
76+
}
77+
78+
/**
79+
* @notice Change the Chainlink AVAX/USD pair aggregator.
80+
* @param newAggregator The new aggregator contract address.
81+
*/
82+
function setAggregator(AggregatorV3Interface newAggregator) external onlyOwner {
83+
aggregator = newAggregator;
84+
}
85+
86+
/**
87+
* @notice Convert an AVAX amount to its equivalent of the stablecoin.
88+
* This allow to handle the AVAX purchase the same way as the stablecoin purchases.
89+
* @param amountAVAX The amount in AVAX wei.
90+
* @return The amount in STC.
91+
*/
92+
function convertAVAXtoSTC(uint256 amountAVAX) public view returns (uint256) {
93+
(, int256 answer, , , ) = aggregator.latestRoundData();
94+
require(answer > 0, "Sale: answer cannot be negative");
95+
96+
return (amountAVAX * uint256(answer) * 10**STC.decimals()) / 10**(18 + aggregator.decimals());
97+
}
98+
99+
/**
100+
* @notice Convert a stablecoin amount in DPS.
101+
* @dev Maximum possible working value is 210M DPS * 1e18 * 1e6 = 210e30.
102+
* Since log2(210e30) ~= 107, this cannot overflow an uint256.
103+
* @param amountSTC The amount in stablecoin.
104+
* @return The amount in DPS.
105+
*/
106+
function convertSTCtoDPS(uint256 amountSTC) public view returns (uint256) {
107+
return (amountSTC * (10**DPS.decimals()) * 100) / rate / (10**STC.decimals());
108+
}
109+
110+
/**
111+
* @notice Convert a DPS amount in stablecoin.
112+
* @dev Maximum possible working value is 210M DPS * 1e18 * 1e6 = 210e30.
113+
* Since log2(210e30) ~= 107,this cannot overflow an uint256.
114+
* @param amountDPS The amount in DPS.
115+
* @return The amount in stablecoin.
116+
*/
117+
function convertDPStoSTC(uint256 amountDPS) public view returns (uint256) {
118+
return (amountDPS * (10**STC.decimals()) * rate) / 100 / (10**DPS.decimals());
119+
}
120+
121+
/**
122+
* @notice Get the remaining DPS tokens to sell.
123+
* @return The amount of DPS remaining in the sale.
124+
*/
125+
function remaining() external view returns (uint256) {
126+
return DPS.balanceOf(address(this));
127+
}
128+
129+
/**
130+
* @notice Get the raised stablecoin amount.
131+
* @return The amount of stablecoin raised in the sale.
132+
*/
133+
function raised() external view returns (uint256) {
134+
return convertDPStoSTC(sold);
135+
}
136+
137+
/**
138+
* @notice Validate that the account is allowed to buy DPS.
139+
* @dev Requirements:
140+
* - the account is not the sale owner.
141+
* - the account is eligible.
142+
* @param account The account to check that should receive the DPS.
143+
* @param amountSTC The amount of stablecoin that will be used to purchase DPS.
144+
* @return The amount of DPS that should be transferred.
145+
*/
146+
function _validate(address account, uint256 amountSTC) internal returns (uint256) {
147+
require(account != owner(), "Sale: investor is the sale owner");
148+
149+
(uint8 tier, uint256 limit) = eligibility.lookup(account);
150+
151+
require(tier > 0, "Sale: account is not eligible");
152+
153+
uint256 investmentSTC = convertDPStoSTC(DPS.balanceOf(account)) + amountSTC;
154+
uint256 limitSTC = limit * (10**STC.decimals());
155+
156+
if (limitSTC != 0) {
157+
// zero limit means that the tier has no restrictions
158+
require(investmentSTC <= limitSTC, "Sale: exceeds tier limit");
159+
}
160+
161+
uint256 amountDPS = convertSTCtoDPS(amountSTC);
162+
require(DPS.balanceOf(address(this)) >= amountDPS, "Sale: no enough tokens remaining");
163+
164+
return amountDPS;
165+
}
166+
167+
/**
168+
* @notice Deliver the DPS to the account.
169+
* @dev Requirements:
170+
* - there are enough DPS remaining in the sale.
171+
* @param account The account that will receive the DPS.
172+
* @param amountDPS The amount of DPS to transfer.
173+
*/
174+
function _transferDPS(address account, uint256 amountDPS) internal {
175+
sold += amountDPS;
176+
DPS.transfer(account, amountDPS);
177+
178+
emit Purchase(account, amountDPS);
179+
}
180+
181+
/**
182+
* @notice Purchase DPS with AVAX native currency.
183+
* The invested amount will be msg.value.
184+
*/
185+
function purchaseDPSWithAVAX() external payable {
186+
require(!isPaused, "Sale is paused");
187+
uint256 amountSTC = convertAVAXtoSTC(msg.value);
188+
189+
require(amountSTC >= minimumPurchaseSTC, "Sale: amount lower than minimum");
190+
uint256 amountDPS = _validate(msg.sender, amountSTC);
191+
192+
// Using .transfer() might cause an out-of-gas revert if using gnosis safe as owner
193+
(bool sent, ) = payable(owner()).call{ value: msg.value }(""); // solhint-disable-line avoid-low-level-calls
194+
require(sent, "Sale: failed to forward AVAX");
195+
_transferDPS(msg.sender, amountDPS);
196+
}
197+
198+
/**
199+
* @notice Purchase DPS with stablecoin.
200+
* @param amountSTC The amount of stablecoin to invest.
201+
*/
202+
function purchaseDPSWithSTC(uint256 amountSTC) external {
203+
require(!isPaused, "Sale is paused");
204+
require(amountSTC >= minimumPurchaseSTC, "Sale: amount lower than minimum");
205+
uint256 amountDPS = _validate(msg.sender, amountSTC);
206+
207+
STC.transferFrom(msg.sender, owner(), amountSTC);
208+
_transferDPS(msg.sender, amountDPS);
209+
}
210+
211+
/**
212+
* @notice Deliver DPS tokens to an investor. Restricted to the sale OWNER.
213+
* @param amountSTC The amount of stablecoins invested, no minimum amount.
214+
* @param account The investor address.
215+
*/
216+
function deliverDPS(uint256 amountSTC, address account) external onlyOwner {
217+
uint256 amountDPS = _validate(account, amountSTC);
218+
_transferDPS(account, amountDPS);
219+
}
220+
221+
/**
222+
* @notice Pause the sale so that only the owner can deliverDps.
223+
*/
224+
function setPause(bool _isPaused) external onlyOwner {
225+
isPaused = _isPaused;
226+
}
227+
228+
/**
229+
* @notice Close the sale by sending the remaining tokens back to the owner and then renouncing ownership.
230+
*/
231+
function close() external onlyOwner {
232+
DPS.transfer(owner(), DPS.balanceOf(address(this))); // Transfer all the DPS back to the owner
233+
renounceOwnership();
234+
}
235+
}

0 commit comments

Comments
 (0)