Update contracts and sdk to fix bugs

This commit is contained in:
Drew 2022-05-12 18:24:06 +00:00
parent b7db58a6ab
commit 1868071afb
23 changed files with 414 additions and 217 deletions

View File

@ -5,6 +5,7 @@ pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "../../libraries/external/BytesLib.sol";
@ -15,11 +16,11 @@ import "./ConductorGovernance.sol";
import "../shared/ICCOStructs.sol";
contract Conductor is ConductorGovernance {
contract Conductor is ConductorGovernance, ReentrancyGuard {
function createSale(
ICCOStructs.Raise memory raise,
ICCOStructs.Token[] memory acceptedTokens
) public payable returns (
) public payable nonReentrant returns (
uint saleId,
uint wormholeSequence
) {
@ -31,18 +32,28 @@ contract Conductor is ConductorGovernance {
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");
// grab the local token address (address for the conductor chain)
address localTokenAddress;
if (raise.tokenChain == chainId()) {
localTokenAddress = address(uint160(uint256(raise.token)));
} else {
// identify wormhole token bridge wrapper
localTokenAddress = tokenBridge().wrappedAsset(raise.tokenChain, raise.token);
require(localTokenAddress != address(0), "wrapped address not found on this chain");
}
{ // token deposit context to avoid stack too deep errors
// query own token balance before transfer
(,bytes memory queriedBalanceBefore) = raise.token.staticcall(abi.encodeWithSelector(IERC20.balanceOf.selector, address(this)));
(,bytes memory queriedBalanceBefore) = localTokenAddress.staticcall(abi.encodeWithSelector(IERC20.balanceOf.selector, address(this)));
uint256 balanceBefore = abi.decode(queriedBalanceBefore, (uint256));
// deposit tokens
SafeERC20.safeTransferFrom(IERC20(raise.token), msg.sender, address(this), raise.tokenAmount);
SafeERC20.safeTransferFrom(IERC20(localTokenAddress), msg.sender, address(this), raise.tokenAmount);
// query own token balance after transfer
(,bytes memory queriedBalanceAfter) = raise.token.staticcall(abi.encodeWithSelector(IERC20.balanceOf.selector, address(this)));
(,bytes memory queriedBalanceAfter) = localTokenAddress.staticcall(abi.encodeWithSelector(IERC20.balanceOf.selector, address(this)));
uint256 balanceAfter = abi.decode(queriedBalanceAfter, (uint256));
// revert if token has fee
@ -54,8 +65,9 @@ contract Conductor is ConductorGovernance {
saleId = useSaleId();
ConductorStructs.Sale memory sale = ConductorStructs.Sale({
saleID : saleId,
tokenAddress : bytes32(uint256(uint160(raise.token))),
tokenChain : chainId(),
tokenAddress : raise.token,
tokenChain : raise.tokenChain,
localTokenAddress: localTokenAddress,
tokenAmount : raise.tokenAmount,
minRaise: raise.minRaise,
maxRaise: raise.maxRaise,
@ -96,9 +108,9 @@ contract Conductor is ConductorGovernance {
// Sale ID
saleID : saleId,
// Address of the token. Left-zero-padded if shorter than 32 bytes
tokenAddress : bytes32(uint256(uint160(raise.token))),
tokenAddress : raise.token,
// Chain ID of the token
tokenChain : chainId(),
tokenChain : raise.tokenChain,
// token amount being sold
tokenAmount : raise.tokenAmount,
// min raise amount
@ -118,7 +130,7 @@ contract Conductor is ConductorGovernance {
});
wormholeSequence = wormhole().publishMessage{
value : msg.value
}(0, ICCOStructs.encodeSaleInit(saleInit), consistencyLevel());
}(0, ICCOStructs.encodeSaleInit(saleInit), consistencyLevel());
}
function abortSaleBeforeStartTime(uint saleId) public payable returns (uint wormholeSequence) {
@ -178,6 +190,14 @@ contract Conductor is ConductorGovernance {
require(!sale.isSealed && !sale.isAborted, "already sealed / aborted");
// query sale tokens decimals
// bypass stack too deep
uint8 saleTokenDecimals;
{
(,bytes memory queriedDecimals) = sale.localTokenAddress.staticcall(abi.encodeWithSignature("decimals()"));
saleTokenDecimals = abi.decode(queriedDecimals, (uint8));
}
ConductorStructs.InternalAccounting memory accounting;
for (uint i = 0; i < sale.contributionsCollected.length; i++) {
@ -209,18 +229,18 @@ contract Conductor is ConductorGovernance {
uint allocation = sale.tokenAmount * (sale.contributions[i] * sale.acceptedTokensConversionRates[i] / 1e18) / accounting.totalContribution;
uint excessContribution = accounting.totalExcessContribution * sale.contributions[i] / accounting.totalContribution;
if(allocation > 0) {
if (allocation > 0) {
// send allocations to contributor contracts
if (sale.acceptedTokensChains[i] == chainId()) {
// simple transfer on same chain
SafeERC20.safeTransfer(IERC20(address(uint160(uint256(sale.tokenAddress)))), address(uint160(uint256(contributorContracts(sale.acceptedTokensChains[i])))), allocation);
SafeERC20.safeTransfer(IERC20(sale.localTokenAddress), address(uint160(uint256(contributorCustody(sale.acceptedTokensChains[i])))), allocation);
} else {
// adjust allocation for dust after token bridge transfer
allocation = (allocation / 1e10) * 1e10;
allocation = ICCOStructs.deNormalizeAmount(ICCOStructs.normalizeAmount(allocation, saleTokenDecimals), saleTokenDecimals);
// transfer over wormhole token bridge
SafeERC20.safeApprove(IERC20(address(uint160(uint256(sale.tokenAddress)))), address(tknBridge), allocation);
SafeERC20.safeApprove(IERC20(sale.localTokenAddress), address(tknBridge), allocation);
require(accounting.valueSent >= accounting.messageFee, "insufficient wormhole messaging fees");
accounting.valueSent -= accounting.messageFee;
@ -228,10 +248,10 @@ contract Conductor is ConductorGovernance {
tknBridge.transferTokens{
value : accounting.messageFee
}(
address(uint160(uint256(sale.tokenAddress))),
sale.localTokenAddress,
allocation,
sale.acceptedTokensChains[i],
contributorContracts(sale.acceptedTokensChains[i]),
contributorCustody(sale.acceptedTokensChains[i]),
0,
0
);
@ -248,7 +268,7 @@ contract Conductor is ConductorGovernance {
// transfer dust back to refund recipient
accounting.dust = sale.tokenAmount - accounting.totalAllocated;
if (accounting.dust > 0) {
SafeERC20.safeTransfer(IERC20(address(uint160(uint256(sale.tokenAddress)))), address(uint160(uint256(sale.refundRecipient))), accounting.dust);
SafeERC20.safeTransfer(IERC20(sale.localTokenAddress), address(uint160(uint256(sale.refundRecipient))), accounting.dust);
}
require(accounting.valueSent >= accounting.messageFee, "insufficient wormhole messaging fees");
@ -284,7 +304,7 @@ contract Conductor is ConductorGovernance {
setRefundClaimed(saleId);
SafeERC20.safeTransfer(IERC20(address(uint160(uint256(sale.tokenAddress)))), address(uint160(uint256(sale.refundRecipient))), sale.tokenAmount);
SafeERC20.safeTransfer(IERC20(sale.localTokenAddress), address(uint160(uint256(sale.refundRecipient))), sale.tokenAmount);
}
function useSaleId() internal returns(uint256 saleId) {

View File

@ -39,6 +39,10 @@ contract ConductorGetters is ConductorState {
return _state.contributorImplementations[chainId_];
}
function contributorCustody(uint16 chainId_) public view returns (bytes32){
return _state.contributorCustody[chainId_];
}
function sales(uint saleId_) public view returns (ConductorStructs.Sale memory sale){
return _state.sales[saleId_];
}

View File

@ -21,9 +21,9 @@ contract ConductorGovernance is ConductorGetters, ConductorSetters, ERC1967Upgra
event OwnershipTransfered(address indexed oldOwner, address indexed newOwner);
// register contributor contract
function registerChain(uint16 contributorChainId, bytes32 contributorAddress) public onlyOwner {
function registerChain(uint16 contributorChainId, bytes32 contributorAddress, bytes32 custodyAddress) public onlyOwner {
require(contributorContracts(contributorChainId) == bytes32(0), "chain already registered");
setContributor(contributorChainId, contributorAddress);
setContributor(contributorChainId, contributorAddress, custodyAddress);
}
function upgrade(uint16 conductorChainId, address newImplementation) public onlyOwner {

View File

@ -11,8 +11,9 @@ contract ConductorSetters is ConductorState, Context {
_state.owner = owner_;
}
function setContributor(uint16 chainId, bytes32 emitter) internal {
function setContributor(uint16 chainId, bytes32 emitter, bytes32 custody) internal {
_state.contributorImplementations[chainId] = emitter;
_state.contributorCustody[chainId] = custody;
}
function setInitialized(address implementatiom) internal {

View File

@ -33,6 +33,7 @@ contract ConductorStorage {
// Mapping of Conductor contracts on other chains
mapping(uint16 => bytes32) contributorImplementations;
mapping(uint16 => bytes32) contributorCustody;
// Mapping of Sales
mapping(uint => ConductorStructs.Sale) sales;

View File

@ -7,10 +7,12 @@ contract ConductorStructs {
struct Sale {
// Sale ID
uint256 saleID;
// Address of the token. Left-zero-padded if shorter than 32 bytes
// Native address of the token. Left-zero-padded if shorter than 32 bytes
bytes32 tokenAddress;
// Chain ID of the token
// Native chain ID of the token
uint16 tokenChain;
// address of token on conductor chain, will be different if selling a wrapped token
address localTokenAddress;
// token amount being sold
uint256 tokenAmount;
// min raise amount

View File

@ -209,19 +209,21 @@ contract Contributor is ContributorGovernance, ReentrancyGuard {
require(tokenBalance >= tokenAllocation, "insufficient sale token balance");
setSaleSealed(sealedSale.saleID);
}
uint16 conductorChainId = conductorChainId();
if (conductorChainId == thisChainId) {
// REVIEW: need to refactor this code
uint16 conductorChainId = conductorChainId(); // REVIEW: can the recipient be a cross-chain wallet?
if (conductorChainId == thisChainId) { // REVIEW: change the logic to make it more readible
// raised funds are payed out on this chain
for (uint i = 0; i < sale.acceptedTokensAddresses.length; i++) {
if (sale.acceptedTokensChains[i] == thisChainId) {
// send contributions less excess owed to contributors
uint256 totalContributionsLessExcess = getSaleTotalContribution(sale.saleID, i) - getSaleExcessContribution(sale.saleID, i);
SafeERC20.safeTransfer(
IERC20(address(uint160(uint256(sale.acceptedTokensAddresses[i])))),
address(uint160(uint256(sale.recipient))),
totalContributionsLessExcess
);
if (totalContributionsLessExcess > 0) {
SafeERC20.safeTransfer(
IERC20(address(uint160(uint256(sale.acceptedTokensAddresses[i])))),
address(uint160(uint256(sale.recipient))),
totalContributionsLessExcess
);
}
}
}
} else {
@ -231,26 +233,43 @@ contract Contributor is ContributorGovernance, ReentrancyGuard {
uint valueSent = msg.value;
for (uint i = 0; i < sale.acceptedTokensAddresses.length; i++) {
if (sale.acceptedTokensChains[i] == thisChainId) {
if (sale.acceptedTokensChains[i] == thisChainId) {
address acceptedTokenAddress = address(uint160(uint256(sale.acceptedTokensAddresses[i])));
// get token decimals for normalization of token amount
uint8 acceptedTokenDecimals;
{// bypass stack too deep
(,bytes memory queriedDecimals) = acceptedTokenAddress.staticcall(abi.encodeWithSignature("decimals()"));
acceptedTokenDecimals = abi.decode(queriedDecimals, (uint8));
}
// send contributions less excess owed to contributors
uint totalContributionsLessExcess = ((getSaleTotalContribution(sale.saleID, i) - getSaleExcessContribution(sale.saleID, i)) / 1e10) * 1e10;
// transfer over wormhole token bridge
SafeERC20.safeApprove(IERC20(address(uint160(uint256(sale.acceptedTokensAddresses[i])))), address(tknBridge), totalContributionsLessExcess);
require(valueSent >= messageFee, "insufficient wormhole messaging fees");
valueSent -= messageFee;
tknBridge.transferTokens{
value : messageFee
}(
address(uint160(uint256(sale.acceptedTokensAddresses[i]))),
totalContributionsLessExcess,
conductorChainId,
sale.recipient,
0,
0
uint totalContributionsLessExcess = ICCOStructs.deNormalizeAmount(
ICCOStructs.normalizeAmount(
getSaleTotalContribution(sale.saleID, i) - getSaleExcessContribution(sale.saleID, i),
acceptedTokenDecimals
),
acceptedTokenDecimals
);
if (totalContributionsLessExcess > 0) {
// transfer over wormhole token bridge
SafeERC20.safeApprove(IERC20(acceptedTokenAddress), address(tknBridge), totalContributionsLessExcess);
require(valueSent >= messageFee, "insufficient wormhole messaging fees");
valueSent -= messageFee;
tknBridge.transferTokens{
value : messageFee
}(
acceptedTokenAddress,
totalContributionsLessExcess,
conductorChainId,
sale.recipient,
0,
0
);
}
}
}
}

View File

@ -31,7 +31,9 @@ library ICCOStructs {
struct Raise {
// sale token address
address token;
bytes32 token;
// sale token chainId
uint16 tokenChain;
// token amount being sold
uint256 tokenAmount;
// min raise amount
@ -102,6 +104,20 @@ library ICCOStructs {
uint256 saleID;
}
function normalizeAmount(uint256 amount, uint8 decimals) public pure returns(uint256){
if (decimals > 8) {
amount /= 10 ** (decimals - 8);
}
return amount;
}
function deNormalizeAmount(uint256 amount, uint8 decimals) public pure returns(uint256){
if (decimals > 8) {
amount *= 10 ** (decimals - 8);
}
return amount;
}
function encodeSaleInit(SaleInit memory saleInit) public pure returns (bytes memory encoded) {
return abi.encodePacked(
uint8(1),

View File

@ -1,15 +0,0 @@
module.exports = {
development: {
wormhole: "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550",
tokenBridge: "0x0290FB167208Af455bB137780163b7B7a9a10C16",
},
eth_devnet: {
wormhole: "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550",
tokenBridge: "0x0290FB167208Af455bB137780163b7B7a9a10C16",
},
eth_devnet2: {
wormhole: "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550",
tokenBridge: "0x0290FB167208Af455bB137780163b7B7a9a10C16",
},
// add more based on truffle-config.js
};

View File

@ -27,8 +27,12 @@ export const ETH_CORE_BRIDGE_ADDRESS =
export const ETH_TOKEN_BRIDGE_ADDRESS =
"0x0290FB167208Af455bB137780163b7B7a9a10C16";
// decimals for min/max raise denomination
export const DENOMINATION_DECIMALS = 18;
// contributors only registered with conductor on CHAIN_ID_ETH
export const ETH_TOKEN_SALE_CONDUCTOR_ADDRESS = Tilt.conductorAddress;
export const ETH_TOKEN_SALE_CONDUCTOR_CHAIN_ID = Tilt.conductorChain;
export const TOKEN_SALE_CONTRIBUTOR_ADDRESSES = new Map<ChainId, string>();
TOKEN_SALE_CONTRIBUTOR_ADDRESSES.set(CHAIN_ID_ETH, Tilt.ethContributorAddress);
TOKEN_SALE_CONTRIBUTOR_ADDRESSES.set(CHAIN_ID_BSC, Tilt.bscContributorAddress);

View File

@ -20,6 +20,7 @@ import {
uint8ArrayToHex,
hexToNativeString,
uint8ArrayToNative,
importCoreWasm,
} from "@certusone/wormhole-sdk";
import { Contributor__factory } from "../../ethers-contracts";
@ -37,16 +38,13 @@ import {
createSaleOnEth,
contributeOnEth,
secureContributeOnEth,
extractVaaPayload,
getAllocationIsClaimedOnEth,
getCurrentBlock,
getErc20Balance,
getRefundIsClaimedOnEth,
getSaleContributionOnEth,
getSaleFromConductorOnEth,
getSaleFromContributorOnEth,
makeAcceptedToken,
makeAcceptedWrappedTokenEth,
nativeToUint8Array,
parseSaleInit,
parseSaleSealed,
@ -55,9 +53,9 @@ import {
sleepFor,
wrapEth,
saleAbortedOnEth,
sealSaleAndParseReceiptOnEth,
SealSaleResult,
getSaleWalletAllocationOnEth,
ConductorSale,
getTargetChainIdFromTransferVaa,
} from "..";
import {
ETH_CORE_BRIDGE_ADDRESS,
@ -66,7 +64,13 @@ import {
TOKEN_SALE_CONTRIBUTOR_ADDRESSES,
KYC_PRIVATE_KEYS,
WORMHOLE_RPC_HOSTS,
DENOMINATION_DECIMALS,
ETH_TOKEN_SALE_CONDUCTOR_CHAIN_ID,
ETH_NODE_URL,
} from "./consts";
import { getSaleIdFromIccoVaa } from "../signedVaa";
import { normalizeConversionRate } from "../..";
import { getAcceptedTokenDecimalsOnConductor } from "../misc";
const ERC20 = require("@openzeppelin/contracts/build/contracts/ERC20PresetMinterPauser.json");
@ -93,6 +97,14 @@ enum BalanceChange {
Decrease,
}
export async function extractVaaPayload(
signedVaa: Uint8Array
): Promise<Uint8Array> {
const { parse_vaa } = await importCoreWasm();
const { payload: payload } = parse_vaa(signedVaa);
return payload;
}
export async function deployTokenOnEth(
rpc: string,
name: string,
@ -124,13 +136,17 @@ export async function deployTokenOnEth(
// TODO: add terra and solana handling to this (doing it serially here to make it easier to adapt)
export async function makeAcceptedTokensFromConfigs(
configs: EthContributorConfig[],
potentialBuyers: EthBuyerConfig[]
potentialBuyers: EthBuyerConfig[],
denominationDecimals: number
): Promise<AcceptedToken[]> {
const acceptedTokens: AcceptedToken[] = [];
// create map to record which accepted tokens have been created
const tokenMap: Map<number, string[]> = new Map<number, string[]>();
// eth conductor provider
const ethProvider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
for (const buyer of potentialBuyers) {
const info = await getOriginalAssetEth(
ETH_TOKEN_BRIDGE_ADDRESS,
@ -163,11 +179,37 @@ export async function makeAcceptedTokensFromConfigs(
}
}
// normalize the conversion rates here
const token = ERC20__factory.connect(
contributor.collateralAddress,
contributor.wallet
);
const acceptedTokenDecimals = await token.decimals();
// compute the normalized conversionRate
const acceptedTokenDecimalsOnConductor =
await getAcceptedTokenDecimalsOnConductor(
buyer.chainId,
ETH_TOKEN_SALE_CONDUCTOR_CHAIN_ID as ChainId,
ETH_TOKEN_BRIDGE_ADDRESS,
ETH_TOKEN_BRIDGE_ADDRESS,
buyer.wallet.provider,
ethProvider,
buyer.collateralAddress,
acceptedTokenDecimals
);
const normalizedConversionRate = await normalizeConversionRate(
denominationDecimals,
acceptedTokenDecimals,
contributor.conversionRate,
acceptedTokenDecimalsOnConductor
);
acceptedTokens.push(
makeAcceptedToken(
buyer.chainId,
buyer.collateralAddress,
contributor.conversionRate
normalizedConversionRate
)
);
}
@ -396,7 +438,9 @@ export async function transferFromEthNativeAndRedeemOnEth(
export async function createSaleOnEthAndGetVaa(
seller: ethers.Wallet,
chainId: ChainId,
localTokenAddress: string,
tokenAddress: string,
tokenChain: ChainId,
amount: ethers.BigNumberish,
minRaise: ethers.BigNumberish,
maxRaise: ethers.BigNumberish,
@ -407,7 +451,9 @@ export async function createSaleOnEthAndGetVaa(
// create
const receipt = await createSaleOnEth(
ETH_TOKEN_SALE_CONDUCTOR_ADDRESS,
localTokenAddress,
tokenAddress,
tokenChain,
amount,
minRaise,
maxRaise,
@ -590,7 +636,9 @@ export async function makeSaleStartFromLastBlock(
export async function createSaleOnEthAndInit(
conductorConfig: EthContributorConfig,
contributorConfigs: EthContributorConfig[],
localTokenAddress: string,
saleTokenAddress: string,
saleTokenChain: ChainId,
tokenAmount: string,
minRaise: string,
maxRaise: string,
@ -609,10 +657,12 @@ export async function createSaleOnEthAndInit(
const saleInitVaa = await createSaleOnEthAndGetVaa(
conductorConfig.wallet,
conductorConfig.chainId,
localTokenAddress,
saleTokenAddress,
saleTokenChain,
ethers.utils.parseUnits(tokenAmount, decimals),
ethers.utils.parseUnits(minRaise),
ethers.utils.parseUnits(maxRaise),
ethers.utils.parseUnits(minRaise, DENOMINATION_DECIMALS),
ethers.utils.parseUnits(maxRaise, DENOMINATION_DECIMALS),
saleStart,
saleEnd,
acceptedTokens
@ -620,7 +670,10 @@ export async function createSaleOnEthAndInit(
console.info("Sale Init VAA:", Buffer.from(saleInitVaa).toString("hex"));
const saleInit = await parseSaleInit(saleInitVaa);
const saleInitPayload = await extractVaaPayload(saleInitVaa);
const saleInit = await parseSaleInit(saleInitPayload);
console.log(saleInit);
{
const receipts = await Promise.all(
@ -715,6 +768,76 @@ export async function attestAndCollectContributions(
return;
}
export interface SealSaleResult {
sale: ConductorSale;
transferVaas: Map<ChainId, Uint8Array[]>;
sealSaleVaa: Uint8Array;
}
export async function sealSaleAndParseReceiptOnEth(
conductorAddress: string,
saleId: ethers.BigNumberish,
coreBridgeAddress: string,
tokenBridgeAddress: string,
wormholeHosts: string[],
extraGrpcOpts: any = {},
wallet: ethers.Wallet
): Promise<SealSaleResult> {
const receipt = await sealSaleOnEth(conductorAddress, saleId, wallet);
const sale = await getSaleFromConductorOnEth(
conductorAddress,
wallet.provider,
saleId
);
const emitterChain = sale.tokenChain as ChainId;
const sequences = parseSequencesFromLogEth(receipt, coreBridgeAddress);
const sealSaleSequence = sequences.pop();
if (sealSaleSequence === undefined) {
throw Error("no vaa sequences found");
}
const result = await getSignedVAAWithRetry(
wormholeHosts,
emitterChain,
getEmitterAddressEth(conductorAddress),
sealSaleSequence,
extraGrpcOpts
);
const sealSaleVaa = result.vaaBytes;
console.info("Seal Sale VAA:", Buffer.from(sealSaleVaa).toString("hex"));
// doing it serially for ease of putting into the map
const mapped = new Map<ChainId, Uint8Array[]>();
for (const sequence of sequences) {
const result = await getSignedVAAWithRetry(
wormholeHosts,
emitterChain,
getEmitterAddressEth(tokenBridgeAddress),
sequence,
extraGrpcOpts
);
const signedVaa = result.vaaBytes;
const vaaPayload = await extractVaaPayload(signedVaa);
const chainId = await getTargetChainIdFromTransferVaa(vaaPayload);
const signedVaas = mapped.get(chainId);
if (signedVaas === undefined) {
mapped.set(chainId, [signedVaa]);
} else {
signedVaas.push(signedVaa);
}
}
return {
sale: sale,
transferVaas: mapped,
sealSaleVaa: sealSaleVaa,
};
}
async function _sealOrAbortSaleOnEth(
saleInit: SaleInit,
conductorConfig: EthContributorConfig,
@ -822,8 +945,8 @@ export async function sealSaleAtContributors(
}
const signedVaa = saleResult.sealSaleVaa;
const saleSealed = await parseSaleSealed(signedVaa);
const vaaPayload = await extractVaaPayload(signedVaa);
const saleSealed = await parseSaleSealed(vaaPayload);
// first check if the sale token has been attested
{
@ -841,7 +964,8 @@ export async function sealSaleAtContributors(
return saleSealedOnEth(
TOKEN_SALE_CONTRIBUTOR_ADDRESSES.get(config.chainId)!,
signedVaa,
config.wallet
config.wallet,
saleInit.saleId
);
}
)
@ -856,6 +980,8 @@ export async function abortSaleAtContributors(
contributorConfigs: EthContributorConfig[]
) {
const signedVaa = saleResult.sealSaleVaa;
const vaaPayload = await extractVaaPayload(signedVaa);
const saleId = await getSaleIdFromIccoVaa(vaaPayload);
{
const receipts = await Promise.all(
@ -864,7 +990,8 @@ export async function abortSaleAtContributors(
return saleAbortedOnEth(
TOKEN_SALE_CONTRIBUTOR_ADDRESSES.get(config.chainId)!,
signedVaa,
config.wallet
config.wallet,
saleId
);
}
)

View File

@ -13,10 +13,7 @@ import {
getContributorContractOnEth,
getSaleFromConductorOnEth,
getSaleFromContributorOnEth,
registerChainOnEth,
sealSaleOnEth,
ConductorSale,
makeAcceptedToken,
} from "../..";
import {
BSC_NODE_URL,
@ -29,9 +26,9 @@ import {
ETH_TOKEN_BRIDGE_ADDRESS,
ETH_TOKEN_SALE_CONDUCTOR_ADDRESS,
TOKEN_SALE_CONTRIBUTOR_ADDRESSES,
KYC_PRIVATE_KEYS,
WBNB_ADDRESS,
WETH_ADDRESS,
DENOMINATION_DECIMALS,
} from "./consts";
import {
EthBuyerConfig,
@ -41,7 +38,6 @@ import {
waitForSaleToStart,
makeAcceptedTokensFromConfigs,
sealOrAbortSaleOnEth,
contributeAllTokensOnEth,
secureContributeAllTokensOnEth,
getCollateralBalancesOnEth,
claimAllAllocationsOnEth,
@ -63,7 +59,6 @@ import {
abortSaleEarlyAtContributors,
abortSaleEarlyAtConductor,
deployTokenOnEth,
signContribution,
} from "./helpers";
// ten minutes? nobody got time for that
@ -228,7 +223,8 @@ describe("Integration Tests", () => {
// we need to set up all of the accepted tokens (natives plus their wrapped versions)
const acceptedTokens = await makeAcceptedTokensFromConfigs(
contributorConfigs,
buyers
buyers,
DENOMINATION_DECIMALS
);
// add fake terra and solana tokens to acceptedTokens
@ -255,6 +251,7 @@ describe("Integration Tests", () => {
conductorConfig.wallet
);
const tokenChain = CHAIN_ID_ETH; // needed to check if token is native or not
const tokenAmount = "1";
const minRaise = "10"; // eth units
const maxRaise = "14";
@ -265,10 +262,16 @@ describe("Integration Tests", () => {
contributorConfigs
);
// the token being sold is on eth
// which means it has the same local token address
const localTokenAddress = tokenAddress;
const saleInit = await createSaleOnEthAndInit(
conductorConfig,
contributorConfigs,
localTokenAddress,
tokenAddress,
tokenChain,
tokenAmount,
minRaise,
maxRaise,
@ -278,8 +281,6 @@ describe("Integration Tests", () => {
);
console.log("Parsed Sale Init:", saleInit);
console.log("Parsed Sale Init:", saleInit);
// balance check
{
const buyerBalancesBefore = await getCollateralBalancesOnEth(
@ -687,7 +688,8 @@ describe("Integration Tests", () => {
// we need to set up all of the accepted tokens (natives plus their wrapped versions)
const acceptedTokens = await makeAcceptedTokensFromConfigs(
contributorConfigs,
buyers
buyers,
DENOMINATION_DECIMALS
);
// conductor lives in CHAIN_ID_ETH
@ -702,6 +704,7 @@ describe("Integration Tests", () => {
conductorConfig.wallet
);
const tokenChain = CHAIN_ID_ETH; // needed to check if token is native or not
const tokenAmount = "1";
const minRaise = "10"; // eth units
const maxRaise = "100";
@ -712,10 +715,16 @@ describe("Integration Tests", () => {
contributorConfigs
);
// the token being sold is on eth
// which means it has the same local token address
const localTokenAddress = tokenAddress;
const saleInit = await createSaleOnEthAndInit(
conductorConfig,
contributorConfigs,
localTokenAddress,
tokenAddress,
tokenChain,
tokenAmount,
minRaise,
maxRaise,
@ -924,7 +933,8 @@ describe("Integration Tests", () => {
// we need to set up all of the accepted tokens
const acceptedTokens = await makeAcceptedTokensFromConfigs(
contributorConfigs,
buyers
buyers,
DENOMINATION_DECIMALS
);
// conductor lives in CHAIN_ID_ETH
@ -939,6 +949,7 @@ describe("Integration Tests", () => {
conductorConfig.wallet
);
const tokenChain = CHAIN_ID_ETH; // needed to check if token is native or not
const tokenAmount = "1";
const minRaise = "10"; // eth units
const maxRaise = "100";
@ -956,10 +967,16 @@ describe("Integration Tests", () => {
contributorConfigs
);
// the token being sold is on eth
// which means it has the same local token address
const localTokenAddress = tokenAddress;
const saleInit = await createSaleOnEthAndInit(
conductorConfig,
contributorConfigs,
localTokenAddress,
tokenAddress,
tokenChain,
tokenAmount,
minRaise,
maxRaise,

View File

@ -47,7 +47,12 @@ export async function contributeOnEth(
const receipt = await tx.wait();
}
const tx = await contributor.contribute(saleId, tokenIndex, amount, signature);
const tx = await contributor.contribute(
saleId,
tokenIndex,
amount,
signature
);
return tx.wait();
}

View File

@ -4,9 +4,8 @@ import {
ERC20__factory,
getForeignAssetEth,
} from "@certusone/wormhole-sdk";
import { Conductor__factory } from "../ethers-contracts";
import { nativeToUint8Array } from "./misc";
import { Conductor__factory } from "../ethers-contracts";
import { AcceptedToken, SaleInit, makeAcceptedToken, Raise } from "./structs";
export { AcceptedToken, SaleInit };
@ -39,7 +38,9 @@ export async function makeAcceptedWrappedTokenEth(
export async function createSaleOnEth(
conductorAddress: string,
localTokenAddress: string,
tokenAddress: string,
tokenChain: ChainId,
amount: ethers.BigNumberish,
minRaise: ethers.BigNumberish,
maxRaise: ethers.BigNumberish,
@ -52,14 +53,18 @@ export async function createSaleOnEth(
): Promise<ethers.ContractReceipt> {
// approve first
{
const token = ERC20__factory.connect(tokenAddress, wallet);
const token = ERC20__factory.connect(localTokenAddress, wallet);
const tx = await token.approve(conductorAddress, amount);
const receipt = await tx.wait();
}
// convert address string to bytes32
const tokenAddressBytes32 = nativeToUint8Array(tokenAddress, tokenChain);
// create a struct to pass to createSale
const raise: Raise = {
token: tokenAddress,
token: tokenAddressBytes32,
tokenChain: tokenChain,
tokenAmount: amount,
minRaise: minRaise,
maxRaise: maxRaise,

View File

@ -18,6 +18,7 @@ export async function getSaleFromConductorOnEth(
saleId: sale.saleID,
tokenAddress: sale.tokenAddress,
tokenChain: sale.tokenChain,
localTokenAddress: sale.localTokenAddress,
tokenAmount: sale.tokenAmount,
minRaise: sale.minRaise,
maxRaise: sale.maxRaise,

View File

@ -1,26 +1,12 @@
import { ethers } from "ethers";
import { Contributor__factory } from "../ethers-contracts";
import { getSaleFromContributorOnEth } from "./getters";
import { parseSaleInit } from "./signedVaa";
export async function initSaleOnEth(
contributorAddress: string,
signedVaa: Uint8Array,
wallet: ethers.Wallet
): Promise<ethers.ContractReceipt> {
const saleInit = await parseSaleInit(signedVaa);
// check if sale exists already
const sale = await getSaleFromContributorOnEth(
contributorAddress,
wallet.provider,
saleInit.saleId
);
if (!ethers.BigNumber.from(sale.saleId).eq("0")) {
throw Error("sale already exists");
}
const contributor = Contributor__factory.connect(contributorAddress, wallet);
const tx = await contributor.initSale(signedVaa);
return tx.wait();

View File

@ -5,7 +5,12 @@ import {
IWETH__factory,
hexToUint8Array,
nativeToHexString,
getForeignAssetEth,
getOriginalAssetEth,
hexToNativeString,
uint8ArrayToNative,
} from "@certusone/wormhole-sdk";
import { parseUnits } from "ethers/lib/utils";
export function nativeToUint8Array(
address: string,
@ -44,3 +49,94 @@ export async function getErc20Balance(
const token = ERC20__factory.connect(tokenAddress, provider);
return token.balanceOf(walletAddress);
}
export async function getErc20Decimals(
provider: ethers.providers.Provider,
tokenAddress: string
): Promise<number> {
const token = ERC20__factory.connect(tokenAddress, provider);
return token.decimals();
}
export async function getAcceptedTokenDecimalsOnConductor(
contributorChain: ChainId,
conductorChain: ChainId,
contributorTokenBridgeAddress: string,
conductorTokenBridgeAddress: string,
contributorProvider: ethers.providers.Provider,
conductorProvider: ethers.providers.Provider,
contributedTokenAddress: string,
condtributedTokenDecimals: number
): Promise<number> {
if (contributorChain !== conductorChain) {
// fetch the original token address for contributed token
const originalToken = await getOriginalAssetEth(
contributorTokenBridgeAddress,
contributorProvider,
contributedTokenAddress,
contributorChain
);
let tokenDecimalsOnConductor;
if (originalToken.chainId === conductorChain) {
// get the original decimals
const nativeConductorAddress = uint8ArrayToNative(
originalToken.assetAddress,
originalToken.chainId
);
if (nativeConductorAddress !== undefined) {
// fetch the token decimals on the conductor chain
tokenDecimalsOnConductor = await getErc20Decimals(
conductorProvider,
nativeConductorAddress
);
} else {
throw Error("Native conductor address is undefined");
}
} else {
// get the wrapped versionals decimals on eth
const conductorWrappedToken = await getForeignAssetEth(
conductorTokenBridgeAddress,
conductorProvider,
originalToken.chainId,
originalToken.assetAddress
);
if (conductorWrappedToken !== null) {
// fetch the token decimals on the conductor chain
tokenDecimalsOnConductor = await getErc20Decimals(
conductorProvider,
conductorWrappedToken
);
} else {
throw Error("Wrapped conductor address is null");
}
}
return tokenDecimalsOnConductor;
} else {
return condtributedTokenDecimals;
}
}
export async function normalizeConversionRate(
denominationDecimals: number,
acceptedTokenDecimals: number,
conversionRate: string,
conductorDecimals: number
): Promise<ethers.BigNumberish> {
const precision = 18;
const normDecimals = denominationDecimals + precision - acceptedTokenDecimals;
let normalizedConversionRate = parseUnits(conversionRate, normDecimals);
if (acceptedTokenDecimals === conductorDecimals) {
return normalizedConversionRate;
} else if (acceptedTokenDecimals > conductorDecimals) {
return normalizedConversionRate.div(
parseUnits("1", acceptedTokenDecimals - conductorDecimals)
);
} else {
return normalizedConversionRate.mul(
parseUnits("1", conductorDecimals - acceptedTokenDecimals)
);
}
}

View File

@ -7,12 +7,14 @@ export async function registerChainOnEth(
conductorAddress: string,
contributorChain: ChainId,
contributorAddress: Uint8Array,
contributorCustodyAddress: Uint8Array,
wallet: ethers.Wallet
): Promise<ethers.ContractReceipt> {
const contributor = Conductor__factory.connect(conductorAddress, wallet);
const tx = await contributor.registerChain(
contributorChain,
contributorAddress
contributorAddress,
contributorCustodyAddress
);
return tx.wait();
}

View File

@ -2,15 +2,13 @@ import { ethers } from "ethers";
import { Contributor__factory } from "../ethers-contracts";
import { getSaleFromContributorOnEth } from "./getters";
import { getSaleIdFromIccoVaa } from "./signedVaa";
export async function saleAbortedOnEth(
contributorAddress: string,
signedVaa: Uint8Array,
wallet: ethers.Wallet
wallet: ethers.Wallet,
saleId: ethers.BigNumberish
): Promise<ethers.ContractReceipt> {
const saleId = await getSaleIdFromIccoVaa(signedVaa);
// save on gas by checking the state of the sale
const sale = await getSaleFromContributorOnEth(
contributorAddress,

View File

@ -2,15 +2,13 @@ import { ethers } from "ethers";
import { Contributor__factory } from "../ethers-contracts";
import { getSaleFromContributorOnEth } from "./getters";
import { getSaleIdFromIccoVaa } from "./signedVaa";
export async function saleSealedOnEth(
contributorAddress: string,
signedVaa: Uint8Array,
wallet: ethers.Wallet
wallet: ethers.Wallet,
saleId: ethers.BigNumberish
): Promise<ethers.ContractReceipt> {
const saleId = await getSaleIdFromIccoVaa(signedVaa);
// save on gas by checking the state of the sale
const sale = await getSaleFromContributorOnEth(
contributorAddress,

View File

@ -1,13 +1,4 @@
import { ethers } from "ethers";
import {
ChainId,
getEmitterAddressEth,
getSignedVAA,
getSignedVAAWithRetry,
parseSequencesFromLogEth,
} from "@certusone/wormhole-sdk";
import { ConductorSale, getTargetChainIdFromTransferVaa } from ".";
import { Conductor__factory } from "../ethers-contracts";
import { getSaleFromConductorOnEth } from "./getters";
@ -33,72 +24,3 @@ export async function sealSaleOnEth(
const tx = await conductor.sealSale(saleId);
return tx.wait();
}
export interface SealSaleResult {
sale: ConductorSale;
transferVaas: Map<ChainId, Uint8Array[]>;
sealSaleVaa: Uint8Array;
}
export async function sealSaleAndParseReceiptOnEth(
conductorAddress: string,
saleId: ethers.BigNumberish,
coreBridgeAddress: string,
tokenBridgeAddress: string,
wormholeHosts: string[],
extraGrpcOpts: any = {},
wallet: ethers.Wallet
): Promise<SealSaleResult> {
const receipt = await sealSaleOnEth(conductorAddress, saleId, wallet);
const sale = await getSaleFromConductorOnEth(
conductorAddress,
wallet.provider,
saleId
);
const emitterChain = sale.tokenChain as ChainId;
const sequences = parseSequencesFromLogEth(receipt, coreBridgeAddress);
const sealSaleSequence = sequences.pop();
if (sealSaleSequence === undefined) {
throw Error("no vaa sequences found");
}
const result = await getSignedVAAWithRetry(
wormholeHosts,
emitterChain,
getEmitterAddressEth(conductorAddress),
sealSaleSequence,
extraGrpcOpts
);
const sealSaleVaa = result.vaaBytes;
console.info("Seal Sale VAA:", Buffer.from(sealSaleVaa).toString("hex"));
// doing it serially for ease of putting into the map
const mapped = new Map<ChainId, Uint8Array[]>();
for (const sequence of sequences) {
const result = await getSignedVAAWithRetry(
wormholeHosts,
emitterChain,
getEmitterAddressEth(tokenBridgeAddress),
sequence,
extraGrpcOpts
);
const signedVaa = result.vaaBytes;
const chainId = await getTargetChainIdFromTransferVaa(signedVaa);
const signedVaas = mapped.get(chainId);
if (signedVaas === undefined) {
mapped.set(chainId, [signedVaa]);
} else {
signedVaas.push(signedVaa);
}
}
return {
sale: sale,
transferVaas: mapped,
sealSaleVaa: sealSaleVaa,
};
}

View File

@ -10,31 +10,19 @@ import { AcceptedToken, Allocation, SaleInit, SaleSealed } from "./structs";
const VAA_PAYLOAD_NUM_ACCEPTED_TOKENS = 227;
const VAA_PAYLOAD_ACCEPTED_TOKEN_BYTES_LENGTH = 50;
export async function extractVaaPayload(
signedVaa: Uint8Array
): Promise<Uint8Array> {
const { parse_vaa } = await importCoreWasm();
const { payload: payload } = parse_vaa(signedVaa);
return payload;
}
export async function getSaleIdFromIccoVaa(
signedVaa: Uint8Array
payload: Uint8Array
): Promise<ethers.BigNumberish> {
const payload = await extractVaaPayload(signedVaa);
return ethers.BigNumber.from(payload.slice(1, 33)).toString();
}
export async function getTargetChainIdFromTransferVaa(
signedVaa: Uint8Array
payload: Uint8Array
): Promise<ChainId> {
const payload = await extractVaaPayload(signedVaa);
return Buffer.from(payload).readUInt16BE(99) as ChainId;
}
export async function parseSaleInit(signedVaa: Uint8Array): Promise<SaleInit> {
const payload = await extractVaaPayload(signedVaa);
export async function parseSaleInit(payload: Uint8Array): Promise<SaleInit> {
const buffer = Buffer.from(payload);
const numAcceptedTokens = buffer.readUInt8(VAA_PAYLOAD_NUM_ACCEPTED_TOKENS);
@ -93,10 +81,8 @@ const VAA_PAYLOAD_NUM_ALLOCATIONS = 33;
const VAA_PAYLOAD_ALLOCATION_BYTES_LENGTH = 65;
export async function parseSaleSealed(
signedVaa: Uint8Array
payload: Uint8Array
): Promise<SaleSealed> {
const payload = await extractVaaPayload(signedVaa);
const buffer = Buffer.from(payload);
const numAllocations = buffer.readUInt8(VAA_PAYLOAD_NUM_ALLOCATIONS);

View File

@ -4,7 +4,8 @@ import { ChainId } from "@certusone/wormhole-sdk";
import { nativeToUint8Array } from "./misc";
export interface Raise {
token: string;
token: ethers.BytesLike;
tokenChain: ChainId;
tokenAmount: ethers.BigNumberish;
minRaise: ethers.BigNumberish;
maxRaise: ethers.BigNumberish;
@ -37,6 +38,7 @@ export interface Sale {
export interface ConductorSale extends Sale {
initiator: string;
localTokenAddress: string;
contributions: ethers.BigNumberish[];
contributionsCollected: boolean[];
refundIsClaimed: boolean;
@ -83,11 +85,11 @@ export interface SaleSealed {
export function makeAcceptedToken(
chainId: ChainId,
address: string,
conversion: string
conversion: ethers.BigNumberish
): AcceptedToken {
return {
tokenChain: chainId,
tokenAddress: nativeToUint8Array(address, chainId),
conversionRate: ethers.utils.parseUnits(conversion), // always 1e18
conversionRate: conversion,
};
}