Skip to content

Commit 4589881

Browse files
Merge pull request #170 from moleculeprotocol/feature/hubs-224-crowdsale-with-individual-locking-staking-periods
HUBS-224 crowdsale with individual locking staking periods
2 parents 43f8c86 + 46fba8d commit 4589881

26 files changed

+1418
-160
lines changed

.env.example

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,15 @@ PRICEFEED_ADDRESS=0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6
2626
TERMS_ACCEPTED_PERMISSIONER_ADDRESS=0x8A791620dd6260079BF849Dc5567aDC3F2FdC318
2727

2828
TOKENIZER_ADDRESS=0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e
29-
STAKED_LOCKING_CROWDSALE_ADDRESS=0x0B306BF915C4d645ff596e518fAf3F9669b97016
29+
#iptoken implementation=0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82
3030

31-
USDC6_ADDRESS=0x68B1D87F95878fE05B998F19b66F4baba5De1aed
32-
WETH_ADDRESS=0x4ed7c70F96B99c776995fB64377f0d4aB3B0e1C1
33-
PLAIN_CROWDSALE_ADDRESS=0x7a2088a1bFc9d81c55368AE168C2C02570cB814F
31+
#timelocked token implementation=0x0B306BF915C4d645ff596e518fAf3F9669b97016
32+
STAKED_LOCKING_CROWDSALE_ADDRESS=0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1
3433

35-
#this is created during the tokenizer deployment
36-
IPTOKEN_IMPLEMENTATION_ADDRESS=0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82
34+
USDC6_ADDRESS=0x3Aa5ebB10DC797CAC828524e59A333d0A371443c
35+
WETH_ADDRESS=0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44
36+
PLAIN_CROWDSALE_ADDRESS=0x09635F643e140090A9A8Dcd712eD6285858ceBef
3737

3838
#these are generated when running the fixture scripts
3939
IPTS_ADDRESS=0x8dAF17A20c9DBA35f005b6324F493785D239719d
40-
LOCKED_IPTS_ADDRESS=0x16eBC21B3d38Db5e3EE1a022bEBA8Ec87D4CDbe6
40+
LOCKED_IPTS_ADDRESS=0x24B3c7704709ed1491473F30393FFc93cFB0FC34

