tokenbridge: decimal shifting & max outstanding
Change-Id: Ia9f27f317fe08c1d8dbb9eaa60e53633acfdd381
This commit is contained in:
parent
14e892300c
commit
51e00dc1bf
|
@ -60,32 +60,77 @@ contract Bridge is BridgeGovernance {
|
|||
}(nonce, encoded, 15);
|
||||
}
|
||||
|
||||
function wrapAndTransferETH(uint16 recipientChain, bytes32 recipient, uint256 fee, uint32 nonce) public payable returns (uint64 sequence) {
|
||||
function wrapAndTransferETH(uint16 recipientChain, bytes32 recipient, uint256 arbiterFee, uint32 nonce) public payable returns (uint64 sequence) {
|
||||
uint wormholeFee = wormhole().messageFee();
|
||||
|
||||
require(wormholeFee < msg.value, "value is smaller than wormhole fee");
|
||||
|
||||
uint amount = msg.value - wormholeFee;
|
||||
|
||||
require(arbiterFee <= amount, "fee is bigger than amount minus wormhole fee");
|
||||
|
||||
uint normalizedAmount = amount / (10**10);
|
||||
uint normalizedArbiterFee = arbiterFee / (10**10);
|
||||
|
||||
// refund dust
|
||||
uint dust = amount - (normalizedAmount * (10**10));
|
||||
if (dust > 0) {
|
||||
payable(msg.sender).transfer(dust);
|
||||
}
|
||||
|
||||
// deposit into WETH
|
||||
WETH().deposit{
|
||||
value : msg.value - wormholeFee
|
||||
value : amount - dust
|
||||
}();
|
||||
|
||||
sequence = logTransfer(chainId(), bytes32(uint256(uint160(address(WETH())))), msg.value, recipientChain, recipient, fee, wormholeFee, nonce);
|
||||
// track and check outstanding token amounts
|
||||
bridgeOut(address(WETH()), normalizedAmount);
|
||||
|
||||
sequence = logTransfer(chainId(), bytes32(uint256(uint160(address(WETH())))), normalizedAmount, recipientChain, recipient, normalizedArbiterFee, wormholeFee, nonce);
|
||||
}
|
||||
|
||||
// Initiate a Transfer
|
||||
function transferTokens(uint16 tokenChain, bytes32 tokenAddress, uint256 amount, uint16 recipientChain, bytes32 recipient, uint256 fee, uint32 nonce) public payable returns (uint64 sequence) {
|
||||
if(tokenChain == chainId()){
|
||||
SafeERC20.safeTransferFrom(IERC20(address(uint160(uint256(tokenAddress)))), msg.sender, address(this), amount);
|
||||
function transferTokens(address token, uint256 amount, uint16 recipientChain, bytes32 recipient, uint256 arbiterFee, uint32 nonce) public payable returns (uint64 sequence) {
|
||||
// determine token parameters
|
||||
uint16 tokenChain;
|
||||
bytes32 tokenAddress;
|
||||
if(isWrappedAsset(token)){
|
||||
tokenChain = TokenImplementation(token).chainId();
|
||||
tokenAddress = TokenImplementation(token).nativeContract();
|
||||
}else{
|
||||
address wrapped = wrappedAsset(tokenChain, tokenAddress);
|
||||
require(wrapped != address(0), "no wrapper for this token created yet");
|
||||
|
||||
SafeERC20.safeTransferFrom(IERC20(wrapped), msg.sender, address(this), amount);
|
||||
|
||||
TokenImplementation(wrapped).burn(address(this), amount);
|
||||
tokenChain = chainId();
|
||||
tokenAddress = bytes32(uint256(uint160(token)));
|
||||
}
|
||||
|
||||
sequence = logTransfer(tokenChain, tokenAddress, amount, recipientChain, recipient, fee, msg.value, nonce);
|
||||
// query tokens decimals
|
||||
(,bytes memory queriedDecimals) = token.staticcall(abi.encodeWithSignature("decimals()"));
|
||||
uint8 decimals = abi.decode(queriedDecimals, (uint8));
|
||||
|
||||
// adjust decimals
|
||||
uint256 normalizedAmount = amount;
|
||||
uint256 normalizedArbiterFee = arbiterFee;
|
||||
if(decimals > 8) {
|
||||
uint multiplier = 10**(decimals - 8);
|
||||
|
||||
normalizedAmount /= multiplier;
|
||||
normalizedArbiterFee /= multiplier;
|
||||
|
||||
// don't deposit dust that can not be bridged due to the decimal shift
|
||||
amount = normalizedAmount * multiplier;
|
||||
}
|
||||
|
||||
if(tokenChain == chainId()){
|
||||
SafeERC20.safeTransferFrom(IERC20(token), msg.sender, address(this), amount);
|
||||
|
||||
// track and check outstanding token amounts
|
||||
bridgeOut(token, normalizedAmount);
|
||||
} else {
|
||||
SafeERC20.safeTransferFrom(IERC20(token), msg.sender, address(this), amount);
|
||||
|
||||
TokenImplementation(token).burn(address(this), amount);
|
||||
}
|
||||
|
||||
sequence = logTransfer(tokenChain, tokenAddress, normalizedAmount, recipientChain, recipient, normalizedArbiterFee, msg.value, nonce);
|
||||
}
|
||||
|
||||
function logTransfer(uint16 tokenChain, bytes32 tokenAddress, uint256 amount, uint16 recipientChain, bytes32 recipient, uint256 fee, uint256 callValue, uint32 nonce) internal returns (uint64 sequence) {
|
||||
|
@ -180,39 +225,72 @@ contract Bridge is BridgeGovernance {
|
|||
IERC20 transferToken;
|
||||
if(transfer.tokenChain == chainId()){
|
||||
transferToken = IERC20(address(uint160(uint256(transfer.tokenAddress))));
|
||||
|
||||
// track outstanding token amounts
|
||||
bridgedIn(address(transferToken), transfer.amount);
|
||||
} else {
|
||||
address wrapped = wrappedAsset(transfer.tokenChain, transfer.tokenAddress);
|
||||
require(wrapped != address(0), "no wrapper for this token created yet");
|
||||
|
||||
TokenImplementation(wrapped).mint(address(this), transfer.amount);
|
||||
|
||||
transferToken = IERC20(wrapped);
|
||||
}
|
||||
|
||||
if(transfer.fee > 0) {
|
||||
require(transfer.fee <= transfer.amount, "fee higher than transferred amount");
|
||||
require(unwrapWETH == false || address(transferToken) == address(WETH()), "invalid token, can only unwrap WETH");
|
||||
|
||||
// query decimals
|
||||
(,bytes memory queriedDecimals) = address(transferToken).staticcall(abi.encodeWithSignature("decimals()"));
|
||||
uint8 decimals = abi.decode(queriedDecimals, (uint8));
|
||||
|
||||
// adjust decimals
|
||||
uint256 nativeAmount = transfer.amount;
|
||||
uint256 nativeFee = transfer.fee;
|
||||
if(decimals > 8) {
|
||||
uint multiplier = 10**(decimals - 8);
|
||||
nativeAmount *= multiplier;
|
||||
nativeFee *= multiplier;
|
||||
}
|
||||
|
||||
// mint wrapped asset
|
||||
if(transfer.tokenChain != chainId()) {
|
||||
TokenImplementation(address(transferToken)).mint(address(this), nativeAmount);
|
||||
}
|
||||
|
||||
// transfer fee to arbiter
|
||||
if(nativeFee > 0) {
|
||||
require(nativeFee <= nativeAmount, "fee higher than transferred amount");
|
||||
|
||||
if (unwrapWETH) {
|
||||
require(address(transferToken) == address(WETH()), "invalid token, can only unwrap ETH");
|
||||
WETH().withdraw(transfer.fee);
|
||||
payable(msg.sender).transfer(transfer.fee);
|
||||
WETH().withdraw(nativeFee);
|
||||
|
||||
payable(msg.sender).transfer(nativeFee);
|
||||
} else {
|
||||
SafeERC20.safeTransfer(transferToken, msg.sender, transfer.fee);
|
||||
SafeERC20.safeTransfer(transferToken, msg.sender, nativeFee);
|
||||
}
|
||||
}
|
||||
|
||||
uint transferAmount = transfer.amount - transfer.fee;
|
||||
address payable transferRecipient = payable(address(uint160(uint256(transfer.to))));
|
||||
// transfer bridged amount to recipient
|
||||
uint transferAmount = nativeAmount - nativeFee;
|
||||
address transferRecipient = address(uint160(uint256(transfer.to)));
|
||||
|
||||
if (unwrapWETH) {
|
||||
require(address(transferToken) == address(WETH()), "invalid token, can only unwrap ETH");
|
||||
WETH().withdraw(transferAmount);
|
||||
transferRecipient.transfer(transferAmount);
|
||||
|
||||
payable(transferRecipient).transfer(transferAmount);
|
||||
} else {
|
||||
SafeERC20.safeTransfer(transferToken, transferRecipient, transferAmount);
|
||||
}
|
||||
}
|
||||
|
||||
function bridgeOut(address token, uint normalizedAmount) internal {
|
||||
uint outstanding = outstandingBridged(token);
|
||||
require(outstanding + normalizedAmount <= type(uint64).max, "transfer exceeds max outstanding bridged token amount");
|
||||
setOutstandingBridged(token, outstanding + normalizedAmount);
|
||||
}
|
||||
|
||||
function bridgedIn(address token, uint normalizedAmount) internal {
|
||||
setOutstandingBridged(token, outstandingBridged(token) - normalizedAmount);
|
||||
}
|
||||
|
||||
function verifyBridgeVM(IWormhole.VM memory vm) internal view returns (bool){
|
||||
if (bridgeContracts(vm.emitterChainId) == vm.emitterAddress) {
|
||||
return true;
|
||||
|
|
|
@ -53,6 +53,14 @@ contract BridgeGetters is BridgeState {
|
|||
function WETH() public view returns (IWETH){
|
||||
return IWETH(_state.provider.WETH);
|
||||
}
|
||||
|
||||
function outstandingBridged(address token) public view returns (uint256){
|
||||
return _state.outstandingBridged[token];
|
||||
}
|
||||
|
||||
function isWrappedAsset(address token) public view returns (bool){
|
||||
return _state.isWrappedAsset[token];
|
||||
}
|
||||
}
|
||||
|
||||
interface IWETH is IERC20 {
|
||||
|
|
|
@ -48,5 +48,10 @@ contract BridgeSetters is BridgeState {
|
|||
|
||||
function setWrappedAsset(uint16 tokenChainId, bytes32 tokenAddress, address wrapper) internal {
|
||||
_state.wrappedAssets[tokenChainId][tokenAddress] = wrapper;
|
||||
_state.isWrappedAsset[wrapper] = true;
|
||||
}
|
||||
|
||||
function setOutstandingBridged(address token, uint256 outstanding) internal {
|
||||
_state.outstandingBridged[token] = outstanding;
|
||||
}
|
||||
}
|
|
@ -13,6 +13,11 @@ contract BridgeStorage {
|
|||
address WETH;
|
||||
}
|
||||
|
||||
struct Asset {
|
||||
uint16 chainId;
|
||||
bytes32 assetAddress;
|
||||
}
|
||||
|
||||
struct State {
|
||||
address payable wormhole;
|
||||
address tokenImplementation;
|
||||
|
@ -31,6 +36,12 @@ contract BridgeStorage {
|
|||
// Mapping of wrapped assets (chainID => nativeAddress => wrappedAddress)
|
||||
mapping(uint16 => mapping(bytes32 => address)) wrappedAssets;
|
||||
|
||||
// Mapping to safely identify wrapped assets
|
||||
mapping(address => bool) isWrappedAsset;
|
||||
|
||||
// Mapping of native assets to amount outstanding on other chains
|
||||
mapping(address => uint256) outstandingBridged;
|
||||
|
||||
// Mapping of bridge contracts on other chains
|
||||
mapping(uint16 => bytes32) bridgeImplementations;
|
||||
}
|
||||
|
|
|
@ -303,6 +303,8 @@ contract("Bridge", function () {
|
|||
|
||||
const wrappedAddress = await initialized.methods.wrappedAsset("0x0001", "0x000000000000000000000000b7a2211e8165943192ad04f5dd21bedc29ff003e").call();
|
||||
|
||||
assert.ok(await initialized.methods.isWrappedAsset(wrappedAddress).call())
|
||||
|
||||
const initializedWrappedAsset = new web3.eth.Contract(TokenImplementation.abi, wrappedAddress);
|
||||
|
||||
const symbol = await initializedWrappedAsset.methods.symbol().call();
|
||||
|
@ -324,6 +326,7 @@ contract("Bridge", function () {
|
|||
it("should deposit and log transfers correctly", async function() {
|
||||
const accounts = await web3.eth.getAccounts();
|
||||
const amount = "1000000000000000000";
|
||||
const fee = "100000000000000000";
|
||||
|
||||
// mint and approve tokens
|
||||
const token = new web3.eth.Contract(TokenImplementation.abi, TokenImplementation.address);
|
||||
|
@ -348,12 +351,11 @@ contract("Bridge", function () {
|
|||
assert.equal(bridgeBalanceBefore.toString(10), "0");
|
||||
|
||||
await initialized.methods.transferTokens(
|
||||
testChainId,
|
||||
web3.eth.abi.encodeParameter("address", TokenImplementation.address),
|
||||
TokenImplementation.address,
|
||||
amount,
|
||||
"10",
|
||||
"0x000000000000000000000000b7a2211e8165943192ad04f5dd21bedc29ff003e",
|
||||
"123",
|
||||
fee,
|
||||
"234"
|
||||
).send({
|
||||
value : 0,
|
||||
|
@ -381,7 +383,7 @@ contract("Bridge", function () {
|
|||
assert.equal(log.payload.substr(2, 2), "01");
|
||||
|
||||
// amount
|
||||
assert.equal(log.payload.substr(4, 64), web3.eth.abi.encodeParameter("uint256", amount).substring(2));
|
||||
assert.equal(log.payload.substr(4, 64), web3.eth.abi.encodeParameter("uint256", new BigNumber(amount).div(1e10).toString()).substring(2));
|
||||
|
||||
// token
|
||||
assert.equal(log.payload.substr(68, 64), web3.eth.abi.encodeParameter("address", TokenImplementation.address).substring(2));
|
||||
|
@ -396,7 +398,7 @@ contract("Bridge", function () {
|
|||
assert.equal(log.payload.substr(200, 4), web3.eth.abi.encodeParameter("uint16", 10).substring(2 + 64 - 4))
|
||||
|
||||
// fee
|
||||
assert.equal(log.payload.substr(204, 64), web3.eth.abi.encodeParameter("uint256", 123).substring(2))
|
||||
assert.equal(log.payload.substr(204, 64), web3.eth.abi.encodeParameter("uint256", new BigNumber(fee).div(1e10).toString()).substring(2))
|
||||
})
|
||||
|
||||
it("should transfer out locked assets for a valid transfer vm", async function() {
|
||||
|
@ -416,7 +418,7 @@ contract("Bridge", function () {
|
|||
const data = "0x" +
|
||||
"01" +
|
||||
// amount
|
||||
"0000000000000000000000000000000000000000000000000de0b6b3a7640000" +
|
||||
web3.eth.abi.encodeParameter("uint256", new BigNumber(amount).div(1e10).toString()).substring(2) +
|
||||
// tokenaddress
|
||||
web3.eth.abi.encodeParameter("address", TokenImplementation.address).substr(2) +
|
||||
// tokenchain
|
||||
|
@ -428,8 +430,6 @@ contract("Bridge", function () {
|
|||
// fee
|
||||
"0000000000000000000000000000000000000000000000000000000000000000";
|
||||
|
||||
// console.log(data)
|
||||
|
||||
const vm = await signAndEncodeVM(
|
||||
0,
|
||||
0,
|
||||
|
@ -473,7 +473,7 @@ contract("Bridge", function () {
|
|||
const data = "0x" +
|
||||
"01" +
|
||||
// amount
|
||||
"0000000000000000000000000000000000000000000000000de0b6b3a7640000" +
|
||||
web3.eth.abi.encodeParameter("uint256", new BigNumber(amount).div(1e10).toString()).substring(2) +
|
||||
// tokenaddress
|
||||
testBridgedAssetAddress +
|
||||
// tokenchain
|
||||
|
@ -534,8 +534,7 @@ contract("Bridge", function () {
|
|||
assert.equal(accountBalanceBefore.toString(10), amount);
|
||||
|
||||
await initialized.methods.transferTokens(
|
||||
"0x"+testBridgedAssetChain,
|
||||
"0x"+testBridgedAssetAddress,
|
||||
wrappedAddress,
|
||||
amount,
|
||||
"11",
|
||||
"0x000000000000000000000000b7a2211e8165943192ad04f5dd21bedc29ff003e",
|
||||
|
@ -560,6 +559,7 @@ contract("Bridge", function () {
|
|||
it("should handle ETH deposits correctly", async function() {
|
||||
const accounts = await web3.eth.getAccounts();
|
||||
const amount = "100000000000000000";
|
||||
const fee = "10000000000000000";
|
||||
|
||||
// mint and approve tokens
|
||||
WETH = (await MockWETH9.new()).address;
|
||||
|
@ -584,7 +584,7 @@ contract("Bridge", function () {
|
|||
await initialized.methods.wrapAndTransferETH(
|
||||
"10",
|
||||
"0x000000000000000000000000b7a2211e8165943192ad04f5dd21bedc29ff003e",
|
||||
"123",
|
||||
fee,
|
||||
"234"
|
||||
).send({
|
||||
value : amount,
|
||||
|
@ -612,7 +612,7 @@ contract("Bridge", function () {
|
|||
assert.equal(log.payload.substr(2, 2), "01");
|
||||
|
||||
// amount
|
||||
assert.equal(log.payload.substr(4, 64), web3.eth.abi.encodeParameter("uint256", amount).substring(2));
|
||||
assert.equal(log.payload.substr(4, 64), web3.eth.abi.encodeParameter("uint256", new BigNumber(amount).div(1e10).toString()).substring(2));
|
||||
|
||||
// token
|
||||
assert.equal(log.payload.substr(68, 64), web3.eth.abi.encodeParameter("address", WETH).substring(2));
|
||||
|
@ -627,7 +627,7 @@ contract("Bridge", function () {
|
|||
assert.equal(log.payload.substr(200, 4), web3.eth.abi.encodeParameter("uint16", 10).substring(2 + 64 - 4))
|
||||
|
||||
// fee
|
||||
assert.equal(log.payload.substr(204, 64), web3.eth.abi.encodeParameter("uint256", 123).substring(2))
|
||||
assert.equal(log.payload.substr(204, 64), web3.eth.abi.encodeParameter("uint256", new BigNumber(fee).div(1e10).toString()).substring(2))
|
||||
})
|
||||
|
||||
it("should handle ETH withdrawals and fees correctly", async function() {
|
||||
|
@ -649,7 +649,7 @@ contract("Bridge", function () {
|
|||
const data = "0x" +
|
||||
"01" +
|
||||
// amount
|
||||
web3.eth.abi.encodeParameter("uint256", amount).substr(2) +
|
||||
web3.eth.abi.encodeParameter("uint256", new BigNumber(amount).div(1e10).toString()).substring(2) +
|
||||
// tokenaddress
|
||||
web3.eth.abi.encodeParameter("address", WETH).substr(2) +
|
||||
// tokenchain
|
||||
|
@ -659,7 +659,7 @@ contract("Bridge", function () {
|
|||
// receiving chain
|
||||
web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + (64 - 4)) +
|
||||
// fee
|
||||
web3.eth.abi.encodeParameter("uint256", fee).substr(2);
|
||||
web3.eth.abi.encodeParameter("uint256", new BigNumber(fee).div(1e10).toString()).substring(2);
|
||||
|
||||
const vm = await signAndEncodeVM(
|
||||
0,
|
||||
|
@ -689,6 +689,62 @@ contract("Bridge", function () {
|
|||
assert.equal((new BigNumber(accountBalanceAfter)).minus(accountBalanceBefore).toString(10), (new BigNumber(amount)).minus(fee).toString(10))
|
||||
assert.ok((new BigNumber(feeRecipientBalanceAfter)).gt(feeRecipientBalanceBefore))
|
||||
})
|
||||
|
||||
it("should revert on transfer out of a total of > max(uint64) tokens", async function() {
|
||||
const accounts = await web3.eth.getAccounts();
|
||||
const supply = "184467440737095516160000000000";
|
||||
const firstTransfer = "1000000000000";
|
||||
|
||||
// mint and approve tokens
|
||||
const token = new web3.eth.Contract(TokenImplementation.abi, TokenImplementation.address);
|
||||
await token.methods.mint(accounts[0], supply).send({
|
||||
value : 0,
|
||||
from : accounts[0],
|
||||
gasLimit : 2000000
|
||||
});
|
||||
await token.methods.approve(TokenBridge.address, supply).send({
|
||||
value : 0,
|
||||
from : accounts[0],
|
||||
gasLimit : 2000000
|
||||
});
|
||||
|
||||
// deposit tokens
|
||||
const initialized = new web3.eth.Contract(BridgeImplementationFullABI, TokenBridge.address);
|
||||
|
||||
await initialized.methods.transferTokens(
|
||||
TokenImplementation.address,
|
||||
firstTransfer,
|
||||
"10",
|
||||
"0x000000000000000000000000b7a2211e8165943192ad04f5dd21bedc29ff003e",
|
||||
"0",
|
||||
"0"
|
||||
).send({
|
||||
value : 0,
|
||||
from : accounts[0],
|
||||
gasLimit : 2000000
|
||||
});
|
||||
|
||||
let failed = false;
|
||||
try {
|
||||
await initialized.methods.transferTokens(
|
||||
TokenImplementation.address,
|
||||
new BigNumber(supply).minus(firstTransfer).toString(10),
|
||||
"10",
|
||||
"0x000000000000000000000000b7a2211e8165943192ad04f5dd21bedc29ff003e",
|
||||
"0",
|
||||
"0"
|
||||
).send({
|
||||
value : 0,
|
||||
from : accounts[0],
|
||||
gasLimit : 2000000
|
||||
});
|
||||
} catch(error) {
|
||||
assert.equal(error.message, "Returned error: VM Exception while processing transaction: revert transfer exceeds max outstanding bridged token amount")
|
||||
failed = true
|
||||
}
|
||||
|
||||
assert.ok(failed)
|
||||
})
|
||||
});
|
||||
|
||||
const signAndEncodeVM = async function (
|
||||
|
|
Loading…
Reference in New Issue