Skip to content

Commit

Permalink
Merge pull request #170 from moleculeprotocol/feature/hubs-224-crowds…
Browse files Browse the repository at this point in the history
…ale-with-individual-locking-staking-periods

HUBS-224 crowdsale with individual locking staking periods
  • Loading branch information
elmariachi111 authored Jan 9, 2025
2 parents 43f8c86 + 46fba8d commit 4589881
Show file tree
Hide file tree
Showing 26 changed files with 1,418 additions and 160 deletions.
14 changes: 7 additions & 7 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@ PRICEFEED_ADDRESS=0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6
TERMS_ACCEPTED_PERMISSIONER_ADDRESS=0x8A791620dd6260079BF849Dc5567aDC3F2FdC318

TOKENIZER_ADDRESS=0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e
STAKED_LOCKING_CROWDSALE_ADDRESS=0x0B306BF915C4d645ff596e518fAf3F9669b97016
#iptoken implementation=0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82

USDC6_ADDRESS=0x68B1D87F95878fE05B998F19b66F4baba5De1aed
WETH_ADDRESS=0x4ed7c70F96B99c776995fB64377f0d4aB3B0e1C1
PLAIN_CROWDSALE_ADDRESS=0x7a2088a1bFc9d81c55368AE168C2C02570cB814F
#timelocked token implementation=0x0B306BF915C4d645ff596e518fAf3F9669b97016
STAKED_LOCKING_CROWDSALE_ADDRESS=0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1

#this is created during the tokenizer deployment
IPTOKEN_IMPLEMENTATION_ADDRESS=0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82
USDC6_ADDRESS=0x3Aa5ebB10DC797CAC828524e59A333d0A371443c
WETH_ADDRESS=0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44
PLAIN_CROWDSALE_ADDRESS=0x09635F643e140090A9A8Dcd712eD6285858ceBef

#these are generated when running the fixture scripts
IPTS_ADDRESS=0x8dAF17A20c9DBA35f005b6324F493785D239719d
LOCKED_IPTS_ADDRESS=0x16eBC21B3d38Db5e3EE1a022bEBA8Ec87D4CDbe6
LOCKED_IPTS_ADDRESS=0x24B3c7704709ed1491473F30393FFc93cFB0FC34
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@ IP-NFTs allow their users to tokenize intellectual property. This repo contains
| 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> |
| 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> |
| 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> |
| 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> |
| 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> |

timelocked token implementation=0x625ed621d814645AA81C50c4f333D4a407576e8F


#### Subgraph

API: https://subgraph.satsuma-prod.com/742d8952ab24/molecule--4039244/ip-nft-mainnet/api
Expand Down Expand Up @@ -46,7 +50,12 @@ Deprecated after migrating to Defender 2 (was 0x3D30452c48F2448764d5819a9A2b684A
| 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> |
| 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> |
| 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> |
| 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> |
| 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> |
| 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> |

timelocked token implementation=0xF8F79c1E02387b0Fc9DE0945cD9A2c06F127D851

new SLCS with support for verifiable timelocks & distinctly configurable staking / locking periods: https://sepolia.etherscan.io/address/0x2d309CF13dC3872f9c9B1B06Ebf6F60caDe08d55#code

#### Subgraphs

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

### Timelocked Tokens

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.

---

## Prerequisites

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):
Expand Down Expand Up @@ -204,6 +219,9 @@ The crowdsale computation model can be tried out here: <https://docs.google.com/
Deploying and verifying a single contract without the help of any script
`forge create --rpc-url $RPC_URL --private-key $PRIVATE_KEY --chain 5 --etherscan-api-key $ETHERSCAN_API_KEY --verify src/crowdsale/StakedLockingCrowdSale.sol:StakedLockingCrowdSale`

Verifying the staked crowdsale
`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`

### Deploying (vested) test tokens

To test staked / vested token interactions, you need some test tokens. Here are 2 convenient script to get them running:
Expand Down
6 changes: 5 additions & 1 deletion script/DeployTokenizer.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { BioPriceFeed } from "../src/BioPriceFeed.sol";
import { IPermissioner, TermsAcceptedPermissioner } from "../src/Permissioner.sol";
import { CrowdSale } from "../src/crowdsale/CrowdSale.sol";
import { StakedLockingCrowdSale } from "../src/crowdsale/StakedLockingCrowdSale.sol";
import { TimelockedToken } from "../src/TimelockedToken.sol";

