Skip to content
This repository has been archived by the owner on Oct 8, 2024. It is now read-only.

Add wormhole bridge #59

Open
wants to merge 69 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
00158b8
Add prototype of wormhole bridge
krzkaczor Nov 9, 2021
06ce967
Start extracting WormholeGUID lib
krzkaczor Nov 12, 2021
e099a69
Extract WormholeGUID as lib
krzkaczor Nov 16, 2021
b76d3e6
Remove close features
krzkaczor Nov 16, 2021
849942f
Update interfaces
krzkaczor Nov 19, 2021
795e4da
Update contract name
krzkaczor Nov 22, 2021
3752c0c
Add basic test scaffolding
krzkaczor Nov 22, 2021
8d6a8f1
Adjust import paths
krzkaczor Nov 22, 2021
a65eb79
Add flush() test
krzkaczor Nov 23, 2021
49a2ea4
Fix code review comments
krzkaczor Nov 30, 2021
91716f2
Rename methods
olivdb Dec 2, 2021
be6b7c4
Merge pull request #62 from makerdao/oli/wormhole-bridge-fix
olivdb Dec 2, 2021
5c094ce
Address Sam's review comments
olivdb Dec 10, 2021
77284b7
Remove unused WormholeLib.getHash
olivdb Dec 10, 2021
c5a512a
Remove unused dependency
olivdb Dec 10, 2021
bfe5d03
Let flush be callable for 0 DAI settlements
olivdb Dec 10, 2021
618e27f
Merge pull request #63 from makerdao/oli/wh-bridge-fix
olivdb Dec 14, 2021
a308ec5
Add initWormhole tests
olivdb Dec 14, 2021
08993ac
Remove unused auth from L2DaiWormholeBridge
olivdb Dec 14, 2021
40a9391
Add L2DaiWormholeBridge tests
olivdb Dec 14, 2021
380fd86
Remove unused auth from L1DAIWormholeBridge
olivdb Dec 14, 2021
bf13328
Fix and tests for L1DAIWormholeBridge
olivdb Dec 15, 2021
5e18600
Rename constants
olivdb Dec 16, 2021
2754466
Add comments
olivdb Dec 17, 2021
b01234d
Revert "Remove unused auth from L2DaiWormholeBridge"
olivdb Dec 17, 2021
49df71b
Add close() to L2DaiWormholeBridge + auth tests
olivdb Dec 17, 2021
83a82e2
Remove mocking of router DAI transfer
olivdb Dec 17, 2021
b50934d
Add xchain revert tests to L1DAIWOrmholeBridge
olivdb Dec 17, 2021
9923b09
Add check for L1DAIWormholeBridge constructor
olivdb Dec 17, 2021
b2621cf
Cleanup
olivdb Dec 17, 2021
595cdec
Merge pull request #64 from makerdao/oli/moar-tests
olivdb Dec 17, 2021
2c3fbbc
Restrict Flush and Add Target Domain Whitelist (#65)
hexonaut Dec 17, 2021
eae61d7
Index targetDomain in Flushed event
olivdb Jan 4, 2022
24e8492
Merge pull request #67 from makerdao/oli/audit-fix
olivdb Jan 5, 2022
73d0e18
Add coverage
olivdb Jan 11, 2022
c924762
Improve L2DaiWormholeBridge coverage
olivdb Jan 11, 2022
ea97e16
Add coverage command
olivdb Jan 11, 2022
e92acb7
Skip test directory
olivdb Jan 11, 2022
212ffeb
Add domain separator test for l2Dai
olivdb Jan 11, 2022
906ea39
Add istanbul checks
olivdb Jan 11, 2022
8c9b9b0
Archive coverage artifacts
olivdb Jan 11, 2022
d2c41f9
Merge pull request #69 from makerdao/oli/coverage
olivdb Jan 12, 2022
bcac2da
Fix approve interface
olivdb Jan 13, 2022
f0e688c
Address slither issues
olivdb Jan 13, 2022
5086ac2
Merge pull request #70 from makerdao/oli/interface-fix
olivdb Jan 13, 2022
50fad1e
Change address to bytes32 in WormholeGUID struct
krzkaczor Jan 13, 2022
ce61379
Merge pull request #71 from makerdao/kk/address-bytes32
krzkaczor Jan 14, 2022
46a3656
Optimize calldata for common cases
krzkaczor Jan 19, 2022
6aba333
Merge pull request #72 from makerdao/kk/optimize-calldata
krzkaczor Jan 20, 2022
8bc48a8
Update router interface
krzkaczor Jan 25, 2022
50635b2
Merge pull request #73 from makerdao/kk/update-interface
krzkaczor Jan 25, 2022
680a536
Remove operator fee
krzkaczor Jan 28, 2022
c11e87b
Merge pull request #74 from makerdao/kk/remove-operator-fee
krzkaczor Jan 28, 2022
cd35af7
Revert "Remove operator fee"
krzkaczor Feb 3, 2022
a03f13a
Merge pull request #75 from makerdao/kk/operator-fee
krzkaczor Feb 3, 2022
e1c7698
Fix router interface
olivdb Feb 16, 2022
4f93de2
Merge pull request #77 from makerdao/update-router-interface
olivdb Feb 16, 2022
da0c874
Adjust naming of l1 wormhole public variables
krzkaczor Mar 9, 2022
5318012
Merge pull request #78 from makerdao/kk/adjust-naming
krzkaczor Mar 10, 2022
6a4c301
Extract interfaces for L1
krzkaczor Mar 17, 2022
676c343
Extract interfaces for L2
krzkaczor Mar 17, 2022
cd3aecc
Tweak router interface name
krzkaczor Mar 17, 2022
962ece1
Rename WormholeBridge to WormholeGateway
krzkaczor Mar 17, 2022
adc8723
Move slither changes
krzkaczor Mar 17, 2022
1cf3ea0
Merge pull request #79 from makerdao/kk/extract-common-interfaces
krzkaczor Mar 17, 2022
6adcb7b
Rename wormhole gateway to LxDaiWormholeGateway
krzkaczor Mar 20, 2022
9907e06
Merge pull request #80 from makerdao/kk/rename-wormhole-gateway
krzkaczor Mar 21, 2022
30d7d57
Add readme security note
krzkaczor Apr 26, 2022
d20e538
Merge pull request #82 from makerdao/kk/security-note
krzkaczor May 12, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions contracts/common/LibWormholeGUID.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
pragma solidity >=0.7.6;
pragma abicoder v2;

struct WormholeGUID {
bytes32 sourceDomain;
bytes32 targetDomain;
address receiver;
address operator;
uint128 amount;
uint64 nonce;
uint64 timestamp;
hexonaut marked this conversation as resolved.
Show resolved Hide resolved
}

library WormholeLib {
function getHash(WormholeGUID memory wormhole) public pure returns (bytes32) {
return
keccak256(
abi.encode(
wormhole.sourceDomain,
wormhole.targetDomain,
wormhole.receiver,
wormhole.operator,
wormhole.amount,
wormhole.nonce,
wormhole.timestamp
)
);
}
}
100 changes: 100 additions & 0 deletions contracts/l1/L1DAIWormholeBridge.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2021 Dai Foundation
// @unsupported: ovm
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

pragma solidity >=0.7.6;
pragma abicoder v2;

import {iOVM_L1ERC20Bridge} from "@eth-optimism/contracts/iOVM/bridge/tokens/iOVM_L1ERC20Bridge.sol";
import {iOVM_L2ERC20Bridge} from "@eth-optimism/contracts/iOVM/bridge/tokens/iOVM_L2ERC20Bridge.sol";
import {OVM_CrossDomainEnabled} from "@eth-optimism/contracts/libraries/bridge/OVM_CrossDomainEnabled.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {WormholeGUID, WormholeLib} from "../common/LibWormholeGUID.sol";

interface TokenLike {
krzkaczor marked this conversation as resolved.
Show resolved Hide resolved
function transferFrom(
address _from,
address _to,
uint256 _value
) external returns (bool success);
}

interface WormholeRouter {
function registerWormhole(WormholeGUID memory wormholeGUID) external; // @todo what's the memory location?
krzkaczor marked this conversation as resolved.
Show resolved Hide resolved

function settle(bytes32 targetDomain, uint256 daiToFlush) external;
}

contract L1DAIWormholeBridge is OVM_CrossDomainEnabled {
using WormholeLib for WormholeGUID;
// --- Auth ---
mapping(address => uint256) public wards;

function rely(address usr) external auth {
wards[usr] = 1;
emit Rely(usr);
}

function deny(address usr) external auth {
wards[usr] = 0;
emit Deny(usr);
}

modifier auth() {
require(wards[msg.sender] == 1, "L1DAITokenBridge/not-authorized");
hexonaut marked this conversation as resolved.
Show resolved Hide resolved
_;
}

event Rely(address indexed usr);
event Deny(address indexed usr);

address public immutable l1Token;
address public immutable l2DAITokenBridge;
hexonaut marked this conversation as resolved.
Show resolved Hide resolved
address public immutable l2Token;
hexonaut marked this conversation as resolved.
Show resolved Hide resolved
address public immutable escrow;
WormholeRouter public immutable wormholeRouter;

constructor(
address _l1Token,
address _l2DAITokenBridge,
address _l2Token,
address _l1messenger,
address _escrow,
address _wormholeRouter
) OVM_CrossDomainEnabled(_l1messenger) {
wards[msg.sender] = 1;
emit Rely(msg.sender);

l1Token = _l1Token;
l2DAITokenBridge = _l2DAITokenBridge;
l2Token = _l2Token;
escrow = _escrow;
wormholeRouter = WormholeRouter(_wormholeRouter);
}

function finalizeFlush(bytes32 targetDomain, uint256 daiToFlush)
external
onlyFromCrossDomainAccount(l2DAITokenBridge)
{
wormholeRouter.settle(targetDomain, daiToFlush);
}
hexonaut marked this conversation as resolved.
Show resolved Hide resolved

function finalizeRegisterInboundWormhole(WormholeGUID calldata wormhole)
external
onlyFromCrossDomainAccount(l2DAITokenBridge)
{
wormholeRouter.registerWormhole(wormhole);
olivdb marked this conversation as resolved.
Show resolved Hide resolved
}
}
124 changes: 124 additions & 0 deletions contracts/l2/L2DAIWormholeBridge.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2021 Dai Foundation
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

pragma solidity >=0.7.6;
pragma abicoder v2;

import {iOVM_L1ERC20Bridge} from "@eth-optimism/contracts/iOVM/bridge/tokens/iOVM_L1ERC20Bridge.sol";
import {iOVM_L2ERC20Bridge} from "@eth-optimism/contracts/iOVM/bridge/tokens/iOVM_L2ERC20Bridge.sol";
import {OVM_CrossDomainEnabled} from "@eth-optimism/contracts/libraries/bridge/OVM_CrossDomainEnabled.sol";
import {WormholeGUID, WormholeLib} from "../common/LibWormholeGUID.sol";
import {L1DAIWormholeBridge} from "../l1/L1DAIWormholeBridge.sol";

interface Mintable {
function mint(address usr, uint256 wad) external;

function burn(address usr, uint256 wad) external;
}

contract L2DAIWormholeBridge is OVM_CrossDomainEnabled {
using WormholeLib for WormholeGUID;

// --- Auth ---
mapping(address => uint256) public wards;

function rely(address usr) external auth {
wards[usr] = 1;
emit Rely(usr);
}

function deny(address usr) external auth {
wards[usr] = 0;
emit Deny(usr);
}

modifier auth() {
require(wards[msg.sender] == 1, "L2DAITokenBridge/not-authorized");
hexonaut marked this conversation as resolved.
Show resolved Hide resolved
_;
}

event Rely(address indexed usr);
event Deny(address indexed usr);

address public immutable l1Token;
hexonaut marked this conversation as resolved.
Show resolved Hide resolved
address public immutable l2Token;
address public immutable l1DAITokenBridge;
bytes32 public immutable sourceDomain;
hexonaut marked this conversation as resolved.
Show resolved Hide resolved
uint64 public nonce = 0;
mapping(bytes32 => uint256) public batchedDaiToFlush;

event WormholeInitialized(WormholeGUID wormhole);
event Flushed(bytes32 targetDomain, uint256 dai);

constructor(
address _l2CrossDomainMessenger,
address _l2Token,
address _l1Token,
address _l1DAITokenBridge,
bytes32 _sourceDomain
) public OVM_CrossDomainEnabled(_l2CrossDomainMessenger) {
wards[msg.sender] = 1;
emit Rely(msg.sender);

l2Token = _l2Token;
l1Token = _l1Token;
l1DAITokenBridge = _l1DAITokenBridge;
sourceDomain = _sourceDomain;
}

function initiateWormhole(
bytes32 targetDomain,
address receiver,
uint128 amount,
address operator
) external {
WormholeGUID memory wormhole = WormholeGUID({
sourceDomain: sourceDomain,
targetDomain: targetDomain,
receiver: receiver,
operator: operator,
amount: amount,
nonce: nonce++,
krzkaczor marked this conversation as resolved.
Show resolved Hide resolved
timestamp: uint64(block.timestamp)
});

batchedDaiToFlush[targetDomain] += amount;
hexonaut marked this conversation as resolved.
Show resolved Hide resolved
Mintable(l2Token).burn(msg.sender, amount);

uint32 l1Gas = 20000; // @todo: does it matter? messages need to be relied manually anyway
hexonaut marked this conversation as resolved.
Show resolved Hide resolved
bytes memory message = abi.encodeWithSelector(
L1DAIWormholeBridge.finalizeRegisterInboundWormhole.selector,
wormhole
);
sendCrossDomainMessage(l1DAITokenBridge, l1Gas, message);

emit WormholeInitialized(wormhole);
}

function flush(bytes32 targetDomain) external {
uint256 daiToFlush = batchedDaiToFlush[targetDomain];
hexonaut marked this conversation as resolved.
Show resolved Hide resolved

uint32 l1Gas = 20000; // @todo: does it matter? messages need to be relied manually anyway
bytes memory message = abi.encodeWithSelector(
L1DAIWormholeBridge.finalizeFlush.selector,
targetDomain,
daiToFlush
);
sendCrossDomainMessage(l1DAITokenBridge, l1Gas, message);

batchedDaiToFlush[targetDomain] = 0;
hexonaut marked this conversation as resolved.
Show resolved Hide resolved
emit Flushed(targetDomain, daiToFlush);
}
}
83 changes: 83 additions & 0 deletions test/l2/L2DAIWormholeBridge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { simpleDeploy } from '@makerdao/hardhat-utils'
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address'
import { expect } from 'chai'
import { ethers } from 'hardhat'

import { Dai__factory, L2DAIWormholeBridge__factory } from '../../typechain-types'
import { deployMock, deployOptimismContractMock } from '../helpers'

const INITIAL_TOTAL_L2_SUPPLY = 3000
const WORMHOLE_AMOUNT = 100
const DEFAULT_XDOMAIN_GAS = 20000
const SOURCE_DOMAIN_NAME = ethers.utils.formatBytes32String('optimism-a')
const TARGET_DOMAIN_NAME = ethers.utils.formatBytes32String('arbitrum-a')

describe('L2DAIWormholeBridge', () => {
describe('initiateWormhole()', () => {
it('burns DAI immediately and marks it for future flush', async () => {
const [_, l2MessengerImpersonator, user1] = await ethers.getSigners()
const { l2Dai, l2DAIWormholeBridge } = await setupTest({ l2MessengerImpersonator, user1 })

await l2DAIWormholeBridge
.connect(user1)
.initiateWormhole(TARGET_DOMAIN_NAME, user1.address, WORMHOLE_AMOUNT, user1.address)

expect(await l2Dai.balanceOf(user1.address)).to.eq(INITIAL_TOTAL_L2_SUPPLY - WORMHOLE_AMOUNT)
expect(await l2DAIWormholeBridge.batchedDaiToFlush(TARGET_DOMAIN_NAME)).to.eq(WORMHOLE_AMOUNT)
})
})

describe('flush()', () => {
it('flushes batched debt', async () => {
const [_, l2MessengerImpersonator, user1] = await ethers.getSigners()
const { l2DAIWormholeBridge, l2CrossDomainMessengerMock, l1DAIWormholeBridgeMock } = await setupTest({
l2MessengerImpersonator,
user1,
})

// init two wormholes
await l2DAIWormholeBridge
.connect(user1)
.initiateWormhole(TARGET_DOMAIN_NAME, user1.address, WORMHOLE_AMOUNT, user1.address)
await l2DAIWormholeBridge
.connect(user1)
.initiateWormhole(TARGET_DOMAIN_NAME, user1.address, WORMHOLE_AMOUNT, user1.address)
expect(await l2DAIWormholeBridge.batchedDaiToFlush(TARGET_DOMAIN_NAME)).to.eq(WORMHOLE_AMOUNT * 2)

await l2DAIWormholeBridge.flush(TARGET_DOMAIN_NAME)
const xDomainMessengerCall = l2CrossDomainMessengerMock.smocked.sendMessage.calls[0]
expect(await l2DAIWormholeBridge.batchedDaiToFlush(TARGET_DOMAIN_NAME)).to.eq(0)

expect(xDomainMessengerCall._target).to.equal(l1DAIWormholeBridgeMock.address)
expect(xDomainMessengerCall._message).to.equal(
l1DAIWormholeBridgeMock.interface.encodeFunctionData('finalizeFlush', [
TARGET_DOMAIN_NAME,
WORMHOLE_AMOUNT * 2,
]),
)
expect(xDomainMessengerCall._gasLimit).to.equal(DEFAULT_XDOMAIN_GAS)
})
})
})

async function setupTest(signers: { l2MessengerImpersonator: SignerWithAddress; user1: SignerWithAddress }) {
const l2CrossDomainMessengerMock = await deployOptimismContractMock(
'OVM_L2CrossDomainMessenger',
{ address: await signers.l2MessengerImpersonator.getAddress() }, // This allows us to use an ethers override {from: Mock__OVM_L2CrossDomainMessenger.address} to mock calls
)
const l1Dai = await simpleDeploy<Dai__factory>('Dai', [])
const l2Dai = await simpleDeploy<Dai__factory>('Dai', [])
const l1DAIWormholeBridgeMock = await deployMock('L1DAIWormholeBridge')
const l2DAIWormholeBridge = await simpleDeploy<L2DAIWormholeBridge__factory>('L2DAIWormholeBridge', [
l2CrossDomainMessengerMock.address,
l2Dai.address,
l1Dai.address,
l1DAIWormholeBridgeMock.address,
SOURCE_DOMAIN_NAME,
])

await l2Dai.rely(l2DAIWormholeBridge.address)
await l2Dai.mint(signers.user1.address, INITIAL_TOTAL_L2_SUPPLY)

return { l2Dai, l1DAIWormholeBridgeMock, l2CrossDomainMessengerMock, l2DAIWormholeBridge, l1Dai }
}