Evm upgrades and tests (#38)

* Add event testing to icco.js

* Add testnet upgrade script

* Add wormhole fee to tests

* Generate Solana specific VAAs in Conductor.sol

* Add solana contract to testnet.json

Co-authored-by: Drew <dsterioti@users.noreply.github.com>
This commit is contained in:
Drew 2022-05-31 10:00:32 -05:00 committed by GitHub
parent 9c492e8d3a
commit 19910c7239
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1352 additions and 309 deletions

View File

@ -21,36 +21,25 @@ import "../shared/ICCOStructs.sol";
* @notice This contract manages cross-chain token sales. It uses the wormhole
* core messaging layer to communicate token sale information to linked Contributor
* contracts. For successful sales, it uses the wormhole token bridge to
* send the sale token to contributor contracts in exchange for contributed funds.
* send the sale token to Contributor contracts in exchange for contributed funds.
* For unsuccessful sales, this contract will return the sale tokens to a
* specified recipient address.
*/
contract Conductor is ConductorGovernance, ReentrancyGuard {
/**
* @dev createSale serves to initialize a cross-chain token sale and disseminate
* information about the sale to registered Contributor contracts.
* - it validates sale parameters passed in by the client
* - it saves a copy of the sale in contract storage
* - it encodes and disseminates sale information to contributor contracts via wormhole
*/
function createSale(
ICCOStructs.Raise memory raise,
ICCOStructs.Token[] memory acceptedTokens
) public payable nonReentrant returns (
uint saleId,
uint wormholeSequence
) {
/// validate sale parameters from client
require(block.timestamp < raise.saleStart, "sale start must be in the future");
require(raise.saleStart < raise.saleEnd, "sale end must be after sale start");
/// set timestamp cap for non-evm contributor contracts
require(raise.saleStart <= 2**63-1, "saleStart too far in the future");
require(raise.tokenAmount > 0, "amount must be > 0");
require(acceptedTokens.length > 0, "must accept at least one token");
require(acceptedTokens.length < 255, "too many tokens");
require(raise.maxRaise >= raise.minRaise, "maxRaise must be >= minRaise");
/// @dev create dynamic storage for accepted solana tokens
ICCOStructs.SolanaToken[] solanaAcceptedTokens;
/// grab the local token address (address of sale token on conductor chain)
/**
* @dev receiveSaleToken serves to take custody of the sale token and
* returns information about the token on the Conductor chain.
* - it transfers the sale tokens to this contract
* - it finds the address of the token on the Conductor chain
* - it finds the ERC20 token decimals of the token on the Conductor chain
*/
function receiveSaleToken(
ICCOStructs.Raise memory raise
) internal returns (address, uint8) {
/// @dev grab the local token address (address of sale token on conductor chain)
address localTokenAddress;
if (raise.tokenChain == chainId()) {
localTokenAddress = address(uint160(uint256(raise.token)));
@ -60,42 +49,68 @@ contract Conductor is ConductorGovernance, ReentrancyGuard {
require(localTokenAddress != address(0), "wrapped address not found on this chain");
}
uint8 localTokenDecimals;
{ /// avoid stack too deep errors
/**
* @dev Fetch the sale token decimals and place in the SaleInit struct.
* The contributors need to know this to scale allocations on non-evm chains.
*/
(,bytes memory queriedDecimals) = localTokenAddress.staticcall(
abi.encodeWithSignature("decimals()")
);
localTokenDecimals = abi.decode(queriedDecimals, (uint8));
/**
* @dev Fetch the sale token decimals and place in the SaleInit struct.
* The Contributors need to know this to scale allocations on non-evm chains.
*/
(,bytes memory queriedDecimals) = localTokenAddress.staticcall(
abi.encodeWithSignature("decimals()")
);
uint8 localTokenDecimals = abi.decode(queriedDecimals, (uint8));
/// query own token balance before transfer
(,bytes memory queriedBalanceBefore) = localTokenAddress.staticcall(
abi.encodeWithSelector(IERC20.balanceOf.selector,
address(this))
);
uint256 balanceBefore = abi.decode(queriedBalanceBefore, (uint256));
/// query own token balance before transfer
(,bytes memory queriedBalanceBefore) = localTokenAddress.staticcall(
abi.encodeWithSelector(IERC20.balanceOf.selector,
address(this))
);
uint256 balanceBefore = abi.decode(queriedBalanceBefore, (uint256));
/// deposit sale tokens
SafeERC20.safeTransferFrom(
IERC20(localTokenAddress),
msg.sender,
address(this),
raise.tokenAmount
);
/// deposit sale tokens
SafeERC20.safeTransferFrom(
IERC20(localTokenAddress),
msg.sender,
address(this),
raise.tokenAmount
);
/// query own token balance after transfer
(,bytes memory queriedBalanceAfter) = localTokenAddress.staticcall(
abi.encodeWithSelector(IERC20.balanceOf.selector,
address(this))
);
uint256 balanceAfter = abi.decode(queriedBalanceAfter, (uint256));
/// query own token balance after transfer
(,bytes memory queriedBalanceAfter) = localTokenAddress.staticcall(
abi.encodeWithSelector(IERC20.balanceOf.selector,
address(this))
);
uint256 balanceAfter = abi.decode(queriedBalanceAfter, (uint256));
/// revert if token has fee
require(raise.tokenAmount == balanceAfter - balanceBefore, "fee-on-transfer tokens are not supported");
}
/// revert if token has fee
require(raise.tokenAmount == balanceAfter - balanceBefore, "fee-on-transfer tokens are not supported");
return (localTokenAddress, localTokenDecimals);
}
/**
* @dev createSale serves to initialize a cross-chain token sale and disseminate
* information about the sale to registered Contributor contracts.
* - it validates sale parameters passed in by the client
* - it saves a copy of the sale in contract storage
* - it encodes and disseminates sale information to Contributor contracts via wormhole
*/
function createSale(
ICCOStructs.Raise memory raise,
ICCOStructs.Token[] memory acceptedTokens
) public payable nonReentrant returns (
uint256 saleId
) {
/// validate sale parameters from client
require(block.timestamp < raise.saleStart, "sale start must be in the future");
require(raise.saleStart < raise.saleEnd, "sale end must be after sale start");
/// set timestamp cap for non-evm Contributor contracts
require(raise.saleStart <= 2**63-1, "saleStart too far in the future");
require(raise.tokenAmount > 0, "amount must be > 0");
require(acceptedTokens.length > 0, "must accept at least one token");
require(acceptedTokens.length < 255, "too many tokens");
require(raise.maxRaise >= raise.minRaise, "maxRaise must be >= minRaise");
/// @dev take custody of sale token and fetch decimal/address info for the sale token
(address localTokenAddress, uint8 localTokenDecimals) = receiveSaleToken(raise);
/// create Sale struct for Conductor's view of the sale
saleId = useSaleId();
@ -116,7 +131,8 @@ contract Conductor is ConductorGovernance, ReentrancyGuard {
acceptedTokensChains : new uint16[](acceptedTokens.length),
acceptedTokensAddresses : new bytes32[](acceptedTokens.length),
acceptedTokensConversionRates : new uint128[](acceptedTokens.length),
contributions : new uint[](acceptedTokens.length),
solanaAcceptedTokensCount: 0,
contributions : new uint256[](acceptedTokens.length),
contributionsCollected : new bool[](acceptedTokens.length),
/// sale wallet management
initiator : msg.sender,
@ -128,18 +144,37 @@ contract Conductor is ConductorGovernance, ReentrancyGuard {
refundIsClaimed : false
});
/// populate the accpeted token arrays
for(uint i = 0; i < acceptedTokens.length; i++) {
/// populate the accepted token arrays
for (uint256 i = 0; i < acceptedTokens.length; i++) {
require(acceptedTokens[i].conversionRate > 0, "conversion rate cannot be zero");
sale.acceptedTokensChains[i] = acceptedTokens[i].tokenChain;
sale.acceptedTokensAddresses[i] = acceptedTokens[i].tokenAddress;
sale.acceptedTokensConversionRates[i] = acceptedTokens[i].conversionRate;
/// store the accepted tokens for the SolanaSaleInit VAA
if (acceptedTokens[i].tokenChain == 1) {
ICCOStructs.SolanaToken memory solanaToken = ICCOStructs.SolanaToken({
tokenIndex: uint8(i),
tokenAddress: acceptedTokens[i].tokenAddress
});
/// only allow 10 accepted tokens for the Solana Contributor
require(solanaAcceptedTokens.length < 8, "too many solana tokens");
/// save in contract storage
solanaAcceptedTokens.push(solanaToken);
}
}
/// save number of accepted solana tokens in the sale
sale.solanaAcceptedTokensCount = uint8(solanaAcceptedTokens.length);
/// store sale info
setSale(saleId, sale);
/// create SaleInit struct to disseminate to contributors
/// cache wormhole instance
IWormhole wormhole = wormhole();
uint256 messageFee = wormhole.messageFee();
/// create SaleInit struct to disseminate to Contributors
ICCOStructs.SaleInit memory saleInit = ICCOStructs.SaleInit({
payloadID : 1,
/// sale ID
@ -168,15 +203,44 @@ contract Conductor is ConductorGovernance, ReentrancyGuard {
recipient : bytes32(uint256(uint160(raise.recipient))),
/// refund recipient in case the sale is aborted
refundRecipient : bytes32(uint256(uint160(raise.refundRecipient)))
});
});
/**
* @dev send encoded SaleInit struct to contributors via wormhole.
* The msg.value is the fee collected by wormhole for messages.
*/
wormholeSequence = wormhole().publishMessage{
value : msg.value
/// @dev send encoded SaleInit struct to Contributors via wormhole.
wormhole.publishMessage{
value : messageFee
}(0, ICCOStructs.encodeSaleInit(saleInit), consistencyLevel());
/// see if the sale accepts any Solana tokens
if (solanaAcceptedTokens.length > 0) {
/// create SolanaSaleInit struct to disseminate to the Solana Contributor
ICCOStructs.SolanaSaleInit memory solanaSaleInit = ICCOStructs.SolanaSaleInit({
payloadID : 5,
/// sale ID
saleID : saleId,
/// sale token ATA for solana
solanaTokenAccount: raise.solanaTokenAccount,
/// chain ID of the token
tokenChain : raise.tokenChain,
/// token decimals
tokenDecimals: localTokenDecimals,
/// timestamp raise start
saleStart : raise.saleStart,
/// timestamp raise end
saleEnd : raise.saleEnd,
/// accepted Tokens
acceptedTokens : solanaAcceptedTokens,
/// recipient of proceeds
recipient : bytes32(uint256(uint160(raise.recipient)))
});
/// @dev send encoded SolanaSaleInit struct to the solana Contributor
wormhole.publishMessage{
value : messageFee
}(0, ICCOStructs.encodeSolanaSaleInit(solanaSaleInit), consistencyLevel());
/// @dev garbage collection to save on gas fees
delete solanaAcceptedTokens;
}
}
/**
@ -186,7 +250,7 @@ contract Conductor is ConductorGovernance, ReentrancyGuard {
* - it only allows the sale initiator to invoke the method
* - it encodes and disseminates a saleAborted message to the Contributor contracts
*/
function abortSaleBeforeStartTime(uint saleId) public payable returns (uint wormholeSequence) {
function abortSaleBeforeStartTime(uint256 saleId) public payable returns (uint256 wormholeSequence) {
require(saleExists(saleId), "sale not initiated");
ConductorStructs.Sale memory sale = sales(saleId);
@ -241,7 +305,7 @@ contract Conductor is ConductorGovernance, ReentrancyGuard {
);
/// save the total contribution amount for each accepted token
for(uint i = 0; i < conSealed.contributions.length; i++) {
for (uint256 i = 0; i < conSealed.contributions.length; i++) {
setSaleContribution(
conSealed.saleID,
conSealed.contributions[i].tokenIndex,
@ -257,7 +321,7 @@ contract Conductor is ConductorGovernance, ReentrancyGuard {
* - it calculates allocations and excess contributions for each accepted token
* - it disseminates a saleSealed or saleAborted message to Contributors via wormhole
*/
function sealSale(uint saleId) public payable returns (uint wormholeSequence) {
function sealSale(uint256 saleId) public payable returns (uint256 wormholeSequence) {
require(saleExists(saleId), "sale not initiated");
ConductorStructs.Sale memory sale = sales(saleId);
@ -267,7 +331,7 @@ contract Conductor is ConductorGovernance, ReentrancyGuard {
ConductorStructs.InternalAccounting memory accounting;
for (uint i = 0; i < sale.contributionsCollected.length; i++) {
for (uint256 i = 0; i < sale.contributionsCollected.length; i++) {
require(saleContributionIsCollected(saleId, i), "missing contribution info");
/**
* @dev This calculates the total contribution for each accepted token.
@ -284,7 +348,7 @@ contract Conductor is ConductorGovernance, ReentrancyGuard {
/// set the messageFee and valueSent values
accounting.messageFee = wormhole.messageFee();
accounting.valueSent = msg.value;
accounting.valueSent = msg.value;
/**
* @dev This determines if contributors qualify for refund payments.
@ -304,9 +368,9 @@ contract Conductor is ConductorGovernance, ReentrancyGuard {
});
/// calculate allocations and excessContributions for each accepted token
for(uint i = 0; i < sale.acceptedTokensAddresses.length; i++) {
uint allocation = sale.tokenAmount * (sale.contributions[i] * sale.acceptedTokensConversionRates[i] / 1e18) / accounting.totalContribution;
uint excessContribution = accounting.totalExcessContribution * sale.contributions[i] / accounting.totalContribution;
for (uint256 i = 0; i < sale.acceptedTokensAddresses.length; i++) {
uint256 allocation = sale.tokenAmount * (sale.contributions[i] * sale.acceptedTokensConversionRates[i] / 1e18) / accounting.totalContribution;
uint256 excessContribution = accounting.totalExcessContribution * sale.contributions[i] / accounting.totalContribution;
if (allocation > 0) {
/// send allocations to Contributor contracts
@ -377,6 +441,30 @@ contract Conductor is ConductorGovernance, ReentrancyGuard {
wormholeSequence = wormhole.publishMessage{
value : accounting.messageFee
}(0, ICCOStructs.encodeSaleSealed(saleSealed), consistencyLevel());
{ /// scope to make code more readable
/// @dev send separate SaleSealed VAA if accepting Solana tokens
if (sale.solanaAcceptedTokensCount > 0) {
/// create new array to handle solana allocations
ICCOStructs.Allocation[] memory solanaAllocations = new ICCOStructs.Allocation[](sale.solanaAcceptedTokensCount);
/// remove non-solana allocations in SaleSealed VAA
uint8 solanaAllocationIndex;
for (uint256 i = 0; i < sale.acceptedTokensAddresses.length; i++) {
if (sale.acceptedTokensChains[i] == 1) {
solanaAllocations[solanaAllocationIndex] = saleSealed.allocations[i];
solanaAllocationIndex += 1;
}
}
/// @dev replace allocations in the saleSealed struct with Solana only allocations
saleSealed.allocations = solanaAllocations;
/// @dev send encoded SaleSealed message to Solana Contributor
wormholeSequence = wormhole.publishMessage{
value : accounting.messageFee
}(0, ICCOStructs.encodeSaleSealed(saleSealed), consistencyLevel());
}
}
} else {
/// set saleAborted
setSaleAborted(sale.saleID);
@ -396,7 +484,7 @@ contract Conductor is ConductorGovernance, ReentrancyGuard {
* - it confirms that the sale was aborted
* - it transfers the sale tokens to the refundRecipient
*/
function claimRefund(uint saleId) public {
function claimRefund(uint256 saleId) public {
require(saleExists(saleId), "sale not initiated");
ConductorStructs.Sale memory sale = sales(saleId);
@ -430,7 +518,7 @@ contract Conductor is ConductorGovernance, ReentrancyGuard {
}
/// @dev saleExists serves to check if a sale exists
function saleExists(uint saleId) public view returns (bool exists) {
function saleExists(uint256 saleId) public view returns (bool exists) {
exists = (saleId < getNextSaleId());
}
}

View File

@ -39,11 +39,11 @@ contract ConductorGetters is ConductorState {
return _state.contributorImplementations[chainId_];
}
function solanaWallet(uint saleId_) public view returns (bytes32) {
function solanaWallet(uint256 saleId_) public view returns (bytes32) {
return _state.sales[saleId_].solanaTokenAccount;
}
function contributorWallets(uint saleId_, uint16 chainId_) public view returns (bytes32) {
function contributorWallets(uint256 saleId_, uint16 chainId_) public view returns (bytes32) {
/// @dev Solana chainID == 1
if (chainId_ == 1) {
return solanaWallet(saleId_);
@ -52,19 +52,19 @@ contract ConductorGetters is ConductorState {
}
}
function sales(uint saleId_) public view returns (ConductorStructs.Sale memory sale) {
function sales(uint256 saleId_) public view returns (ConductorStructs.Sale memory sale) {
return _state.sales[saleId_];
}
function getNextSaleId() public view returns (uint) {
function getNextSaleId() public view returns (uint256) {
return _state.nextSaleId;
}
function saleContributionIsCollected(uint saleId_, uint tokenIndex) public view returns (bool) {
function saleContributionIsCollected(uint256 saleId_, uint256 tokenIndex) public view returns (bool) {
return _state.sales[saleId_].contributionsCollected[tokenIndex];
}
function saleContributions(uint saleId_) public view returns (uint[] memory) {
function saleContributions(uint256 saleId_) public view returns (uint256[] memory) {
return _state.sales[saleId_].contributions;
}
}

View File

@ -35,28 +35,28 @@ contract ConductorSetters is ConductorState, Context {
_state.consistencyLevel = level;
}
function setSale(uint saleId, ConductorStructs.Sale memory sale) internal {
function setSale(uint256 saleId, ConductorStructs.Sale memory sale) internal {
_state.sales[saleId] = sale;
}
function setSaleContribution(uint saleId, uint tokenIndex, uint contribution) internal {
function setSaleContribution(uint256 saleId, uint256 tokenIndex, uint256 contribution) internal {
_state.sales[saleId].contributions[tokenIndex] = contribution;
_state.sales[saleId].contributionsCollected[tokenIndex] = true;
}
function setSaleSealed(uint saleId) internal {
function setSaleSealed(uint256 saleId) internal {
_state.sales[saleId].isSealed = true;
}
function setSaleAborted(uint saleId) internal {
function setSaleAborted(uint256 saleId) internal {
_state.sales[saleId].isAborted = true;
}
function setRefundClaimed(uint saleId) internal {
function setRefundClaimed(uint256 saleId) internal {
_state.sales[saleId].refundIsClaimed = true;
}
function setNextSaleId(uint nextSaleId) internal {
function setNextSaleId(uint256 nextSaleId) internal {
_state.nextSaleId = nextSaleId;
}
}

View File

@ -28,7 +28,7 @@ contract ConductorStorage {
mapping(uint16 => bytes32) contributorImplementations;
/// mapping of Sales
mapping(uint => ConductorStructs.Sale) sales;
mapping(uint256 => ConductorStructs.Sale) sales;
/// next sale id
uint256 nextSaleId;

View File

@ -31,8 +31,9 @@ contract ConductorStructs {
uint16[] acceptedTokensChains;
bytes32[] acceptedTokensAddresses;
uint128[] acceptedTokensConversionRates;
uint8 solanaAcceptedTokensCount;
/// contributions
uint[] contributions;
uint256[] contributions;
bool[] contributionsCollected;
/// sale initiator - can abort the sale before saleStart
address initiator;

View File

@ -74,7 +74,7 @@ contract Contributor is ContributorGovernance, ReentrancyGuard {
* on this Contributor chain.
* - it checks that the token is a valid ERC20 token
*/
for (uint i = 0; i < saleInit.acceptedTokens.length; i++) {
for (uint256 i = 0; i < saleInit.acceptedTokens.length; i++) {
if (saleInit.acceptedTokens[i].tokenChain == chainId()) {
address tokenAddress = address(uint160(uint256(saleInit.acceptedTokens[i].tokenAddress)));
(, bytes memory queriedTotalSupply) = tokenAddress.staticcall(
@ -132,7 +132,7 @@ contract Contributor is ContributorGovernance, ReentrancyGuard {
* - it takes custody of contributed funds
* - it stores information about the contribution and contributor
*/
function contribute(uint saleId, uint tokenIndex, uint amount, bytes memory sig) public nonReentrant {
function contribute(uint256 saleId, uint256 tokenIndex, uint256 amount, bytes memory sig) public nonReentrant {
require(saleExists(saleId), "sale not initiated");
{/// bypass stack too deep
@ -141,7 +141,7 @@ contract Contributor is ContributorGovernance, ReentrancyGuard {
require(!isAborted, "sale was aborted");
(uint start, uint end) = getSaleTimeframe(saleId);
(uint256 start, uint256 end) = getSaleTimeframe(saleId);
require(block.timestamp >= start, "sale not yet started");
require(block.timestamp <= end, "sale has ended");
@ -200,7 +200,7 @@ contract Contributor is ContributorGovernance, ReentrancyGuard {
* - it calculates the total contributions for each accepted token
* - it disseminates a ContributionSealed struct via wormhole
*/
function attestContributions(uint saleId) public payable returns (uint wormholeSequence) {
function attestContributions(uint256 saleId) public payable returns (uint256 wormholeSequence) {
require(saleExists(saleId), "sale not initiated");
/// confirm that the sale period has ended
@ -210,9 +210,9 @@ contract Contributor is ContributorGovernance, ReentrancyGuard {
require(block.timestamp > sale.saleEnd, "sale has not yet ended");
/// count accepted tokens for this contract to allocate memory in ContributionsSealed struct
uint nativeTokens = 0;
uint chainId = chainId(); /// cache from storage
for (uint i = 0; i < sale.acceptedTokensAddresses.length; i++) {
uint256 nativeTokens = 0;
uint16 chainId = chainId(); /// cache from storage
for (uint256 i = 0; i < sale.acceptedTokensAddresses.length; i++) {
if (sale.acceptedTokensChains[i] == chainId) {
nativeTokens++;
}
@ -226,8 +226,8 @@ contract Contributor is ContributorGovernance, ReentrancyGuard {
contributions : new ICCOStructs.Contribution[](nativeTokens)
});
uint ci = 0;
for (uint i = 0; i < sale.acceptedTokensAddresses.length; i++) {
uint256 ci = 0;
for (uint256 i = 0; i < sale.acceptedTokensAddresses.length; i++) {
if (sale.acceptedTokensChains[i] == chainId) {
consSealed.contributions[ci].tokenIndex = uint8(i);
consSealed.contributions[ci].contributed = getSaleTotalContribution(saleId, i);
@ -278,13 +278,13 @@ contract Contributor is ContributorGovernance, ReentrancyGuard {
(, bytes memory queriedTokenBalance) = saleTokenAddress.staticcall(
abi.encodeWithSelector(IERC20.balanceOf.selector, address(this))
);
uint tokenBalance = abi.decode(queriedTokenBalance, (uint256));
uint256 tokenBalance = abi.decode(queriedTokenBalance, (uint256));
require(tokenBalance > 0, "sale token balance must be non-zero");
/// store the allocated token amounts defined in the SaleSealed message
uint tokenAllocation;
for (uint i = 0; i < sealedSale.allocations.length; i++) {
uint256 tokenAllocation;
for (uint256 i = 0; i < sealedSale.allocations.length; i++) {
ICCOStructs.Allocation memory allo = sealedSale.allocations[i];
if (sale.acceptedTokensChains[allo.tokenIndex] == thisChainId) {
tokenAllocation += allo.allocation;
@ -304,15 +304,15 @@ contract Contributor is ContributorGovernance, ReentrancyGuard {
* are being sent to a recipient on a different chain.
*/
ITokenBridge tknBridge = tokenBridge();
uint messageFee = wormhole().messageFee();
uint valueSent = msg.value;
uint256 messageFee = wormhole().messageFee();
uint256 valueSent = msg.value;
/**
* @dev Cache the conductorChainId from storage to save on gas.
* We will check each accpetedToken to see if its from this chain.
*/
uint16 conductorChainId = conductorChainId();
for (uint i = 0; i < sale.acceptedTokensAddresses.length; i++) {
for (uint256 i = 0; i < sale.acceptedTokensAddresses.length; i++) {
if (sale.acceptedTokensChains[i] == thisChainId) {
/// compute the total contributions to send to the recipient
uint256 totalContributionsLessExcess = getSaleTotalContribution(sale.saleID, i) - getSaleExcessContribution(sale.saleID, i);
@ -396,7 +396,7 @@ contract Contributor is ContributorGovernance, ReentrancyGuard {
* - it transfer any excessContributions to the contributors wallet
* - it marks the allocation as claimed to prevent multiple claims for the same allocation
*/
function claimAllocation(uint saleId, uint tokenIndex) public {
function claimAllocation(uint256 saleId, uint256 tokenIndex) public {
require(saleExists(saleId), "sale not initiated");
/// make sure the sale is sealed and not aborted
@ -457,7 +457,7 @@ contract Contributor is ContributorGovernance, ReentrancyGuard {
* - it confirms that the sale was aborted
* - it transfers the contributed funds back to the contributor's wallet
*/
function claimRefund(uint saleId, uint tokenIndex) public {
function claimRefund(uint256 saleId, uint256 tokenIndex) public {
require(saleExists(saleId), "sale not initiated");
(, bool isAborted) = getSaleStatus(saleId);
@ -490,7 +490,7 @@ contract Contributor is ContributorGovernance, ReentrancyGuard {
}
/// @dev saleExists serves to check if a sale exists
function saleExists(uint saleId) public view returns (bool exists) {
function saleExists(uint256 saleId) public view returns (bool exists) {
exists = (getSaleTokenAddress(saleId) != bytes32(0));
}
}

View File

@ -47,11 +47,11 @@ contract ContributorGetters is ContributorState {
return _state.provider.conductorContract;
}
function sales(uint saleId_) public view returns (ContributorStructs.Sale memory sale){
function sales(uint256 saleId_) public view returns (ContributorStructs.Sale memory sale){
return _state.sales[saleId_];
}
function getSaleAcceptedTokenInfo(uint saleId_, uint tokenIndex) public view returns (uint16 tokenChainId, bytes32 tokenAddress, uint128 conversionRate){
function getSaleAcceptedTokenInfo(uint256 saleId_, uint256 tokenIndex) public view returns (uint16 tokenChainId, bytes32 tokenAddress, uint128 conversionRate){
return (
_state.sales[saleId_].acceptedTokensChains[tokenIndex],
_state.sales[saleId_].acceptedTokensAddresses[tokenIndex],
@ -59,45 +59,45 @@ contract ContributorGetters is ContributorState {
);
}
function getSaleTimeframe(uint saleId_) public view returns (uint256 start, uint256 end){
function getSaleTimeframe(uint256 saleId_) public view returns (uint256 start, uint256 end){
return (
_state.sales[saleId_].saleStart,
_state.sales[saleId_].saleEnd
);
}
function getSaleStatus(uint saleId_) public view returns (bool isSealed, bool isAborted){
function getSaleStatus(uint256 saleId_) public view returns (bool isSealed, bool isAborted){
return (
_state.sales[saleId_].isSealed,
_state.sales[saleId_].isAborted
);
}
function getSaleTokenAddress(uint saleId_) public view returns (bytes32 tokenAddress){
function getSaleTokenAddress(uint256 saleId_) public view returns (bytes32 tokenAddress){
tokenAddress = _state.sales[saleId_].tokenAddress;
}
function getSaleAllocation(uint saleId, uint tokenIndex) public view returns (uint256 allocation){
function getSaleAllocation(uint256 saleId, uint256 tokenIndex) public view returns (uint256 allocation){
return _state.sales[saleId].allocations[tokenIndex];
}
function getSaleExcessContribution(uint saleId, uint tokenIndex) public view returns (uint256 allocation){
function getSaleExcessContribution(uint256 saleId, uint256 tokenIndex) public view returns (uint256 allocation){
return _state.sales[saleId].excessContributions[tokenIndex];
}
function getSaleTotalContribution(uint saleId, uint tokenIndex) public view returns (uint256 contributed){
function getSaleTotalContribution(uint256 saleId, uint256 tokenIndex) public view returns (uint256 contributed){
return _state.totalContributions[saleId][tokenIndex];
}
function getSaleContribution(uint saleId, uint tokenIndex, address contributor) public view returns (uint256 contributed){
function getSaleContribution(uint256 saleId, uint256 tokenIndex, address contributor) public view returns (uint256 contributed){
return _state.contributions[saleId][tokenIndex][contributor];
}
function refundIsClaimed(uint saleId, uint tokenIndex, address contributor) public view returns (bool){
function refundIsClaimed(uint256 saleId, uint256 tokenIndex, address contributor) public view returns (bool){
return _state.refundIsClaimed[saleId][tokenIndex][contributor];
}
function allocationIsClaimed(uint saleId, uint tokenIndex, address contributor) public view returns (bool){
function allocationIsClaimed(uint256 saleId, uint256 tokenIndex, address contributor) public view returns (bool){
return _state.allocationIsClaimed[saleId][tokenIndex][contributor];
}
}

View File

@ -43,36 +43,36 @@ contract ContributorSetters is ContributorState, Context {
_state.consistencyLevel = level;
}
function setSale(uint saleId, ContributorStructs.Sale memory sale) internal {
function setSale(uint256 saleId, ContributorStructs.Sale memory sale) internal {
_state.sales[saleId] = sale;
}
function setSaleContribution(uint saleId, address contributor, uint tokenIndex, uint contribution) internal {
function setSaleContribution(uint256 saleId, address contributor, uint256 tokenIndex, uint256 contribution) internal {
_state.contributions[saleId][tokenIndex][contributor] += contribution;
_state.totalContributions[saleId][tokenIndex] += contribution;
}
function setSaleSealed(uint saleId) internal {
function setSaleSealed(uint256 saleId) internal {
_state.sales[saleId].isSealed = true;
}
function setSaleAborted(uint saleId) internal {
function setSaleAborted(uint256 saleId) internal {
_state.sales[saleId].isAborted = true;
}
function setRefundClaimed(uint saleId, uint tokenIndex, address contributor) internal {
function setRefundClaimed(uint256 saleId, uint256 tokenIndex, address contributor) internal {
_state.refundIsClaimed[saleId][tokenIndex][contributor] = true;
}
function setAllocationClaimed(uint saleId, uint tokenIndex, address contributor) internal {
function setAllocationClaimed(uint256 saleId, uint256 tokenIndex, address contributor) internal {
_state.allocationIsClaimed[saleId][tokenIndex][contributor] = true;
}
function setSaleAllocation(uint saleId, uint tokenIndex, uint allocation) internal {
function setSaleAllocation(uint256 saleId, uint256 tokenIndex, uint256 allocation) internal {
_state.sales[saleId].allocations[tokenIndex] = allocation;
}
function setExcessContribution(uint saleId, uint tokenIndex, uint excessContribution) internal {
function setExcessContribution(uint256 saleId, uint256 tokenIndex, uint256 excessContribution) internal {
_state.sales[saleId].excessContributions[tokenIndex] = excessContribution;
}
}

View File

@ -28,19 +28,19 @@ contract ContributorStorage {
mapping(address => bool) initializedImplementations;
/// mapping of Sales
mapping(uint => ContributorStructs.Sale) sales;
mapping(uint256 => ContributorStructs.Sale) sales;
/// sale id > token id > contributor > contribution
mapping(uint => mapping(uint => mapping(address => uint))) contributions;
mapping(uint256 => mapping(uint256 => mapping(address => uint256))) contributions;
/// sale id > token id > contribution
mapping(uint => mapping(uint => uint)) totalContributions;
mapping(uint256 => mapping(uint256 => uint256)) totalContributions;
/// sale id > token id > contributor > isClaimed
mapping(uint => mapping(uint => mapping(address => bool))) allocationIsClaimed;
mapping(uint256 => mapping(uint256 => mapping(address => bool))) allocationIsClaimed;
/// sale id > [token id > contributor > isClaimed
mapping(uint => mapping(uint => mapping(address => bool))) refundIsClaimed;
mapping(uint256 => mapping(uint256 => mapping(address => bool))) refundIsClaimed;
}
}

View File

@ -14,6 +14,11 @@ library ICCOStructs {
uint128 conversionRate;
}
struct SolanaToken {
uint8 tokenIndex;
bytes32 tokenAddress;
}
struct Contribution {
/// index in acceptedTokens array
uint8 tokenIndex;
@ -83,6 +88,27 @@ library ICCOStructs {
bytes32 refundRecipient;
}
struct SolanaSaleInit {
/// payloadID uint8 = 5
uint8 payloadID;
/// sale ID
uint256 saleID;
/// sale token ATA for solana
bytes32 solanaTokenAccount;
/// chain ID of the token
uint16 tokenChain;
/// token decimals
uint8 tokenDecimals;
/// timestamp raise start
uint256 saleStart;
/// timestamp raise end
uint256 saleEnd;
/// accepted Tokens
SolanaToken[] acceptedTokens;
/// recipient of proceeds
bytes32 recipient;
}
struct ContributionsSealed {
/// payloadID uint8 = 2
uint8 payloadID;
@ -143,8 +169,22 @@ library ICCOStructs {
);
}
function encodeSolanaSaleInit(SolanaSaleInit memory solanaSaleInit) public pure returns (bytes memory encoded) {
return abi.encodePacked(
uint8(5),
solanaSaleInit.saleID,
solanaSaleInit.solanaTokenAccount,
solanaSaleInit.tokenChain,
solanaSaleInit.tokenDecimals,
solanaSaleInit.saleStart,
solanaSaleInit.saleEnd,
encodeSolanaTokens(solanaSaleInit.acceptedTokens),
solanaSaleInit.recipient
);
}
function parseSaleInit(bytes memory encoded) public pure returns (SaleInit memory saleInit) {
uint index = 0;
uint256 index = 0;
saleInit.payloadID = encoded.toUint8(index);
index += 1;
@ -178,7 +218,7 @@ library ICCOStructs {
saleInit.saleEnd = encoded.toUint256(index);
index += 32;
uint len = 1 + 50 * uint256(uint8(encoded[index]));
uint256 len = 1 + 50 * uint256(uint8(encoded[index]));
saleInit.acceptedTokens = parseTokens(encoded.slice(index, len));
index += len;
@ -196,7 +236,7 @@ library ICCOStructs {
function encodeTokens(Token[] memory tokens) public pure returns (bytes memory encoded) {
encoded = abi.encodePacked(uint8(tokens.length));
for (uint i = 0; i < tokens.length; i++) {
for (uint256 i = 0; i < tokens.length; i++) {
encoded = abi.encodePacked(
encoded,
tokens[i].tokenAddress,
@ -206,6 +246,17 @@ library ICCOStructs {
}
}
function encodeSolanaTokens(SolanaToken[] memory tokens) public pure returns (bytes memory encoded) {
encoded = abi.encodePacked(uint8(tokens.length));
for (uint256 i = 0; i < tokens.length; i++) {
encoded = abi.encodePacked(
encoded,
tokens[i].tokenIndex,
tokens[i].tokenAddress
);
}
}
function parseTokens(bytes memory encoded) public pure returns (Token[] memory tokens) {
require(encoded.length % 50 == 1, "invalid Token[]");
@ -213,7 +264,7 @@ library ICCOStructs {
tokens = new Token[](len);
for (uint i = 0; i < len; i++) {
for (uint256 i = 0; i < len; i++) {
tokens[i].tokenAddress = encoded.toBytes32( 1 + i * 50);
tokens[i].tokenChain = encoded.toUint16( 33 + i * 50);
tokens[i].conversionRate = encoded.toUint128(35 + i * 50);
@ -230,7 +281,7 @@ library ICCOStructs {
}
function parseContributionsSealed(bytes memory encoded) public pure returns (ContributionsSealed memory consSealed) {
uint index = 0;
uint256 index = 0;
consSealed.payloadID = encoded.toUint8(index);
index += 1;
@ -243,7 +294,7 @@ library ICCOStructs {
consSealed.chainID = encoded.toUint16(index);
index += 2;
uint len = 1 + 33 * uint256(uint8(encoded[index]));
uint256 len = 1 + 33 * uint256(uint8(encoded[index]));
consSealed.contributions = parseContributions(encoded.slice(index, len));
index += len;
@ -252,7 +303,7 @@ library ICCOStructs {
function encodeContributions(Contribution[] memory contributions) public pure returns (bytes memory encoded) {
encoded = abi.encodePacked(uint8(contributions.length));
for (uint i = 0; i < contributions.length; i++) {
for (uint256 i = 0; i < contributions.length; i++) {
encoded = abi.encodePacked(
encoded,
contributions[i].tokenIndex,
@ -268,7 +319,7 @@ library ICCOStructs {
cons = new Contribution[](len);
for (uint i = 0; i < len; i++) {
for (uint256 i = 0; i < len; i++) {
cons[i].tokenIndex = encoded.toUint8(1 + i * 33);
cons[i].contributed = encoded.toUint256(2 + i * 33);
}
@ -283,7 +334,7 @@ library ICCOStructs {
}
function parseSaleSealed(bytes memory encoded) public pure returns (SaleSealed memory ss) {
uint index = 0;
uint256 index = 0;
ss.payloadID = encoded.toUint8(index);
index += 1;
@ -292,7 +343,7 @@ library ICCOStructs {
ss.saleID = encoded.toUint256(index);
index += 32;
uint len = 1 + 65 * uint256(uint8(encoded[index]));
uint256 len = 1 + 65 * uint256(uint8(encoded[index]));
ss.allocations = parseAllocations(encoded.slice(index, len));
index += len;
@ -301,7 +352,7 @@ library ICCOStructs {
function encodeAllocations(Allocation[] memory allocations) public pure returns (bytes memory encoded) {
encoded = abi.encodePacked(uint8(allocations.length));
for (uint i = 0; i < allocations.length; i++) {
for (uint256 i = 0; i < allocations.length; i++) {
encoded = abi.encodePacked(
encoded,
allocations[i].tokenIndex,
@ -318,7 +369,7 @@ library ICCOStructs {
allos = new Allocation[](len);
for (uint i = 0; i < len; i++) {
for (uint256 i = 0; i < len; i++) {
allos[i].tokenIndex = encoded.toUint8(1 + i * 65);
allos[i].allocation = encoded.toUint256(2 + i * 65);
allos[i].excessContribution = encoded.toUint256(34 + i * 65);
@ -330,7 +381,7 @@ library ICCOStructs {
}
function parseSaleAborted(bytes memory encoded) public pure returns (SaleAborted memory sa) {
uint index = 0;
uint256 index = 0;
sa.payloadID = encoded.toUint8(index);
index += 1;

View File

@ -12,6 +12,7 @@ module.exports = {
tokenBridge: "0x3ee18B2214AFF97000D974cf647E7C347E8fa585",
mnemonic: "",
rpc: "",
deployImplementationOnly: false,
},
binance: {
conductorChainId: 2,
@ -22,6 +23,7 @@ module.exports = {
tokenBridge: "0xB6F6D86a8f9879A9c87f643768d9efc38c1Da6E7",
mnemonic: "",
rpc: "",
deployImplementationOnly: false,
},
polygon: {
conductorChainId: 2,
@ -32,6 +34,7 @@ module.exports = {
tokenBridge: "0x5a58505a96D1dbf8dF91cB21B54419FC36e93fdE",
mnemonic: "",
rpc: "",
deployImplementationOnly: false,
},
avalanche: {
conductorChainId: 2,
@ -42,6 +45,7 @@ module.exports = {
tokenBridge: "0x0e082F06FF657D94310cB8cE8B0D9a04541d8052",
mnemonic: "",
rpc: "",
deployImplementationOnly: false,
},
development: {
conductorChainId: 2,
@ -76,6 +80,7 @@ module.exports = {
tokenBridge: "0xF890982f9310df57d00f659cf4fd87e65adEd8d7",
mnemonic: "",
rpc: "",
deployImplementationOnly: false,
},
fuji: {
conductorChainId: 2,
@ -86,6 +91,7 @@ module.exports = {
tokenBridge: "0x61E44E506Ca5659E6c0bba9b678586fA2d729756",
mnemonic: "",
rpc: "",
deployImplementationOnly: false,
},
binance_testnet: {
conductorChainId: 2,
@ -96,6 +102,7 @@ module.exports = {
tokenBridge: "0x9dcF9D205C9De35334D646BeE44b2D2859712A09",
mnemonic: "",
rpc: "",
deployImplementationOnly: false,
},
mumbai: {
conductorChainId: 2,
@ -106,6 +113,7 @@ module.exports = {
tokenBridge: "0x377D55a7928c046E18eEbb61977e714d2a76472a",
mnemonic: "",
rpc: "",
deployImplementationOnly: false,
},
fantom_testnet: {
conductorChainId: 2,
@ -116,5 +124,6 @@ module.exports = {
tokenBridge: "0x599CEa2204B4FaECd584Ab1F2b6aCA137a0afbE8",
mnemonic: "",
rpc: "",
deployImplementationOnly: false,
},
};

View File

@ -9,7 +9,6 @@ const DeploymentConfig = require(`${ethereumRootPath}/icco_deployment_config.js`
const fs = require("fs");
module.exports = async function(deployer, network) {
console.log(network);
const config = DeploymentConfig[network];
if (!config) {
throw Error("deployment config undefined");
@ -22,30 +21,32 @@ module.exports = async function(deployer, network) {
// deploy conductor implementation
await deployer.deploy(ConductorImplementation);
// deploy conductor setup
await deployer.deploy(ConductorSetup);
if (!config.deployImplementationOnly) {
// deploy conductor setup
await deployer.deploy(ConductorSetup);
// encode initialisation data
const conductorSetup = new web3.eth.Contract(
ConductorSetup.abi,
ConductorSetup.address
);
const conductorInitData = conductorSetup.methods
.setup(
ConductorImplementation.address,
config.conductorChainId,
config.wormhole,
config.tokenBridge,
config.consistencyLevel
)
.encodeABI();
// encode initialisation data
const conductorSetup = new web3.eth.Contract(
ConductorSetup.abi,
ConductorSetup.address
);
const conductorInitData = conductorSetup.methods
.setup(
ConductorImplementation.address,
config.conductorChainId,
config.wormhole,
config.tokenBridge,
config.consistencyLevel
)
.encodeABI();
// deploy conductor proxy
await deployer.deploy(
TokenSaleConductor,
ConductorSetup.address,
conductorInitData
);
// deploy conductor proxy
await deployer.deploy(
TokenSaleConductor,
ConductorSetup.address,
conductorInitData
);
}
// cache address depending on whether contract
// has been deployed to mainnet, testnet or devnet
@ -73,12 +74,14 @@ module.exports = async function(deployer, network) {
const contents = fs.existsSync(fp)
? JSON.parse(fs.readFileSync(fp, "utf8"))
: {};
contents.conductorAddress = TokenSaleConductor.address;
contents.conductorChain = parseInt(config.conductorChainId);
if (!config.deployImplementationOnly) {
contents.conductorAddress = TokenSaleConductor.address;
contents.conductorChain = parseInt(config.conductorChainId);
} else {
const implementationString = network.concat("ConductorImplementation");
contents[implementationString] = ConductorImplementation.address;
}
fs.writeFileSync(fp, JSON.stringify(contents, null, 2), "utf8");
}
// TODO: mainnet
if (network == "mainnet") {
}
};

View File

@ -23,15 +23,6 @@ module.exports = async function(deployer, network) {
// deploy contributor implementation
await deployer.deploy(ContributorImplementation);
// deploy contributor setup
await deployer.deploy(ContributorSetup);
// encode initialisation data
const contributorSetup = new web3.eth.Contract(
ContributorSetup.abi,
ContributorSetup.address
);
// figure out which conductor address to use
let conductorAddr = undefined;
if (network == "development") {
@ -61,25 +52,36 @@ module.exports = async function(deployer, network) {
throw Error("conductorAddr is undefined");
}
const contributorInitData = contributorSetup.methods
.setup(
ContributorImplementation.address,
config.contributorChainId,
config.conductorChainId,
conductorAddr,
config.authority,
config.wormhole,
config.tokenBridge,
config.consistencyLevel
)
.encodeABI();
if (!config.deployImplementationOnly) {
// deploy contributor setup
await deployer.deploy(ContributorSetup);
// deploy conductor proxy
await deployer.deploy(
TokenSaleContributor,
ContributorSetup.address,
contributorInitData
);
// encode initialisation data
const contributorSetup = new web3.eth.Contract(
ContributorSetup.abi,
ContributorSetup.address
);
const contributorInitData = contributorSetup.methods
.setup(
ContributorImplementation.address,
config.contributorChainId,
config.conductorChainId,
conductorAddr,
config.authority,
config.wormhole,
config.tokenBridge,
config.consistencyLevel
)
.encodeABI();
// deploy conductor proxy
await deployer.deploy(
TokenSaleContributor,
ContributorSetup.address,
contributorInitData
);
}
// cache address for registration purposes
{
@ -108,7 +110,14 @@ module.exports = async function(deployer, network) {
if (network == "eth_devnet" || network == "eth_devnet2") {
contents[addrName] = TokenSaleContributor.address;
} else {
contents[network] = TokenSaleContributor.address;
if (!config.deployImplementationOnly) {
contents[network] = TokenSaleContributor.address;
} else {
const implementationString = network.concat(
"ContributorImplementation"
);
contents[implementationString] = ContributorImplementation.address;
}
}
fs.writeFileSync(fp, JSON.stringify(contents, null, 2), "utf8");
}

File diff suppressed because it is too large Load Diff

View File

@ -32,6 +32,7 @@ export async function getSaleFromConductorOnEth(
acceptedTokensChains: sale.acceptedTokensChains,
acceptedTokensAddresses: sale.acceptedTokensAddresses,
acceptedTokensConversionRates: sale.acceptedTokensConversionRates,
solanaAcceptedTokensCount: sale.solanaAcceptedTokensCount,
contributions: sale.contributions,
contributionsCollected: sale.contributionsCollected,
isSealed: sale.isSealed,

View File

@ -7,7 +7,6 @@ import {
nativeToHexString,
getForeignAssetEth,
getOriginalAssetEth,
hexToNativeString,
uint8ArrayToNative,
} from "@certusone/wormhole-sdk";
import { parseUnits } from "ethers/lib/utils";

View File

@ -42,6 +42,7 @@ export interface ConductorSale extends Sale {
localTokenDecimals: number;
localTokenAddress: string;
solanaTokenAccount: ethers.BytesLike;
solanaAcceptedTokensCount: number;
contributions: ethers.BigNumberish[];
contributionsCollected: boolean[];
refundIsClaimed: boolean;

View File

@ -12,6 +12,9 @@ export const CONTRIBUTOR_INFO = JSON.parse(
fs.readFileSync(`${__dirname}/../cfg/contributors.json`, "utf8")
);
// VAA fetching params
export const RETRY_TIMEOUT_SECONDS = 180;
// deployment info for the sale
export const CONDUCTOR_ADDRESS = TESTNET_ADDRESSES.conductorAddress;
export const CONDUCTOR_CHAIN_ID = TESTNET_ADDRESSES.conductorChain;

View File

@ -14,6 +14,7 @@ import {
abortSaleEarlyAtConductor,
abortSaleEarlyAtContributor,
testProvider,
abortSaleAtContributors,
} from "./utils";
import {
SALE_CONFIG,
@ -21,10 +22,14 @@ import {
CONDUCTOR_NETWORK,
CONTRIBUTOR_INFO,
CONTRIBUTOR_NETWORKS,
CONDUCTOR_ADDRESS,
} from "./consts";
import { Contribution, saleParams, SealSaleResult } from "./structs";
import { setDefaultWasm } from "@certusone/wormhole-sdk";
import { getSaleFromContributorOnEth } from "wormhole-icco-sdk";
import {
getSaleFromConductorOnEth,
getSaleFromContributorOnEth,
} from "wormhole-icco-sdk";
setDefaultWasm("node");
@ -49,6 +54,8 @@ async function main() {
console.info("Sale", saleInit.saleId, "has been initialized.");
// test aborting the sale early
let saleTerminatedEarly = false;
if (SALE_CONFIG["testParams"].abortSaleEarly) {
console.log("Aborting sale early on the Conductor.");
// abort the sale early in the conductor
@ -57,6 +64,61 @@ async function main() {
console.log("Aborting sale early on the Contributors.");
await abortSaleEarlyAtContributor(saleInit, abortEarlyReceipt);
saleTerminatedEarly = true;
}
// continue with the sale if it wasn't aborted early
let saleResult: SealSaleResult;
let successfulContributions: Contribution[] = [];
if (!saleTerminatedEarly) {
// wait for the sale to start before contributing
console.info("Waiting for the sale to start...");
const extraTime: number = 5; // wait an extra 5 seconds
await waitForSaleToStart(saleInit, extraTime);
// loop through contributors and safe contribute one by one
const contributions: Contribution[] = CONTRIBUTOR_INFO["contributions"];
for (let i = 0; i < contributions.length; i++) {
const successful = await prepareAndExecuteContribution(
saleInit.saleId,
raiseParams.token,
contributions[i]
);
if (successful) {
console.info("Contribution successful for contribution:", i);
successfulContributions.push(contributions[i]);
} else {
console.log("Contribution failed for contribution:", i);
}
}
// wait for sale to end
console.log("Waiting for the sale to end...");
await waitForSaleToEnd(saleInit, 10);
// attest contributions on each contributor and collect contributions in conductor
await attestAndCollectContributions(saleInit);
// seal the sale on the conductor contract
saleResult = await sealOrAbortSaleOnEth(saleInit);
console.log("Sale results have been finalized.");
} else {
console.log("Skipping contributions, the sale was aborted early!");
}
// check to see if the sale failed, abort and refund folks if so
const conductorSale = await getSaleFromConductorOnEth(
CONDUCTOR_ADDRESS,
testProvider(CONDUCTOR_NETWORK),
saleInit.saleId
);
if (conductorSale.isAborted || saleTerminatedEarly) {
// abort on the contributors if not saleTerminatedEarly
if (!saleTerminatedEarly) {
await abortSaleAtContributors(saleResult);
}
// confirm that the sale was aborted on each contributor
for (let i = 0; i < CONTRIBUTOR_NETWORKS.length; i++) {
let network = CONTRIBUTOR_NETWORKS[i];
@ -67,45 +129,13 @@ async function main() {
);
if (contributorSale.isAborted) {
console.log("Successfully aborted sale on contributor:", network);
} else {
console.log("Failed to abort the sale on contributor:", network);
}
}
return;
}
// wait for the sale to start before contributing
console.info("Waiting for the sale to start...");
const extraTime: number = 5; // wait an extra 5 seconds
await waitForSaleToStart(saleInit, extraTime);
// loop through contributors and safe contribute one by one
const successfulContributions: Contribution[] = [];
const contributions: Contribution[] = CONTRIBUTOR_INFO["contributions"];
for (let i = 0; i < contributions.length; i++) {
const successful = await prepareAndExecuteContribution(
saleInit.saleId,
raiseParams.token,
contributions[i]
);
if (successful) {
console.info("Contribution successful for contribution:", i);
successfulContributions.push(contributions[i]);
} else {
console.log("Contribution failed for contribution:", i);
}
}
// wait for sale to end
console.log("Waiting for the sale to end...");
await waitForSaleToEnd(saleInit, 10);
// attest contributions on each contributor and collect contributions in conductor
await attestAndCollectContributions(saleInit);
// seal the sale on the Conductor contract
const saleResult: SealSaleResult = await sealOrAbortSaleOnEth(saleInit);
console.log("Sale results have been finalized.");
// redeem the transfer VAAs on all chains
await redeemCrossChainAllocations(saleResult);

View File

@ -37,6 +37,7 @@ import {
nativeToUint8Array,
abortSaleBeforeStartOnEth,
saleAbortedOnEth,
getSaleIdFromIccoVaa,
} from "wormhole-icco-sdk";
import {
WORMHOLE_ADDRESSES,
@ -48,6 +49,7 @@ import {
KYC_AUTHORITY_KEY,
CHAIN_ID_TO_NETWORK,
CONDUCTOR_CHAIN_ID,
RETRY_TIMEOUT_SECONDS,
} from "./consts";
import {
TokenConfig,
@ -57,6 +59,7 @@ import {
SaleSealed,
} from "./structs";
import { signContribution } from "./kyc";
import { assert } from "console";
export async function extractVaaPayload(
signedVaa: Uint8Array
@ -152,6 +155,7 @@ export async function getSignedVaaFromSequence(
emitterAddress: string,
sequence: string
): Promise<Uint8Array> {
console.log("Searching for VAA with sequence:", sequence);
const result = await getSignedVAAWithRetry(
WORMHOLE_ADDRESSES.guardianRpc,
chainId,
@ -159,8 +163,10 @@ export async function getSignedVaaFromSequence(
sequence,
{
transport: NodeHttpTransport(),
}
},
RETRY_TIMEOUT_SECONDS
);
console.log("Found VAA for sequence:", sequence);
return result.vaaBytes;
}
@ -469,6 +475,7 @@ export async function attestAndCollectContributions(
signedVaas,
initiatorWallet(CONDUCTOR_NETWORK)
);
assert(receipts.length == signedVaas.length);
}
console.info("Finished collecting contributions.");
@ -496,11 +503,10 @@ export async function sealSaleAndParseReceiptOnEth(
saleId: ethers.BigNumberish,
coreBridgeAddress: string,
tokenBridgeAddress: string,
wormholeHosts: string[],
extraGrpcOpts: any = {},
wallet: ethers.Wallet
): Promise<SealSaleResult> {
const receipt = await sealSaleOnEth(conductorAddress, saleId, wallet);
console.log("Finished sealing the sale on the Conductor.");
const sale = await getSaleFromConductorOnEth(
conductorAddress,
@ -515,37 +521,35 @@ export async function sealSaleAndParseReceiptOnEth(
throw Error("no vaa sequences found");
}
const result = await getSignedVAAWithRetry(
wormholeHosts,
// fetch the VAA
const sealSaleVaa = await getSignedVaaFromSequence(
emitterChain,
getEmitterAddressEth(conductorAddress),
sealSaleSequence,
extraGrpcOpts
sealSaleSequence
);
const sealSaleVaa = result.vaaBytes;
console.log("Found the sealSale VAA emitted from the Conductor.");
// search for allocations
// doing it serially for ease of putting into the map
const mapped = new Map<ChainId, Uint8Array[]>();
for (const sequence of sequences) {
const result = await getSignedVAAWithRetry(
wormholeHosts,
emitterChain,
getEmitterAddressEth(tokenBridgeAddress),
sequence,
extraGrpcOpts
);
const signedVaa = result.vaaBytes;
const vaaPayload = await extractVaaPayload(signedVaa);
const chainId = await getTargetChainIdFromTransferVaa(vaaPayload);
if (sale.isSealed) {
for (const sequence of sequences) {
const signedVaa = await getSignedVaaFromSequence(
emitterChain,
getEmitterAddressEth(tokenBridgeAddress),
sequence
);
const vaaPayload = await extractVaaPayload(signedVaa);
const chainId = await getTargetChainIdFromTransferVaa(vaaPayload);
const signedVaas = mapped.get(chainId);
if (signedVaas === undefined) {
mapped.set(chainId, [signedVaa]);
} else {
signedVaas.push(signedVaa);
const signedVaas = mapped.get(chainId);
if (signedVaas === undefined) {
mapped.set(chainId, [signedVaa]);
} else {
signedVaas.push(signedVaa);
}
}
}
return {
sale: sale,
transferVaas: mapped,
@ -563,10 +567,6 @@ export async function sealOrAbortSaleOnEth(
saleId,
WORMHOLE_ADDRESSES[CONDUCTOR_NETWORK].wormhole,
WORMHOLE_ADDRESSES[CONDUCTOR_NETWORK].tokenBridge,
WORMHOLE_ADDRESSES.guardianRpc,
{
transport: NodeHttpTransport(),
},
initiatorWallet(CONDUCTOR_NETWORK)
);
}
@ -687,18 +687,13 @@ export async function redeemCrossChainContributions(
}
for (const sequence of sequences) {
const result = await getSignedVAAWithRetry(
WORMHOLE_ADDRESSES.guardianRpc,
const signedVaa = await getSignedVaaFromSequence(
emitterChain,
getEmitterAddressEth(
WORMHOLE_ADDRESSES[CHAIN_ID_TO_NETWORK.get(emitterChain)].tokenBridge
),
sequence,
{
transport: NodeHttpTransport(),
}
sequence
);
const signedVaa = result.vaaBytes;
const vaaPayload = await extractVaaPayload(signedVaa);
const chainId = await getTargetChainIdFromTransferVaa(vaaPayload);
const targetNetwork = CHAIN_ID_TO_NETWORK.get(chainId);
@ -758,3 +753,26 @@ export async function abortSaleEarlyAtContributor(
return;
}
export async function abortSaleAtContributors(saleResult: SealSaleResult) {
const signedVaa = saleResult.sealSaleVaa;
const vaaPayload = await extractVaaPayload(signedVaa);
const saleId = await getSaleIdFromIccoVaa(vaaPayload);
{
const receipts = await Promise.all(
CONTRIBUTOR_NETWORKS.map(
async (network): Promise<ethers.ContractReceipt> => {
return saleAbortedOnEth(
TESTNET_ADDRESSES[network],
signedVaa,
initiatorWallet(network),
saleId
);
}
)
);
}
return;
}

View File

@ -1,7 +1,8 @@
{
"gaurdianRpc": "https://wormhole-v2-testnet-api.certus.one",
"conductorAddress": "0x5c49f34D92316A2ac68d10A1e2168e16610e84f9",
"conductorAddress": "0xce121EA9c289390df7d812F83Ed6bE79A167DfE4",
"conductorChain": 2,
"goerli": "0xB6AF16A9B216c9eEd2AbA4452088FE28cc22d5Ff",
"fuji": "0xE60C9105BF114f198CE93F3A1aDf0FB09427C674"
}
"goerli": "0xFee98e512e4Ab409294a080b3C2B19748Fd54A6d",
"fuji": "0x72df3672A0E889ca8875080d414056fD4f7Aa9fA",
"solana_testnet": "8WCGVzrvGoLRrWY5V4L7iDCJym3wu6SiL3DCjGsSrP4k"
}

View File

@ -12,7 +12,8 @@
},
"include": [
"register_tilt_contributors.ts",
"register_testnet_contributors.ts"
"register_testnet_contributors.ts",
"upgrade_testnet_contracts.ts"
],
"exclude": ["node_modules"]
}

View File

@ -0,0 +1,79 @@
import yargs from "yargs";
import { ethers } from "ethers";
import { Conductor__factory, Contributor__factory } from "wormhole-icco-sdk";
const fs = require("fs");
const DeploymentConfig = require("../../ethereum/icco_deployment_config.js");
function parseArgs(): string[] {
const parsed = yargs(process.argv.slice(2))
.options("contractType", {
type: "string",
description: "Type of contract (e.g. conductor)",
require: true,
})
.options("network", {
type: "string",
description: "Network to deploy to (e.g. goerli)",
require: true,
})
.help("h")
.alias("h", "help").argv;
const args = [parsed.contractType, parsed.network];
return args;
}
async function main() {
const args = parseArgs();
// create checksum address
const contractType = args[0];
const network = args[1];
const config = DeploymentConfig[network];
if (!config) {
throw Error("deployment config undefined");
}
const testnet = JSON.parse(
fs.readFileSync(`${__dirname}/../../testnet.json`, "utf8")
);
// create wallet to call sdk method with
const provider = new ethers.providers.JsonRpcProvider(config.rpc);
const wallet: ethers.Wallet = new ethers.Wallet(config.mnemonic, provider);
// create the factory and grab the implementation address
let contractFactory;
let chainId;
let newImplementation;
if (contractType == "conductor") {
contractFactory = Conductor__factory.connect(
testnet["conductorAddress"],
wallet
);
chainId = config.conductorChainId;
newImplementation = testnet[network.concat("ConductorImplementation")];
} else {
contractFactory = Contributor__factory.connect(testnet[network], wallet);
chainId = config.contributorChainId;
newImplementation = testnet[network.concat("ContributorImplementation")];
}
// run the upgrade
const tx = await contractFactory.upgrade(chainId, newImplementation);
const receipt = await tx.wait();
console.log(
"transction:",
receipt.transactionHash,
", newImplementation:",
newImplementation
);
return;
}
main();