contract DeployTokenizerInfrastructure is Script {
function run() public {
Expand All @@ -27,13 +28,16 @@ contract DeployTokenizerInfrastructure is Script {
tokenizer.setIPTokenImplementation(initialIpTokenImplementation);

CrowdSale crowdSale = new CrowdSale();
StakedLockingCrowdSale stakedLockingCrowdSale = new StakedLockingCrowdSale();
//this allows the default TimelockedToken implementation to be verified on chain explorers
TimelockedToken timelockedTokenImplementation = new TimelockedToken();
StakedLockingCrowdSale stakedLockingCrowdSale = new StakedLockingCrowdSale(timelockedTokenImplementation);
vm.stopBroadcast();

console.log("TERMS_ACCEPTED_PERMISSIONER_ADDRESS=%s", address(permissioner));
console.log("TOKENIZER_ADDRESS=%s", address(tokenizer));
console.log("CROWDSALE_ADDRESS=%s", address(crowdSale));
console.log("STAKED_LOCKING_CROWDSALE_ADDRESS=%s", address(stakedLockingCrowdSale));
console.log("timelocked token implementation=%s", address(timelockedTokenImplementation));
console.log("initial IP Token implementation=%s", address(initialIpTokenImplementation));
}
}
Expand Down
12 changes: 5 additions & 7 deletions script/dev/CrowdSale.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,19 @@ contract DeployCrowdSale is CommonScript {
}
}

