From 19910c723909510fceb39863fd4b470235e003e4 Mon Sep 17 00:00:00 2001 From: Drew <43194093+dsterioti@users.noreply.github.com> Date: Tue, 31 May 2022 10:00:32 -0500 Subject: [PATCH] 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 --- .../contracts/icco/conductor/Conductor.sol | 244 ++++-- .../icco/conductor/ConductorGetters.sol | 12 +- .../icco/conductor/ConductorSetters.sol | 12 +- .../icco/conductor/ConductorState.sol | 2 +- .../icco/conductor/ConductorStructs.sol | 3 +- .../icco/contributor/Contributor.sol | 36 +- .../icco/contributor/ContributorGetters.sol | 22 +- .../icco/contributor/ContributorSetters.sol | 16 +- .../icco/contributor/ContributorState.sol | 10 +- .../contracts/icco/shared/ICCOStructs.sol | 77 +- ethereum/icco_deployment_config.js.sample | 9 + .../migrations/2_deploy_icco_conductor.js | 61 +- .../migrations/3_deploy_icco_contributor.js | 65 +- ethereum/test/icco.js | 805 +++++++++++++++++- sdk/js/src/icco/getters.ts | 1 + sdk/js/src/icco/misc.ts | 1 - sdk/js/src/icco/structs.ts | 1 + test/testnet/src/consts.ts | 3 + test/testnet/src/run_testnet_sale.ts | 100 ++- test/testnet/src/utils.ts | 90 +- testnet.json | 9 +- tools/tsconfig.json | 3 +- tools/upgrade_testnet_contracts.ts | 79 ++ 23 files changed, 1352 insertions(+), 309 deletions(-) create mode 100644 tools/upgrade_testnet_contracts.ts diff --git a/ethereum/contracts/icco/conductor/Conductor.sol b/ethereum/contracts/icco/conductor/Conductor.sol index daa5df7d..d2116811 100644 --- a/ethereum/contracts/icco/conductor/Conductor.sol +++ b/ethereum/contracts/icco/conductor/Conductor.sol @@ -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()); } } \ No newline at end of file diff --git a/ethereum/contracts/icco/conductor/ConductorGetters.sol b/ethereum/contracts/icco/conductor/ConductorGetters.sol index 5ca60059..48510ea2 100644 --- a/ethereum/contracts/icco/conductor/ConductorGetters.sol +++ b/ethereum/contracts/icco/conductor/ConductorGetters.sol @@ -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; } } \ No newline at end of file diff --git a/ethereum/contracts/icco/conductor/ConductorSetters.sol b/ethereum/contracts/icco/conductor/ConductorSetters.sol index b91bf66b..ac6433ff 100644 --- a/ethereum/contracts/icco/conductor/ConductorSetters.sol +++ b/ethereum/contracts/icco/conductor/ConductorSetters.sol @@ -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; } } \ No newline at end of file diff --git a/ethereum/contracts/icco/conductor/ConductorState.sol b/ethereum/contracts/icco/conductor/ConductorState.sol index 456ba8cb..62e0fd96 100644 --- a/ethereum/contracts/icco/conductor/ConductorState.sol +++ b/ethereum/contracts/icco/conductor/ConductorState.sol @@ -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; diff --git a/ethereum/contracts/icco/conductor/ConductorStructs.sol b/ethereum/contracts/icco/conductor/ConductorStructs.sol index ac4c611d..546c8514 100644 --- a/ethereum/contracts/icco/conductor/ConductorStructs.sol +++ b/ethereum/contracts/icco/conductor/ConductorStructs.sol @@ -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; diff --git a/ethereum/contracts/icco/contributor/Contributor.sol b/ethereum/contracts/icco/contributor/Contributor.sol index e0a4194d..fb95d67f 100644 --- a/ethereum/contracts/icco/contributor/Contributor.sol +++ b/ethereum/contracts/icco/contributor/Contributor.sol @@ -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)); } } \ No newline at end of file diff --git a/ethereum/contracts/icco/contributor/ContributorGetters.sol b/ethereum/contracts/icco/contributor/ContributorGetters.sol index bfb11b9d..579d1078 100644 --- a/ethereum/contracts/icco/contributor/ContributorGetters.sol +++ b/ethereum/contracts/icco/contributor/ContributorGetters.sol @@ -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]; } } \ No newline at end of file diff --git a/ethereum/contracts/icco/contributor/ContributorSetters.sol b/ethereum/contracts/icco/contributor/ContributorSetters.sol index 458e279d..6df91739 100644 --- a/ethereum/contracts/icco/contributor/ContributorSetters.sol +++ b/ethereum/contracts/icco/contributor/ContributorSetters.sol @@ -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; } } \ No newline at end of file diff --git a/ethereum/contracts/icco/contributor/ContributorState.sol b/ethereum/contracts/icco/contributor/ContributorState.sol index e73157c4..994fc76c 100644 --- a/ethereum/contracts/icco/contributor/ContributorState.sol +++ b/ethereum/contracts/icco/contributor/ContributorState.sol @@ -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; } } diff --git a/ethereum/contracts/icco/shared/ICCOStructs.sol b/ethereum/contracts/icco/shared/ICCOStructs.sol index 54bbf320..8ea02ee2 100644 --- a/ethereum/contracts/icco/shared/ICCOStructs.sol +++ b/ethereum/contracts/icco/shared/ICCOStructs.sol @@ -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; diff --git a/ethereum/icco_deployment_config.js.sample b/ethereum/icco_deployment_config.js.sample index eb306983..8ff5ea65 100644 --- a/ethereum/icco_deployment_config.js.sample +++ b/ethereum/icco_deployment_config.js.sample @@ -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, }, }; diff --git a/ethereum/migrations/2_deploy_icco_conductor.js b/ethereum/migrations/2_deploy_icco_conductor.js index 4a0fff16..d5984c9e 100644 --- a/ethereum/migrations/2_deploy_icco_conductor.js +++ b/ethereum/migrations/2_deploy_icco_conductor.js @@ -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") { - } }; diff --git a/ethereum/migrations/3_deploy_icco_contributor.js b/ethereum/migrations/3_deploy_icco_contributor.js index 83bd75ee..36f4eef0 100644 --- a/ethereum/migrations/3_deploy_icco_contributor.js +++ b/ethereum/migrations/3_deploy_icco_contributor.js @@ -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"); } diff --git a/ethereum/test/icco.js b/ethereum/test/icco.js index 9a8d5c9d..f0ff55f4 100644 --- a/ethereum/test/icco.js +++ b/ethereum/test/icco.js @@ -1,6 +1,7 @@ const jsonfile = require("jsonfile"); const elliptic = require("elliptic"); const { assert } = require("chai"); +const ethers = require("ethers"); const TokenImplementation = artifacts.require("TokenImplementation"); @@ -43,6 +44,7 @@ const ContributorImplementationFullABI = jsonfile.readFileSync( ).abi; // global variables +const SOLANA_CHAIN_ID = "1"; const TEST_CHAIN_ID = "2"; const GAS_LIMIT = "3000000"; @@ -58,6 +60,59 @@ contract("ICCO", function(accounts) { const CONDUCTOR_BYTES32_ADDRESS = "0x000000000000000000000000" + TokenSaleConductor.address.substr(2); + const WORMHOLE_FEE = 1000; + + it("should set wormhole fee", async function() { + console.log( + "\n -------------------------- Set Wormhole Messaging Fee --------------------------" + ); + const timestamp = 1000; + const nonce = 1001; + const emitterChainId = "1"; + const emitterAddress = + "0x0000000000000000000000000000000000000000000000000000000000000004"; + const newMessageFee = WORMHOLE_FEE; + + data = [ + //Core + "0x" + + Buffer.from("Core") + .toString("hex") + .padStart(64, 0), + // Action 3 (Set Message Fee) + "03", + // ChainID + web3.eth.abi.encodeParameter("uint16", "2").substring(2 + (64 - 4)), + // Message Fee + web3.eth.abi.encodeParameter("uint256", newMessageFee).substring(2), + ].join(""); + + const vm = await signAndEncodeVM( + timestamp, + nonce, + emitterChainId, + emitterAddress, + 0, + data, + [testSigner1PK], + 0, + 2 + ); + + let before = await WORMHOLE.methods.messageFee().call(); + + await WORMHOLE.methods.submitSetMessageFee("0x" + vm).send({ + value: 0, + from: accounts[0], + gasLimit: 1000000, + }); + + let after = await WORMHOLE.methods.messageFee().call(); + + assert.notEqual(before, after); + assert.equal(after, newMessageFee); + }); + it("conductor should be initialized with the correct values", async function() { console.log( "\n -------------------------- Initialization and Upgrades --------------------------" @@ -152,7 +207,7 @@ contract("ICCO", function(accounts) { assert.ok(failed); - await initialized.methods + const tx = await initialized.methods .registerChain(TEST_CHAIN_ID, contributorAddress) .send({ value: 0, @@ -228,11 +283,13 @@ contract("ICCO", function(accounts) { ConductorImplementation.address.toLowerCase() ); - await initialized.methods.upgrade(TEST_CHAIN_ID, mock.address).send({ - value: 0, - from: accounts[0], - gasLimit: GAS_LIMIT, - }); + const upgradeTx = await initialized.methods + .upgrade(TEST_CHAIN_ID, mock.address) + .send({ + value: 0, + from: accounts[0], + gasLimit: GAS_LIMIT, + }); let after = await web3.eth.getStorageAt( TokenSaleConductor.address, @@ -241,6 +298,15 @@ contract("ICCO", function(accounts) { assert.equal(after.toLowerCase(), mock.address.toLowerCase()); + // confirm that the ContractUpgraded event is emitted + let eventOutput = upgradeTx["events"]["ContractUpgraded"]["returnValues"]; + + assert.equal( + eventOutput["oldContract"].toLowerCase(), + before.toLowerCase() + ); + assert.equal(eventOutput["newContract"].toLowerCase(), after.toLowerCase()); + const mockImpl = new web3.eth.Contract( MockConductorImplementation.abi, TokenSaleConductor.address @@ -292,11 +358,13 @@ contract("ICCO", function(accounts) { ContributorImplementation.address.toLowerCase() ); - await initialized.methods.upgrade(TEST_CHAIN_ID, mock.address).send({ - value: 0, - from: accounts[0], - gasLimit: GAS_LIMIT, - }); + const upgradeTx = await initialized.methods + .upgrade(TEST_CHAIN_ID, mock.address) + .send({ + value: 0, + from: accounts[0], + gasLimit: GAS_LIMIT, + }); let after = await web3.eth.getStorageAt( TokenSaleContributor.address, @@ -305,6 +373,15 @@ contract("ICCO", function(accounts) { assert.equal(after.toLowerCase(), mock.address.toLowerCase()); + // confirm that the ContractUpgraded event is emitted + let eventOutput = upgradeTx["events"]["ContractUpgraded"]["returnValues"]; + + assert.equal( + eventOutput["oldContract"].toLowerCase(), + before.toLowerCase() + ); + assert.equal(eventOutput["newContract"].toLowerCase(), after.toLowerCase()); + const mockImpl = new web3.eth.Contract( MockContributorImplementation.abi, TokenSaleContributor.address @@ -328,7 +405,7 @@ contract("ICCO", function(accounts) { ); // update the kyc authority - await initialized.methods + const updateTx = await initialized.methods .updateAuthority(TEST_CHAIN_ID, newAuthority) .send({ value: "0", @@ -343,6 +420,18 @@ contract("ICCO", function(accounts) { assert.equal(contributorAuthorityAfterUpdate, newAuthority); + // confirm that the AuthorityUpdated event is emitted + let eventOutput = updateTx["events"]["AuthorityUpdated"]["returnValues"]; + + assert.equal( + eventOutput["oldAuthority"].toLowerCase(), + currentAuthority.toLowerCase() + ); + assert.equal( + eventOutput["newAuthority"].toLowerCase(), + newAuthority.toLowerCase() + ); + // make sure only the Contributor owner can change authority let failed = false; try { @@ -388,7 +477,7 @@ contract("ICCO", function(accounts) { ); // update the consistency level - await contributorContract.methods + const contributorTx = await contributorContract.methods .updateConsistencyLevel(TEST_CHAIN_ID, updatedConsistencyLevel) .send({ value: "0", @@ -396,7 +485,7 @@ contract("ICCO", function(accounts) { gasLimit: GAS_LIMIT, }); - await conductorContract.methods + const conductorTx = await conductorContract.methods .updateConsistencyLevel(TEST_CHAIN_ID, updatedConsistencyLevel) .send({ value: "0", @@ -415,6 +504,23 @@ contract("ICCO", function(accounts) { assert.equal(contributorConsistencyLevelAfter, updatedConsistencyLevel); assert.equal(conductorConsistencyLevelAfter, updatedConsistencyLevel); + // confirm that the ConsistencyLevelUpdate event is emitted for contributor + let contributorEventOutput = + contributorTx["events"]["ConsistencyLevelUpdated"]["returnValues"]; + + assert.equal( + contributorEventOutput["oldLevel"], + initializedConsistencyLevel + ); + assert.equal(contributorEventOutput["newLevel"], updatedConsistencyLevel); + + // confirm that the ConsistencyLevelUpdate event is emitted for conductor + let conductorEventOutput = + conductorTx["events"]["ConsistencyLevelUpdated"]["returnValues"]; + + assert.equal(conductorEventOutput["oldLevel"], initializedConsistencyLevel); + assert.equal(conductorEventOutput["newLevel"], updatedConsistencyLevel); + // revert consistencyLevel back to initialized value // update the consistency level await contributorContract.methods @@ -488,7 +594,7 @@ contract("ICCO", function(accounts) { ); // transfer ownership - await contributorContract.methods + const contributorTx = await contributorContract.methods .transferOwnership(TEST_CHAIN_ID, newOwner) .send({ value: "0", @@ -496,7 +602,7 @@ contract("ICCO", function(accounts) { gasLimit: GAS_LIMIT, }); - await conductorContract.methods + const conductorTx = await conductorContract.methods .transferOwnership(TEST_CHAIN_ID, newOwner) .send({ value: "0", @@ -511,6 +617,32 @@ contract("ICCO", function(accounts) { assert.equal(contributorOwner, newOwner); assert.equal(conductorOwner, newOwner); + // confirm that the ConsistencyLevelUpdate event is emitted for contributor + let contributorEventOutput = + contributorTx["events"]["OwnershipTransfered"]["returnValues"]; + + assert.equal( + contributorEventOutput["oldOwner"].toLowerCase(), + currentOwner.toLowerCase() + ); + assert.equal( + contributorEventOutput["newOwner"].toLowerCase(), + newOwner.toLowerCase() + ); + + // confirm that the ConsistencyLevelUpdate event is emitted for conductor + let conductorEventOutput = + conductorTx["events"]["OwnershipTransfered"]["returnValues"]; + + assert.equal( + conductorEventOutput["oldOwner"].toLowerCase(), + currentOwner.toLowerCase() + ); + assert.equal( + conductorEventOutput["newOwner"].toLowerCase(), + newOwner.toLowerCase() + ); + // make sure only the owner can transfer ownership let contributorFailed = false; try { @@ -677,6 +809,7 @@ contract("ICCO", function(accounts) { const acceptedTokenLength = 2; const payloadIdType1 = "01"; const solanaChainId = "1"; + const numAcceptedSolanaTokens = "0"; const initialized = new web3.eth.Contract( ConductorImplementationFullABI, @@ -715,7 +848,7 @@ contract("ICCO", function(accounts) { // create the sale await initialized.methods.createSale(saleParams, acceptedTokens).send({ - value: "0", + value: WORMHOLE_FEE, from: SELLER, gasLimit: GAS_LIMIT, }); @@ -899,6 +1032,9 @@ contract("ICCO", function(accounts) { const sale = await initialized.methods.sales(SALE_ID).call(); assert.equal(SOLD_TOKEN.address, sale.localTokenAddress); + + // confirm that we are not accepting any solana tokens + assert.equal(sale.solanaAcceptedTokensCount, numAcceptedSolanaTokens); }); let INIT_SALE_VM; @@ -1374,6 +1510,7 @@ contract("ICCO", function(accounts) { // attest contributions await initialized.methods.attestContributions(SALE_ID).send({ from: BUYER_ONE, + value: WORMHOLE_FEE, gasLimit: GAS_LIMIT, }); @@ -1598,6 +1735,7 @@ contract("ICCO", function(accounts) { // seal the sale await initialized.methods.sealSale(SALE_ID).send({ + value: WORMHOLE_FEE, from: SELLER, gasLimit: GAS_LIMIT, }); @@ -1914,7 +2052,7 @@ contract("ICCO", function(accounts) { // create a second sale await initialized.methods.createSale(saleParams, acceptedTokens).send({ - value: "0", + value: WORMHOLE_FEE, from: SELLER, gasLimit: GAS_LIMIT, }); @@ -2400,6 +2538,7 @@ contract("ICCO", function(accounts) { // attest contributions await initialized.methods.attestContributions(SALE_2_ID).send({ from: BUYER_ONE, + value: WORMHOLE_FEE, gasLimit: GAS_LIMIT, }); @@ -2558,6 +2697,7 @@ contract("ICCO", function(accounts) { await initialized.methods.sealSale(SALE_2_ID).send({ from: SELLER, + value: WORMHOLE_FEE, gasLimit: GAS_LIMIT, }); @@ -2950,7 +3090,7 @@ contract("ICCO", function(accounts) { // create a third sale await initialized.methods.createSale(saleParams, acceptedTokens).send({ - value: "0", + value: WORMHOLE_FEE, from: SELLER, gasLimit: GAS_LIMIT, }); @@ -3279,6 +3419,7 @@ contract("ICCO", function(accounts) { try { await initialized.methods.abortSaleBeforeStartTime(SALE_3_ID).send({ from: BUYER_ONE, + value: WORMHOLE_FEE, gasLimit: GAS_LIMIT, }); } catch (e) { @@ -3294,6 +3435,7 @@ contract("ICCO", function(accounts) { // abort the sale await initialized.methods.abortSaleBeforeStartTime(SALE_3_ID).send({ from: SELLER, // must be the sale initiator (msg.sender in createSale()) + value: WORMHOLE_FEE, gasLimit: GAS_LIMIT, }); @@ -3754,7 +3896,7 @@ contract("ICCO", function(accounts) { // create the sale await initialized.methods.createSale(saleParams, acceptedTokens).send({ - value: "0", + value: WORMHOLE_FEE, from: SELLER, gasLimit: GAS_LIMIT, }); @@ -4242,6 +4384,7 @@ contract("ICCO", function(accounts) { // attest contributions await initialized.methods.attestContributions(SALE_4_ID).send({ from: BUYER_ONE, + value: WORMHOLE_FEE, gasLimit: GAS_LIMIT, }); @@ -4403,6 +4546,7 @@ contract("ICCO", function(accounts) { // seal the sale await initialized.methods.sealSale(SALE_4_ID).send({ from: SELLER, + value: WORMHOLE_FEE, gasLimit: GAS_LIMIT, }); @@ -4665,6 +4809,528 @@ contract("ICCO", function(accounts) { assert.ok(isAllocationClaimedBuyerTwoAfter); }); + // more global sale test variables + let SALE_5_START; + let SALE_5_END; + let SALE_5_ID; + let SOLANA_TOKEN_INDEX_ONE; + let SOLANA_TOKEN_INDEX_TWO; + let ETH_TOKEN_INDEX; + + it("create a fifth sale correctly and attest over wormhole", async function() { + console.log( + "\n -------------------------- Sale Test #5 (Sale With Solana Token) --------------------------" + ); + + // test variables + const current_block = await web3.eth.getBlock("latest"); + SALE_5_START = current_block.timestamp + 5; + SALE_5_END = SALE_5_START + 8; + + const saleTokenAmount = "10000000000000"; + const minimumTokenRaise = "2000"; + const maximumTokenRaise = "6000"; + const tokenOneConversionRate = "1000000000000000000"; + const tokenTwoConversionRate = "2000000000000000000"; + const saleRecipient = accounts[0]; + const refundRecipient = accounts[0]; + const payloadIdType5 = "05"; + const solanaAcceptedTokensLength = 2; + SOLANA_TOKEN_INDEX_ONE = "01"; + SOLANA_TOKEN_INDEX_TWO = "02"; + ETH_TOKEN_INDEX = "00"; + + // mint some more sale tokens + await SOLD_TOKEN.mint(SELLER, saleTokenAmount); + await SOLD_TOKEN.approve(TokenSaleConductor.address, saleTokenAmount); + + const initialized = new web3.eth.Contract( + ConductorImplementationFullABI, + TokenSaleConductor.address + ); + + // need to register a contributor contract + const solanaContributorAddress = web3.eth.abi.encodeParameter( + "bytes32", + "0x000000000000000000000000" + TokenSaleContributor.address.substr(2) + ); + + await initialized.methods.registerChain(1, solanaContributorAddress).send({ + value: 0, + from: accounts[0], + gasLimit: GAS_LIMIT, + }); + + // create array (struct) for sale params + const saleParams = [ + SOLD_TOKEN_BYTES32_ADDRESS, + TEST_CHAIN_ID, + saleTokenAmount, + minimumTokenRaise, + maximumTokenRaise, + SALE_5_START, + SALE_5_END, + saleRecipient, + refundRecipient, + SOLD_TOKEN_BYTES32_ADDRESS, + ]; + + // make sure createSale fails when trying to pass more than 8 tokens + { + let testAcceptedTokens = []; + + // add 7 more tokens + for (let i = 0; i < 9; i++) { + let defaultToken = [ + SOLANA_CHAIN_ID, + "0x000000000000000000000000" + accounts[i].substr(2), // placeholder address + tokenOneConversionRate, + ]; + testAcceptedTokens.push(defaultToken); + } + + let failed = false; + try { + // attest contributions + await initialized.methods + .createSale(saleParams, testAcceptedTokens) + .send({ + value: WORMHOLE_FEE * 2, + from: SELLER, + gasLimit: GAS_LIMIT, + }); + } catch (e) { + assert.equal( + e.message, + "Returned error: VM Exception while processing transaction: revert too many solana tokens" + ); + failed = true; + } + assert.ok(failed); + } + + // create accepted tokens array + const acceptedTokens = [ + [ + TEST_CHAIN_ID, + "0x000000000000000000000000" + CONTRIBUTED_TOKEN_TWO.address.substr(2), + tokenTwoConversionRate, + ], + [ + SOLANA_CHAIN_ID, + "0x000000000000000000000000" + CONTRIBUTED_TOKEN_ONE.address.substr(2), // placeholder address + tokenOneConversionRate, + ], + [ + SOLANA_CHAIN_ID, + "0x000000000000000000000000" + CONTRIBUTED_TOKEN_TWO.address.substr(2), // placeholder address + tokenTwoConversionRate, + ], + ]; + + // create the sale + await initialized.methods.createSale(saleParams, acceptedTokens).send({ + value: WORMHOLE_FEE * 2, + from: SELLER, + gasLimit: GAS_LIMIT, + }); + + // Verify Solana Payload sent to contributor + const log = ( + await WORMHOLE.getPastEvents("LogMessagePublished", { + fromBlock: "latest", + }) + )[1].returnValues; + + // verify payload + assert.equal(log.sender, TokenSaleConductor.address); + + // payload id + let index = 2; + assert.equal(log.payload.substr(index, 2), payloadIdType5); + index += 2; + + // sale id + SALE_5_ID = SALE_4_ID + 1; + assert.equal(parseInt(log.payload.substr(index, 64), 16), SALE_5_ID); + index += 64; + + // solana ATA for sale token + assert.equal( + log.payload.substr(index, 64), + web3.eth.abi.encodeParameter("address", SOLD_TOKEN.address).substring(2) + ); + index += 64; + + // token chain + assert.equal( + log.payload.substr(index, 4), + web3.eth.abi + .encodeParameter("uint16", TEST_CHAIN_ID) + .substring(2 + 64 - 4) + ); + index += 4; + + // token decimals + assert.equal( + parseInt(log.payload.substr(index, 2), 16), + SOLD_TOKEN_DECIMALS + ); + index += 2; + + // timestamp start + assert.equal(parseInt(log.payload.substr(index, 64), 16), SALE_5_START); + index += 64; + + // timestamp end + assert.equal(parseInt(log.payload.substr(index, 64), 16), SALE_5_END); + index += 64; + + // accepted tokens length + assert.equal( + parseInt(log.payload.substr(index, 2), 16), + solanaAcceptedTokensLength + ); + index += 2; + + // accepted token index + assert.equal( + parseInt(log.payload.substr(index, 2), 16), + SOLANA_TOKEN_INDEX_ONE + ); + index += 2; + + // token address + assert.equal( + log.payload.substr(index, 64), + web3.eth.abi + .encodeParameter("address", CONTRIBUTED_TOKEN_ONE.address) + .substring(2) + ); + index += 64; + + // accepted token index + assert.equal( + parseInt(log.payload.substr(index, 2), 16), + SOLANA_TOKEN_INDEX_TWO + ); + index += 2; + + // token address + assert.equal( + log.payload.substr(index, 64), + web3.eth.abi + .encodeParameter("address", CONTRIBUTED_TOKEN_TWO.address) + .substring(2) + ); + index += 64; + + // recipient of proceeds + assert.equal( + log.payload.substr(index, 64), + web3.eth.abi.encodeParameter("address", saleRecipient).substring(2) + ); + index += 64; + + assert.equal(log.payload.length, index); + + // confirm that we are not accepting any solana tokens + const sale = await initialized.methods.sales(SALE_5_ID).call(); + + assert.equal(sale.solanaAcceptedTokensCount, solanaAcceptedTokensLength); + }); + + it("conductor should accept mock attestContribution VAAs from contributors", async function() { + // skip to end of the sale + await wait(20); + + // test variables + const payloadIdType2 = "02"; + const solanaTokenContribution = "2000"; + const solanaTokenTwoContribution = "0"; + const ethereumTokenContribution = "1000"; + const acceptedTokensLengthSolana = 2; + const acceptedTokensLengthEthereum = 1; + + const initialized = new web3.eth.Contract( + ConductorImplementationFullABI, + TokenSaleConductor.address + ); + + // construct contributions payload coming from Solana contributor + const solanaContributionsSealed = [ + web3.eth.abi + .encodeParameter("uint8", payloadIdType2) + .substring(2 + (64 - 2)), + web3.eth.abi.encodeParameter("uint256", SALE_5_ID).substring(2), + web3.eth.abi + .encodeParameter("uint16", SOLANA_CHAIN_ID) + .substring(2 + (64 - 4)), + web3.eth.abi + .encodeParameter("uint8", acceptedTokensLengthSolana) + .substring(2 + (64 - 2)), + web3.eth.abi + .encodeParameter("uint8", SOLANA_TOKEN_INDEX_ONE) + .substring(2 + (64 - 2)), + web3.eth.abi + .encodeParameter("uint256", solanaTokenContribution) + .substring(2), + web3.eth.abi + .encodeParameter("uint8", SOLANA_TOKEN_INDEX_TWO) + .substring(2 + (64 - 2)), + web3.eth.abi + .encodeParameter("uint256", solanaTokenTwoContribution) + .substring(2), + ]; + + const vm = await signAndEncodeVM( + 1, + 1, + SOLANA_CHAIN_ID, + "0x000000000000000000000000" + TokenSaleContributor.address.substr(2), + 0, + "0x" + solanaContributionsSealed.join(""), + [testSigner1PK], + 0, + 0 + ); + + // collect contributions on the conductor + await initialized.methods.collectContribution("0x" + vm).send({ + from: BUYER_ONE, + gasLimit: GAS_LIMIT, + }); + + // construct contributions payload coming from ethereum contributor + const ethereumContributionsSealed = [ + web3.eth.abi + .encodeParameter("uint8", payloadIdType2) + .substring(2 + (64 - 2)), + web3.eth.abi.encodeParameter("uint256", SALE_5_ID).substring(2), + web3.eth.abi + .encodeParameter("uint16", TEST_CHAIN_ID) + .substring(2 + (64 - 4)), + web3.eth.abi + .encodeParameter("uint8", acceptedTokensLengthEthereum) + .substring(2 + (64 - 2)), + web3.eth.abi + .encodeParameter("uint8", ETH_TOKEN_INDEX) + .substring(2 + (64 - 2)), + web3.eth.abi + .encodeParameter("uint256", ethereumTokenContribution) + .substring(2), + ]; + + const vm2 = await signAndEncodeVM( + 1, + 1, + TEST_CHAIN_ID, + "0x000000000000000000000000" + TokenSaleContributor.address.substr(2), + 0, + "0x" + ethereumContributionsSealed.join(""), + [testSigner1PK], + 0, + 0 + ); + + // collect contributions on the conductor + await initialized.methods.collectContribution("0x" + vm2).send({ + from: BUYER_ONE, + gasLimit: GAS_LIMIT, + }); + + // verify contributions with conductor getter + const contributions = await initialized.methods + .saleContributions(SALE_5_ID) + .call(); + + assert.equal( + solanaTokenContribution, + contributions[parseInt(SOLANA_TOKEN_INDEX_ONE)] + ); + assert.equal( + solanaTokenTwoContribution, + contributions[parseInt(SOLANA_TOKEN_INDEX_TWO)] + ); + assert.equal( + ethereumTokenContribution, + contributions[parseInt(ETH_TOKEN_INDEX)] + ); + }); + + it("conductor sealSale should emit Solana specific VAA when accepting Solana tokens", async function() { + // test variables + const payloadIdType3 = "03"; + const numTokensInSolanaPayload = 2; + const numTokensInEthereumPayload = 3; + const solanaTokenAllocation = "5000000000000"; + const solanaTokenTwoAllocation = "0"; + const ethereumTokenAllocation = "5000000000000"; + const excessContribution = "0"; // same for all tokens - no excess contributions + + const initialized = new web3.eth.Contract( + ConductorImplementationFullABI, + TokenSaleConductor.address + ); + + // seal the sale + await initialized.methods.sealSale(SALE_5_ID).send({ + value: WORMHOLE_FEE * 3, + from: SELLER, + gasLimit: GAS_LIMIT, + }); + + const log = await WORMHOLE.getPastEvents("LogMessagePublished", { + fromBlock: "latest", + }); + + // grab payload from each of the emitted VAAs + const solanaTransferPayload = log[0].returnValues; + const ethereumSealedPayload = log[1].returnValues; + const solanaSealedPayload = log[2].returnValues; + + // parse the solana and ethereum saleSealed payloads together + // payload id + let index = 2; + assert.equal( + ethereumSealedPayload.payload.substr(index, 2), + payloadIdType3 + ); + assert.equal(solanaSealedPayload.payload.substr(index, 2), payloadIdType3); + index += 2; + + // sale id + assert.equal( + parseInt(ethereumSealedPayload.payload.substr(index, 64), 16), + SALE_5_ID + ); + assert.equal( + parseInt(solanaSealedPayload.payload.substr(index, 64), 16), + SALE_5_ID + ); + index += 64; + + // allocations length + assert.equal( + ethereumSealedPayload.payload.substr(index, 2), + numTokensInEthereumPayload + ); + assert.equal( + solanaSealedPayload.payload.substr(index, 2), + numTokensInSolanaPayload + ); + index += 2; + + // copy index + let solanaIndex = index; + let ethereumIndex = index; + + // parse solana token allocations and excess contributions + assert.equal( + solanaSealedPayload.payload.substr(solanaIndex, 2), + SOLANA_TOKEN_INDEX_ONE + ); + solanaIndex += 2; + + // solana allocation + assert.equal( + parseInt(solanaSealedPayload.payload.substr(solanaIndex, 64), 16), + solanaTokenAllocation + ); + solanaIndex += 64; + + // solana excess contribution + assert.equal( + parseInt(solanaSealedPayload.payload.substr(solanaIndex, 64), 16), + excessContribution + ); + solanaIndex += 64; + + // second allocation for solana tokens + assert.equal( + solanaSealedPayload.payload.substr(solanaIndex, 2), + SOLANA_TOKEN_INDEX_TWO + ); + solanaIndex += 2; + + // solana allocation + assert.equal( + parseInt(solanaSealedPayload.payload.substr(solanaIndex, 64), 16), + solanaTokenTwoAllocation + ); + solanaIndex += 64; + + // solana excess contribution + assert.equal( + parseInt(solanaSealedPayload.payload.substr(solanaIndex, 64), 16), + excessContribution + ); + solanaIndex += 64; + + // ethereum saleSealed wormhole message + assert.equal( + ethereumSealedPayload.payload.substr(ethereumIndex, 2), + ETH_TOKEN_INDEX + ); + ethereumIndex += 2; + + // eth allocation + assert.equal( + parseInt(ethereumSealedPayload.payload.substr(ethereumIndex, 64), 16), + ethereumTokenAllocation + ); + ethereumIndex += 64; + + // eth excessContribution + assert.equal( + parseInt(ethereumSealedPayload.payload.substr(ethereumIndex, 64), 16), + excessContribution + ); + ethereumIndex += 64; + + // index of solana token one in ethereum message + assert.equal( + ethereumSealedPayload.payload.substr(ethereumIndex, 2), + SOLANA_TOKEN_INDEX_ONE + ); + ethereumIndex += 2; + + // allocation of solana token one in ethereum message + assert.equal( + parseInt(ethereumSealedPayload.payload.substr(ethereumIndex, 64), 16), + solanaTokenAllocation + ); + ethereumIndex += 64; + + // solana excess contribution for token one + assert.equal( + parseInt(ethereumSealedPayload.payload.substr(ethereumIndex, 64), 16), + excessContribution + ); + ethereumIndex += 64; + + // index of solana token two in ethereum message + assert.equal( + ethereumSealedPayload.payload.substr(ethereumIndex, 2), + SOLANA_TOKEN_INDEX_TWO + ); + ethereumIndex += 2; + + // allocation of solana token one in ethereum message + assert.equal( + parseInt(ethereumSealedPayload.payload.substr(ethereumIndex, 64), 16), + solanaTokenTwoAllocation + ); + ethereumIndex += 64; + + // solana excess contribution for token two + assert.equal( + parseInt(ethereumSealedPayload.payload.substr(ethereumIndex, 64), 16), + excessContribution + ); + ethereumIndex += 64; + }); + it("conductor should not allow a sale to abort after the sale start time", async function() { console.log( "\n -------------------------- Other Tests --------------------------" @@ -4680,7 +5346,7 @@ contract("ICCO", function(accounts) { const tokenTwoConversionRate = "2000000000000000000"; const saleRecipient = accounts[0]; const refundRecipient = accounts[0]; - const saleId5 = 4; + const saleId = 5; const initialized = new web3.eth.Contract( ConductorImplementationFullABI, @@ -4719,7 +5385,7 @@ contract("ICCO", function(accounts) { // create another sale await initialized.methods.createSale(saleParams, acceptedTokens).send({ - value: "0", + value: WORMHOLE_FEE, from: SELLER, gasLimit: GAS_LIMIT, }); @@ -4730,7 +5396,7 @@ contract("ICCO", function(accounts) { let failed = false; try { // try to abort abort after the sale started - await initialized.methods.abortSaleBeforeStartTime(saleId5).send({ + await initialized.methods.abortSaleBeforeStartTime(saleId).send({ from: SELLER, gasLimit: GAS_LIMIT, }); @@ -4824,7 +5490,7 @@ contract("ICCO", function(accounts) { await initializedConductor.methods .createSale(saleParams, acceptedTokens) .send({ - value: "0", + value: WORMHOLE_FEE, from: SELLER, gasLimit: GAS_LIMIT, }); @@ -4942,7 +5608,7 @@ contract("ICCO", function(accounts) { try { // try to create a sale with a token with zero multiplier await initialized.methods.createSale(saleParams, acceptedTokens).send({ - value: "0", + value: WORMHOLE_FEE, from: SELLER, gasLimit: GAS_LIMIT, }); @@ -5025,7 +5691,7 @@ contract("ICCO", function(accounts) { try { // try to create a sale with sale start/end times larger than uint64 await initialized.methods.createSale(saleParams, acceptedTokens).send({ - value: "0", + value: WORMHOLE_FEE, from: SELLER, gasLimit: GAS_LIMIT, }); @@ -5040,7 +5706,7 @@ contract("ICCO", function(accounts) { assert.ok(failed); }); - it("conductor should allow fair launch sale (raiseAmount == minRaise && maxRaise", async function() { + it("conductor should allow fair launch sale (raiseAmount == minRaise && maxRaise)", async function() { // test variables const current_block = await web3.eth.getBlock("latest"); const saleStart = current_block.timestamp + 5; @@ -5122,7 +5788,7 @@ contract("ICCO", function(accounts) { await initializedConductor.methods .createSale(saleParams, acceptedTokens) .send({ - value: "0", + value: WORMHOLE_FEE, from: SELLER, gasLimit: GAS_LIMIT, }); @@ -5187,6 +5853,7 @@ contract("ICCO", function(accounts) { // attest contributions await initializedContributor.methods.attestContributions(saleId).send({ from: BUYER_ONE, + value: WORMHOLE_FEE, gasLimit: GAS_LIMIT, }); @@ -5219,6 +5886,7 @@ contract("ICCO", function(accounts) { // seal the sale in the conductor and check allocation details await initializedConductor.methods.sealSale(saleId).send({ from: SELLER, + value: WORMHOLE_FEE, gasLimit: GAS_LIMIT, }); @@ -5259,6 +5927,61 @@ contract("ICCO", function(accounts) { assert.equal(actualAllocation, expectedAllocation); assert.equal(actualExcessContribution, expectedExcessContribution); }); + + it("sdk should correctly convert conversion rates based on the saleToken decimals", async function() { + // conversion rate ("price" * 1e18) for both accepted tokens + const rawConversionRate = "1"; + + // expected accepted token decimals + const conductorDecimals = 9; + const acceptedTokenDecimals = conductorDecimals; + + // decimals of the sale token on the Conductor chain + const denominationDecimals1 = 6; + const denominationDecimals2 = 18; + const denominationDecimals3 = 9; + + // the expected output of the function + const expectedNormalizedConversionRate1 = "1000000000000000"; + const expectedNormalizedConversionRate2 = "1000000000000000000000000000"; + const expectedNormalizedConversionRate3 = "1000000000000000000"; + + // normalize to denom with 6 decimals + const normalizedConversionRate1 = await normalizeConversionRate( + denominationDecimals1, + acceptedTokenDecimals, + rawConversionRate, + conductorDecimals + ); + // normalize to denom with 18 decimals + const normalizedConversionRate2 = await normalizeConversionRate( + denominationDecimals2, + acceptedTokenDecimals, + rawConversionRate, + conductorDecimals + ); + // normalized to denomc with 9 decimals + const normalizedConversionRate3 = await normalizeConversionRate( + denominationDecimals3, + acceptedTokenDecimals, + rawConversionRate, + conductorDecimals + ); + + // make sure the function is producing the expected result + assert.equal( + normalizedConversionRate1.toString(), + expectedNormalizedConversionRate1 + ); + assert.equal( + normalizedConversionRate2.toString(), + expectedNormalizedConversionRate2 + ); + assert.equal( + normalizedConversionRate3.toString(), + expectedNormalizedConversionRate3 + ); + }); }); contract("ICCO Library Upgrade", function(accounts) { @@ -5389,6 +6112,32 @@ contract("ICCO Library Upgrade", function(accounts) { }); }); +async function normalizeConversionRate( + denominationDecimals, + acceptedTokenDecimals, + rawConversionRate, + conductorDecimals +) { + const precision = 18; + const normDecimals = denominationDecimals + precision - acceptedTokenDecimals; + let normalizedConversionRate = ethers.utils.parseUnits( + rawConversionRate, + normDecimals + ); + + if (acceptedTokenDecimals === conductorDecimals) { + return normalizedConversionRate; + } else if (acceptedTokenDecimals > conductorDecimals) { + return normalizedConversionRate.div( + ethers.utils.parseUnits("1", acceptedTokenDecimals - conductorDecimals) + ); + } else { + return normalizedConversionRate.mul( + ethers.utils.parseUnits("1", conductorDecimals - acceptedTokenDecimals) + ); + } +} + const signContribution = async function( conductorAddress, saleId, diff --git a/sdk/js/src/icco/getters.ts b/sdk/js/src/icco/getters.ts index 50a768d1..0dae911d 100644 --- a/sdk/js/src/icco/getters.ts +++ b/sdk/js/src/icco/getters.ts @@ -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, diff --git a/sdk/js/src/icco/misc.ts b/sdk/js/src/icco/misc.ts index 71386bb2..763a7577 100644 --- a/sdk/js/src/icco/misc.ts +++ b/sdk/js/src/icco/misc.ts @@ -7,7 +7,6 @@ import { nativeToHexString, getForeignAssetEth, getOriginalAssetEth, - hexToNativeString, uint8ArrayToNative, } from "@certusone/wormhole-sdk"; import { parseUnits } from "ethers/lib/utils"; diff --git a/sdk/js/src/icco/structs.ts b/sdk/js/src/icco/structs.ts index 6051abb3..07711285 100644 --- a/sdk/js/src/icco/structs.ts +++ b/sdk/js/src/icco/structs.ts @@ -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; diff --git a/test/testnet/src/consts.ts b/test/testnet/src/consts.ts index feb50f17..3827500c 100644 --- a/test/testnet/src/consts.ts +++ b/test/testnet/src/consts.ts @@ -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; diff --git a/test/testnet/src/run_testnet_sale.ts b/test/testnet/src/run_testnet_sale.ts index 7a951460..99fe9229 100644 --- a/test/testnet/src/run_testnet_sale.ts +++ b/test/testnet/src/run_testnet_sale.ts @@ -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); diff --git a/test/testnet/src/utils.ts b/test/testnet/src/utils.ts index 27082a34..3064c266 100644 --- a/test/testnet/src/utils.ts +++ b/test/testnet/src/utils.ts @@ -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 { + 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 { 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(); - 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 => { + return saleAbortedOnEth( + TESTNET_ADDRESSES[network], + signedVaa, + initiatorWallet(network), + saleId + ); + } + ) + ); + } + + return; +} diff --git a/testnet.json b/testnet.json index e76e9594..508cbad6 100644 --- a/testnet.json +++ b/testnet.json @@ -1,7 +1,8 @@ { "gaurdianRpc": "https://wormhole-v2-testnet-api.certus.one", - "conductorAddress": "0x5c49f34D92316A2ac68d10A1e2168e16610e84f9", + "conductorAddress": "0xce121EA9c289390df7d812F83Ed6bE79A167DfE4", "conductorChain": 2, - "goerli": "0xB6AF16A9B216c9eEd2AbA4452088FE28cc22d5Ff", - "fuji": "0xE60C9105BF114f198CE93F3A1aDf0FB09427C674" -} \ No newline at end of file + "goerli": "0xFee98e512e4Ab409294a080b3C2B19748Fd54A6d", + "fuji": "0x72df3672A0E889ca8875080d414056fD4f7Aa9fA", + "solana_testnet": "8WCGVzrvGoLRrWY5V4L7iDCJym3wu6SiL3DCjGsSrP4k" +} diff --git a/tools/tsconfig.json b/tools/tsconfig.json index d009bc8c..dd6246df 100644 --- a/tools/tsconfig.json +++ b/tools/tsconfig.json @@ -12,7 +12,8 @@ }, "include": [ "register_tilt_contributors.ts", - "register_testnet_contributors.ts" + "register_testnet_contributors.ts", + "upgrade_testnet_contracts.ts" ], "exclude": ["node_modules"] } diff --git a/tools/upgrade_testnet_contracts.ts b/tools/upgrade_testnet_contracts.ts new file mode 100644 index 00000000..3e1b2495 --- /dev/null +++ b/tools/upgrade_testnet_contracts.ts @@ -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();