README.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,12 @@ IP-NFTs allow their users to tokenize intellectual property. This repo contains
1414
| Tokenizer | [0x58EB89C69CB389DBef0c130C6296ee271b82f436](https://etherscan.io/address/0x58EB89C69CB389DBef0c130C6296ee271b82f436#code) | <a href="https://thirdweb.com/ethereum/0x58EB89C69CB389DBef0c130C6296ee271b82f436?utm_source=contract_badge" target="_blank"><img width="200" height="45" src="https://badges.thirdweb.com/contract?address=0x58EB89C69CB389DBef0c130C6296ee271b82f436&theme=dark&chainId=1" alt="View contract" /></a> |
1515
| Permissioner | [0xC837E02982992B701A1B5e4E21fA01cEB0a628fA](https://etherscan.io/address/0xC837E02982992B701A1B5e4E21fA01cEB0a628fA#code) | <a href="https://thirdweb.com/ethereum/0xC837E02982992B701A1B5e4E21fA01cEB0a628fA?utm_source=contract_badge" target="_blank"><img width="200" height="45" src="https://badges.thirdweb.com/contract?address=0xC837E02982992B701A1B5e4E21fA01cEB0a628fA&theme=dark&chainId=1" alt="View contract" /></a> |
1616
| Crowdsale | [0xf0a8d23f38e9cbbe01c4ed37f23bd519b65bc6c2](https://etherscan.io/address/0xf0a8d23f38e9cbbe01c4ed37f23bd519b65bc6c2#code) | <a href="https://thirdweb.com/ethereum/0xf0a8d23f38e9cbbe01c4ed37f23bd519b65bc6c2?utm_source=contract_badge" target="_blank"><img width="200" height="45" src="https://badges.thirdweb.com/contract?address=0xf0a8d23f38e9cbbe01c4ed37f23bd519b65bc6c2&theme=dark&chainId=1" alt="View contract" /></a> |
17+
| Locking Crowdsale | [0xfbfd266bf3b49Db8746155AA318D4533Cc66DB26](https://etherscan.io/address/0xfbfd266bf3b49Db8746155AA318D4533Cc66DB26#code) | <a href="https://thirdweb.com/ethereum/0xfbfd266bf3b49Db8746155AA318D4533Cc66DB26?utm_source=contract_badge" target="_blank"><img width="200" height="45" src="https://badges.thirdweb.com/contract?address=0xfbfd266bf3b49Db8746155AA318D4533Cc66DB26&theme=dark&chainId=1" alt="View contract" /></a> |
1718
| StakedLockingCrowdSale | [0x35Bce29F52f51f547998717CD598068Afa2B29B7](https://etherscan.io/address/0x35Bce29F52f51f547998717CD598068Afa2B29B7#code) | <a href="https://thirdweb.com/ethereum/0x35Bce29F52f51f547998717CD598068Afa2B29B7?utm_source=contract_badge" target="_blank"><img width="200" height="45" src="https://badges.thirdweb.com/contract?address=0x35Bce29F52f51f547998717CD598068Afa2B29B7&theme=dark&chainId=1" alt="View contract" /></a> |
1819

20+
timelocked token implementation=0x625ed621d814645AA81C50c4f333D4a407576e8F
21+
22+
1923
#### Subgraph
2024

2125
API: https://subgraph.satsuma-prod.com/742d8952ab24/molecule--4039244/ip-nft-mainnet/api
@@ -46,7 +50,12 @@ Deprecated after migrating to Defender 2 (was 0x3D30452c48F2448764d5819a9A2b684A
4650
| Terms Permissioner | 0xC05D649368d8A5e2E98CAa205d47795de5fCB599 | <a href="https://sepolia.etherscan.io/address/0xC05D649368d8A5e2E98CAa205d47795de5fCB599#code" target="_blank"><img width="200" height="45" src="https://badges.thirdweb.com/contract?address=0xC05D649368d8A5e2E98CAa205d47795de5fCB599&theme=dark&chainId=1" alt="View contract" /></a> |
4751
| Tokenizer | 0xca63411FF5187431028d003eD74B57531408d2F9 | <a href="https://sepolia.etherscan.io/address/0xca63411FF5187431028d003eD74B57531408d2F9#code" target="_blank"><img width="200" height="45" src="https://badges.thirdweb.com/contract?address=0xca63411FF5187431028d003eD74B57531408d2F9&theme=dark&chainId=1" alt="View contract" /></a> |
4852
| Crowdsale | 0x8cA737E2cdaE1Ceb332bEf7ba9eA711a3a2f8037 | <a href="https://sepolia.etherscan.io/address/0x8cA737E2cdaE1Ceb332bEf7ba9eA711a3a2f8037#code" target="_blank"><img width="200" height="45" src="https://badges.thirdweb.com/contract?address=0x8cA737E2cdaE1Ceb332bEf7ba9eA711a3a2f8037&theme=dark&chainId=1" alt="View contract" /></a> |
49-
| Staked Crowdsale | 0xd1cE2EA7d3b0C9cAB025A4aD762FC00315141ad7 | <a href="https://sepolia.etherscan.io/address/0xd1cE2EA7d3b0C9cAB025A4aD762FC00315141ad7#code" target="_blank"><img width="200" height="45" src="https://badges.thirdweb.com/contract?address=0xd1cE2EA7d3b0C9cAB025A4aD762FC00315141ad7&theme=dark&chainId=1" alt="View contract" /></a> |
53+
| Locking Crowdsale | 0x0Da77f361bB56f065Aa21647d885685eb7cAE10F | <a href="https://sepolia.etherscan.io/address/0x0Da77f361bB56f065Aa21647d885685eb7cAE10F#code" target="_blank"><img width="200" height="45" src="https://badges.thirdweb.com/contract?address=0x0Da77f361bB56f065Aa21647d885685eb7cAE10F&theme=dark&chainId=1" alt="View contract" /></a> |
54+
| Staked Crowdsale | 0xd1cE2EA7d3b0C9cAB025A4aD762FC00315141ad7 | <a href="https://sepolia.etherscan.io/address/0xd1cE2EA7d3b0C9cAB025A4aD762FC00315141ad7#code" target="_blank"><img width="200" height="45" src="https://badges.thirdweb.com/contract?address=0xd1cE2EA7d3b0C9cAB025A4aD762FC00315141ad7&theme=dark&chainId=11155111" alt="View contract" /></a> |
55+
56+
timelocked token implementation=0xF8F79c1E02387b0Fc9DE0945cD9A2c06F127D851
57+
58+
new SLCS with support for verifiable timelocks & distinctly configurable staking / locking periods: https://sepolia.etherscan.io/address/0x2d309CF13dC3872f9c9B1B06Ebf6F60caDe08d55#code
5059

5160
#### Subgraphs
5261

@@ -98,6 +107,12 @@ forge script --private-key=$PRIVATE_KEY --rpc-url=$RPC_URL script/prod/RolloutTo
98107
// 0xTokenizer 0xNewImpl 0xNewTokenImpl
99108
cast send --rpc-url=$RPC_URL --private-key=$PRIVATE_KEY 0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e "upgradeToAndCall(address,bytes)" 0x70e0bA845a1A0F2DA3359C97E0285013525FFC49 0x84646c1f000000000000000000000000998abeb3e57409262ae5b751f60747921b33613e
100109

110+
### Timelocked Tokens
111+
112+
originally the "timelocked token" was an inline concept of the slcs. Timelock contracts weren't reusable among cs impls. This changes as of beginning of 2025. As a rather simple but not very elegant (and certainly not correct) solution we decided to "trust" external locking contracts so you can reuse them among crowdsale instances. This was needed for the VitaRNA crowdsale that's supposed to just support locks, no stakes - and hence required another crowdsale instance. During this upgrade we decided to externalize the timelock token template so upcoming instances can be verified on chain.
113+
114+
---
115+
101116
## Prerequisites
102117

103118
To work with this repository you have to install Foundry (<https://getfoundry.sh>). Run the following command in your terminal, then follow the onscreen instructions (macOS and Linux):
@@ -204,6 +219,9 @@ The crowdsale computation model can be tried out here: <https://docs.google.com/
204219
Deploying and verifying a single contract without the help of any script
205220
`forge create --rpc-url $RPC_URL --private-key $PRIVATE_KEY --chain 5 --etherscan-api-key $ETHERSCAN_API_KEY --verify src/crowdsale/StakedLockingCrowdSale.sol:StakedLockingCrowdSale`
206221

222+
Verifying the staked crowdsale
223+
`forge verify-contract --chain-id=11155111 --etherscan-api-key=$ETHERSCAN_API_KEY --constructor-args $(cast abi-encode "constructor(address)" 0xF8F79c1E02387b0Fc9DE0945cD9A2c06F127D851) 0x7eeb7113f90893fb95c6666e3930235850f2bc6A src/crowdsale/StakedLockingCrowdSale.sol:StakedLockingCrowdSale`
224+
207225
### Deploying (vested) test tokens
208226

209227
To test staked / vested token interactions, you need some test tokens. Here are 2 convenient script to get them running:

script/DeployTokenizer.s.sol

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { BioPriceFeed } from "../src/BioPriceFeed.sol";
1111
import { IPermissioner, TermsAcceptedPermissioner } from "../src/Permissioner.sol";
1212
import { CrowdSale } from "../src/crowdsale/CrowdSale.sol";
1313
import { StakedLockingCrowdSale } from "../src/crowdsale/StakedLockingCrowdSale.sol";
14+
import { TimelockedToken } from "../src/TimelockedToken.sol";
1415

1516
contract DeployTokenizerInfrastructure is Script {
1617
function run() public {
@@ -27,13 +28,16 @@ contract DeployTokenizerInfrastructure is Script {
2728
tokenizer.setIPTokenImplementation(initialIpTokenImplementation);
2829

2930
CrowdSale crowdSale = new CrowdSale();
30-
StakedLockingCrowdSale stakedLockingCrowdSale = new StakedLockingCrowdSale();
31+
//this allows the default TimelockedToken implementation to be verified on chain explorers
32+
TimelockedToken timelockedTokenImplementation = new TimelockedToken();
33+
StakedLockingCrowdSale stakedLockingCrowdSale = new StakedLockingCrowdSale(timelockedTokenImplementation);
3134
vm.stopBroadcast();
3235

3336
console.log("TERMS_ACCEPTED_PERMISSIONER_ADDRESS=%s", address(permissioner));
3437
console.log("TOKENIZER_ADDRESS=%s", address(tokenizer));
3538
console.log("CROWDSALE_ADDRESS=%s", address(crowdSale));
3639
console.log("STAKED_LOCKING_CROWDSALE_ADDRESS=%s", address(stakedLockingCrowdSale));
40+
console.log("timelocked token implementation=%s", address(timelockedTokenImplementation));
3741
console.log("initial IP Token implementation=%s", address(initialIpTokenImplementation));
3842
}
3943
}

script/dev/CrowdSale.s.sol

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,21 +31,19 @@ contract DeployCrowdSale is CommonScript {
3131
}
3232
}
3333

34-
/**
35-
* @title deploy crowdSale
36-
* @author
37-
*/
3834
contract DeployStakedCrowdSale is CommonScript {
3935
function run() public {
4036
prepareAddresses();
4137
vm.startBroadcast(deployer);
42-
StakedLockingCrowdSale stakedLockingCrowdSale = new StakedLockingCrowdSale();
43-
38+
TimelockedToken lockingCrowdsaleImplementation = new TimelockedToken();
39+
StakedLockingCrowdSale stakedLockingCrowdSale = new StakedLockingCrowdSale(lockingCrowdsaleImplementation);
40+
4441
TokenVesting vestedDaoToken = TokenVesting(vm.envAddress("VDAO_TOKEN_ADDRESS"));
4542
vestedDaoToken.grantRole(vestedDaoToken.ROLE_CREATE_SCHEDULE(), address(stakedLockingCrowdSale));
4643
stakedLockingCrowdSale.trustVestingContract(vestedDaoToken);
4744
vm.stopBroadcast();
4845

46+
console.log("timelocked token implementation=%s", address(lockingCrowdsaleImplementation));
4947
console.log("STAKED_LOCKING_CROWDSALE_ADDRESS=%s", address(stakedLockingCrowdSale));
5048
}
5149
}
@@ -154,7 +152,7 @@ contract FixtureStakedCrowdSale is FixtureCrowdSale {
154152
function startSale() internal override returns (uint256 saleId) {
155153
Sale memory _sale = prepareRun();
156154
vm.startBroadcast(bob);
157-
saleId = _slCrowdSale.startSale(_sale, daoToken, vestedDaoToken, 1e18, 7 days);
155+
saleId = _slCrowdSale.startSale(_sale, daoToken, vestedDaoToken, 1e18, 7 days, 7 days);
158156
vm.stopBroadcast();
159157
}
160158

script/dev/SignTermsMessage.s.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import "forge-std/console.sol";
66
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
77

88
contract SignTermsMessage is Script {
9-
function run() public {
9+
function run() public view {
1010
uint256 pk = vm.envUint("PRIVATE_KEY");
1111
string memory terms =
1212
"As an IP token holder of IPNFT #10, I accept all terms that I've read here: ipfs://bafkreigk5dvqblnkdniges6ft5kmuly47ebw4vho6siikzmkaovq6sjstq\n\nChain Id: 31337\nVersion: 1";

script/dev/Tokenizer.s.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ contract DeployTokenizer is CommonScript {
2525

2626
vm.stopBroadcast();
2727
console.log("TOKENIZER_ADDRESS=%s", address(tokenizer));
28-
console.log("IPTOKEN_IMPLEMENTATION_ADDRESS=%s", address(initialIpTokenImplementation));
28+
console.log("iptoken implementation=%s", address(initialIpTokenImplementation));
2929
}
3030
}
3131

script/prod/RolloutV23Sale.sol

Lines changed: 0 additions & 25 deletions
This file was deleted.

script/prod/RolloutV25Sale.s.sol

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.18;
3+
4+
import "forge-std/Script.sol";
5+
import "forge-std/console.sol";
6+
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
7+
import { IPNFT } from "../../src/IPNFT.sol";
8+
import { IPermissioner, TermsAcceptedPermissioner } from "../../src/Permissioner.sol";
9+
import { StakedLockingCrowdSale } from "../../src/crowdsale/StakedLockingCrowdSale.sol";
10+
import { LockingCrowdSale } from "../../src/crowdsale/LockingCrowdSale.sol";
11+
import { TimelockedToken } from "../../src/TimelockedToken.sol";
12+
import { TokenVesting } from "@moleculeprotocol/token-vesting/TokenVesting.sol";
13+
import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
14+
15+
TimelockedToken constant timelockedTokenImplementation = TimelockedToken(0x625ed621d814645AA81C50c4f333D4a407576e8F);
16+
17+
address constant moleculeDevMultisig = 0xCfA0F84660fB33bFd07C369E5491Ab02C449f71B;
18+
19+
contract DeployTimelockedTokenTemplate is Script {
20+
function run() public {
21+
vm.startBroadcast();
22+
TimelockedToken impl = new TimelockedToken();
23+
impl.initialize(IERC20Metadata(address(0x0)));
24+
vm.stopBroadcast();
25+
26+
console.log("timelocked token implementation=%s", address(impl));
27+
}
28+
}
29+
30+
contract RolloutV25LockingSale is Script {
31+
function run() public {
32+
33+
vm.startBroadcast();
34+
LockingCrowdSale lockingCrowdsale = new LockingCrowdSale(timelockedTokenImplementation);
35+
//lockingCrowdsale.transferOwnership(moleculeDevMultisig);
36+
vm.stopBroadcast();
37+
38+
console.log("LOCKING_CROWDSALE_ADDRESS=%s", address(lockingCrowdsale));
39+
console.log("timelocked token implementation=%s", address(timelockedTokenImplementation));
40+
}
41+
}
42+
43+
44+
contract RolloutV25StakedSale is Script {
45+
function run() public {
46+
47+
TokenVesting vesting = TokenVesting(0x8f80d1183CD983B01B0C9AC6777cC732Ec9800de); //Moldao
48+
49+
vm.startBroadcast();
50+
StakedLockingCrowdSale stakedLockingCrowdSale = new StakedLockingCrowdSale(timelockedTokenImplementation);
51+
vesting.grantRole(vesting.ROLE_CREATE_SCHEDULE(), address(stakedLockingCrowdSale));
52+
//stakedLockingCrowdSale.trustLockingContract(IERC20());
53+
stakedLockingCrowdSale.trustVestingContract(vesting);
54+
// stakedLockingCrowdSale.transferOwnership(moleculeDevMultisig);
55+
vm.stopBroadcast();
56+
57+
console.log("STAKED_LOCKING_CROWDSALE_ADDRESS=%s", address(stakedLockingCrowdSale));
58+
console.log("timelocked token implementation=%s", address(timelockedTokenImplementation));
59+
// 0x7c36c64DA1c3a2065074caa9C48e7648FB733aAB
60+
// vestedDaoToken.grantRole(vestedDaoToken.ROLE_CREATE_SCHEDULE(), address(stakedLockingCrowdSale));
61+
// stakedLockingCrowdSale.trustVestingContract(vestedDaoToken);
62+
}
63+
}

setupLocal.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ set +a
99
fixtures=false
1010
extrafixtures=false
1111

12+
show_help() {
13+
echo "Usage: setupLocal.sh [OPTION]"
14+
echo "Sets up the local environment for the IPNFT contracts."
15+
echo "Options:"
16+
echo " -f also runs basic fixture scripts"
17+
echo " -x also runs extra fixture scripts (crowdsales)"
18+
}
19+
1220
# Parse command-line options
1321
while getopts "fx" opt; do
1422
case ${opt} in

src/crowdsale/LockingCrowdSale.sol

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity 0.8.18;
33

4+
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
45
import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
56
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
67
import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";
@@ -25,16 +26,30 @@ contract LockingCrowdSale is CrowdSale {
2526
/// @notice map from token address to reusable TimelockedToken contracts
2627
mapping(address => TimelockedToken) public lockingContracts;
2728

28-
address immutable lockingTokenImplementation = address(new TimelockedToken());
29+
address immutable public TIMELOCKED_TOKEN_IMPLEMENTATION;
2930

3031
event Started(uint256 indexed saleId, address indexed issuer, Sale sale, TimelockedToken lockingToken, uint256 lockingDuration, uint16 feeBp);
3132
event LockingContractCreated(TimelockedToken indexed lockingContract, IERC20Metadata indexed underlyingToken);
3233

34+
constructor(TimelockedToken _timelockedTokenImplementation) {
35+
TIMELOCKED_TOKEN_IMPLEMENTATION = address(_timelockedTokenImplementation);
36+
}
37+
3338
/// @dev disable parent sale starting functions
3439
function startSale(Sale calldata) public pure override returns (uint256) {
3540
revert UnsupportedInitializer();
3641
}
3742

43+
/**
44+
* @notice allows the owner to trust a timelocked token contract for a specific underlying token so it's not registered again.
45+
*
46+
* @param token the underlying token
47+
* @param _timelockedToken the timelocked token contract to trust
48+
*/
49+
function trustLockingContract(IERC20 token, TimelockedToken _timelockedToken) public onlyOwner {
50+
lockingContracts[address(token)] = _timelockedToken;
51+
}
52+
3853
/**
3954
* @notice allows anyone to create a timelocked token that's controlled by this sale contract
4055
* helpful if you want to reuse the timelocked token for your own custom schedules
@@ -114,7 +129,7 @@ contract LockingCrowdSale is CrowdSale {
114129
* @return lockedTokenContract address of the new timelocked token contract
115130
*/
116131
function _makeNewLockedTokenContract(IERC20Metadata auctionToken) private returns (TimelockedToken lockedTokenContract) {
117-
lockedTokenContract = TimelockedToken(Clones.clone(lockingTokenImplementation));
132+
lockedTokenContract = TimelockedToken(Clones.clone(TIMELOCKED_TOKEN_IMPLEMENTATION));
118133
lockedTokenContract.initialize(auctionToken);
119134
emit LockingContractCreated(lockedTokenContract, auctionToken);
120135
}

0 commit comments

Comments
 (0)