/**
* @title deploy crowdSale
* @author
*/
contract DeployStakedCrowdSale is CommonScript {
function run() public {
prepareAddresses();
vm.startBroadcast(deployer);
StakedLockingCrowdSale stakedLockingCrowdSale = new StakedLockingCrowdSale();

TimelockedToken lockingCrowdsaleImplementation = new TimelockedToken();
StakedLockingCrowdSale stakedLockingCrowdSale = new StakedLockingCrowdSale(lockingCrowdsaleImplementation);

TokenVesting vestedDaoToken = TokenVesting(vm.envAddress("VDAO_TOKEN_ADDRESS"));
vestedDaoToken.grantRole(vestedDaoToken.ROLE_CREATE_SCHEDULE(), address(stakedLockingCrowdSale));
stakedLockingCrowdSale.trustVestingContract(vestedDaoToken);
vm.stopBroadcast();

console.log("timelocked token implementation=%s", address(lockingCrowdsaleImplementation));
console.log("STAKED_LOCKING_CROWDSALE_ADDRESS=%s", address(stakedLockingCrowdSale));
}
}
Expand Down Expand Up @@ -154,7 +152,7 @@ contract FixtureStakedCrowdSale is FixtureCrowdSale {
function startSale() internal override returns (uint256 saleId) {
Sale memory _sale = prepareRun();
vm.startBroadcast(bob);
saleId = _slCrowdSale.startSale(_sale, daoToken, vestedDaoToken, 1e18, 7 days);
saleId = _slCrowdSale.startSale(_sale, daoToken, vestedDaoToken, 1e18, 7 days, 7 days);
vm.stopBroadcast();
}

Expand Down
2 changes: 1 addition & 1 deletion script/dev/SignTermsMessage.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import "forge-std/console.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

contract SignTermsMessage is Script {
function run() public {
function run() public view {
uint256 pk = vm.envUint("PRIVATE_KEY");
string memory terms =
"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";
Expand Down
2 changes: 1 addition & 1 deletion script/dev/Tokenizer.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ contract DeployTokenizer is CommonScript {

vm.stopBroadcast();
console.log("TOKENIZER_ADDRESS=%s", address(tokenizer));
console.log("IPTOKEN_IMPLEMENTATION_ADDRESS=%s", address(initialIpTokenImplementation));
console.log("iptoken implementation=%s", address(initialIpTokenImplementation));
}
}

Expand Down
25 changes: 0 additions & 25 deletions script/prod/RolloutV23Sale.sol

This file was deleted.

63 changes: 63 additions & 0 deletions script/prod/RolloutV25Sale.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import "forge-std/Script.sol";
import "forge-std/console.sol";
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import { IPNFT } from "../../src/IPNFT.sol";
import { IPermissioner, TermsAcceptedPermissioner } from "../../src/Permissioner.sol";
import { StakedLockingCrowdSale } from "../../src/crowdsale/StakedLockingCrowdSale.sol";
import { LockingCrowdSale } from "../../src/crowdsale/LockingCrowdSale.sol";
import { TimelockedToken } from "../../src/TimelockedToken.sol";
import { TokenVesting } from "@moleculeprotocol/token-vesting/TokenVesting.sol";
import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";

TimelockedToken constant timelockedTokenImplementation = TimelockedToken(0x625ed621d814645AA81C50c4f333D4a407576e8F);

address constant moleculeDevMultisig = 0xCfA0F84660fB33bFd07C369E5491Ab02C449f71B;

contract DeployTimelockedTokenTemplate is Script {
function run() public {
vm.startBroadcast();
TimelockedToken impl = new TimelockedToken();
impl.initialize(IERC20Metadata(address(0x0)));
vm.stopBroadcast();

console.log("timelocked token implementation=%s", address(impl));
}
}

contract RolloutV25LockingSale is Script {
function run() public {

vm.startBroadcast();
LockingCrowdSale lockingCrowdsale = new LockingCrowdSale(timelockedTokenImplementation);
//lockingCrowdsale.transferOwnership(moleculeDevMultisig);
vm.stopBroadcast();

console.log("LOCKING_CROWDSALE_ADDRESS=%s", address(lockingCrowdsale));
console.log("timelocked token implementation=%s", address(timelockedTokenImplementation));
}
}


contract RolloutV25StakedSale is Script {
function run() public {

TokenVesting vesting = TokenVesting(0x8f80d1183CD983B01B0C9AC6777cC732Ec9800de); //Moldao

vm.startBroadcast();
StakedLockingCrowdSale stakedLockingCrowdSale = new StakedLockingCrowdSale(timelockedTokenImplementation);
vesting.grantRole(vesting.ROLE_CREATE_SCHEDULE(), address(stakedLockingCrowdSale));
//stakedLockingCrowdSale.trustLockingContract(IERC20());
stakedLockingCrowdSale.trustVestingContract(vesting);
// stakedLockingCrowdSale.transferOwnership(moleculeDevMultisig);
vm.stopBroadcast();

console.log("STAKED_LOCKING_CROWDSALE_ADDRESS=%s", address(stakedLockingCrowdSale));
console.log("timelocked token implementation=%s", address(timelockedTokenImplementation));
// 0x7c36c64DA1c3a2065074caa9C48e7648FB733aAB
// vestedDaoToken.grantRole(vestedDaoToken.ROLE_CREATE_SCHEDULE(), address(stakedLockingCrowdSale));
// stakedLockingCrowdSale.trustVestingContract(vestedDaoToken);
}
}
8 changes: 8 additions & 0 deletions setupLocal.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ set +a
fixtures=false
extrafixtures=false

show_help() {
echo "Usage: setupLocal.sh [OPTION]"
echo "Sets up the local environment for the IPNFT contracts."
echo "Options:"
echo " -f also runs basic fixture scripts"
echo " -x also runs extra fixture scripts (crowdsales)"
}

# Parse command-line options
while getopts "fx" opt; do
case ${opt} in
Expand Down
19 changes: 17 additions & 2 deletions src/crowdsale/LockingCrowdSale.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;

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

address immutable lockingTokenImplementation = address(new TimelockedToken());
address immutable public TIMELOCKED_TOKEN_IMPLEMENTATION;

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

constructor(TimelockedToken _timelockedTokenImplementation) {
TIMELOCKED_TOKEN_IMPLEMENTATION = address(_timelockedTokenImplementation);
}

/// @dev disable parent sale starting functions
function startSale(Sale calldata) public pure override returns (uint256) {
revert UnsupportedInitializer();
}

/**
* @notice allows the owner to trust a timelocked token contract for a specific underlying token so it's not registered again.
*
* @param token the underlying token
* @param _timelockedToken the timelocked token contract to trust
*/
function trustLockingContract(IERC20 token, TimelockedToken _timelockedToken) public onlyOwner {
lockingContracts[address(token)] = _timelockedToken;
}

/**
* @notice allows anyone to create a timelocked token that's controlled by this sale contract
* helpful if you want to reuse the timelocked token for your own custom schedules
Expand Down Expand Up @@ -114,7 +129,7 @@ contract LockingCrowdSale is CrowdSale {
* @return lockedTokenContract address of the new timelocked token contract
*/
function _makeNewLockedTokenContract(IERC20Metadata auctionToken) private returns (TimelockedToken lockedTokenContract) {
lockedTokenContract = TimelockedToken(Clones.clone(lockingTokenImplementation));
lockedTokenContract = TimelockedToken(Clones.clone(TIMELOCKED_TOKEN_IMPLEMENTATION));
lockedTokenContract.initialize(auctionToken);
emit LockingContractCreated(lockedTokenContract, auctionToken);
}
Expand Down
Loading

0 comments on commit 4589881

Please sign in to comment.