Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft contracts for Polygon PoS bridge integration #27

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@
path = lib/@uniswap/v3-core
url = https://github.com/Uniswap/v3-core
branch = 0.8
[submodule "lib/pos-portal"]
path = lib/pos-portal
url = https://github.com/maticnetwork/pos-portal
1 change: 1 addition & 0 deletions lib/pos-portal
Submodule pos-portal added at 8460b1
1 change: 1 addition & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
@openzeppelin/contracts/=lib/@openzeppelin/contracts/contracts/
@gnosis/auction/=lib/@gnosis/auction/contracts/
@polygon/pos-portal/=lib/pos-portal/contracts/
24 changes: 24 additions & 0 deletions src/bridge/polygon/PolygonBobToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: CC0-1.0

pragma solidity 0.8.15;

import "../../BobToken.sol";

/**
* @title PolygonBobToken
*/
contract PolygonBobToken is BobToken {
event Withdrawn(address indexed account, uint256 value);

constructor(address _self) BobToken(_self) {}

function deposit(address _user, bytes calldata _depositData) external {
mint(_user, abi.decode(_depositData, (uint256)));
}

function withdraw(uint256 _amount) external {
_burn(msg.sender, _amount);

emit Withdrawn(msg.sender, _amount);
}
}
79 changes: 79 additions & 0 deletions src/bridge/polygon/PolygonERC20MintBurnPredicate.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// SPDX-License-Identifier: MIT

pragma solidity 0.6.6;

import "@polygon/pos-portal/root/TokenPredicates/ITokenPredicate.sol";

interface IERC20MintBurn {
function mint(address user, uint256 amount) external;
function burn(uint256 amount) external;
function burnFrom(address user, uint256 amount) external;
}

/**
* @title ERC20 Mint/Burn Predicate for Polygon PoS bridge.
* Works with a `Withdrawn(address account, uint256 value)` event.
*/
contract PolygonERC20MintBurnPredicate is ITokenPredicate {
using RLPReader for bytes;
using RLPReader for RLPReader.RLPItem;

event LockedERC20(
address indexed depositor, address indexed depositReceiver, address indexed rootToken, uint256 amount
);

// keccak256("Withdrawn(address account, uint256 value)");
bytes32 public constant WITHDRAWN_EVENT_SIG = 0x7084f5476618d8e60b11ef0d7d3f06914655adb8793e28ff7f018d4c76d505d5;

// see https://github.com/maticnetwork/pos-portal/blob/master/contracts/root/RootChainManager/RootChainManager.sol
address public immutable rootChainManager;

constructor(address _rootChainManager) public {
rootChainManager = _rootChainManager;
}

/**
* Burns ERC20 tokens for deposit.
* @dev Reverts if not called by the manager (RootChainManager).
* @param depositor Address who wants to deposit tokens.
* @param depositReceiver Address (address) who wants to receive tokens on child chain.
* @param rootToken Token which gets deposited.
* @param depositData ABI encoded amount.
*/
function lockTokens(
address depositor,
address depositReceiver,
address rootToken,
bytes calldata depositData
)
external
override
{
require(msg.sender == rootChainManager, "Predicate: only manager");
uint256 amount = abi.decode(depositData, (uint256));
emit LockedERC20(depositor, depositReceiver, rootToken, amount);
IERC20MintBurn(rootToken).burnFrom(depositor, amount);
}

/**
* Validates the {Withdrawn} log signature, then mints the correct amount to withdrawer.
* @dev Reverts if not called only by the manager (RootChainManager).
* @param rootToken Token which gets withdrawn
* @param log Valid ERC20 burn log from child chain
*/
function exitTokens(address, address rootToken, bytes memory log) public override {
require(msg.sender == rootChainManager, "Predicate: only manager");

RLPReader.RLPItem[] memory logRLPList = log.toRlpItem().toList();
RLPReader.RLPItem[] memory logTopicRLPList = logRLPList[1].toList(); // topics

require(
bytes32(logTopicRLPList[0].toUint()) == WITHDRAWN_EVENT_SIG, // topic0 is event sig
"Predicate: invalid signature"
);

address withdrawer = address(logTopicRLPList[1].toUint()); // topic1 is from address

IERC20MintBurn(rootToken).mint(withdrawer, logRLPList[2].toUint());
}
}
1 change: 1 addition & 0 deletions src/interfaces/IBurnableERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ pragma solidity 0.8.15;

interface IBurnableERC20 {
function burn(uint256 amount) external;
function burnFrom(address user, uint256 amount) external;
}
16 changes: 15 additions & 1 deletion src/token/ERC20MintBurn.sol
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ abstract contract ERC20MintBurn is IMintableERC20, IBurnableERC20, Ownable, Base
* @param _to address of the tokens receiver.
* @param _amount amount of tokens to mint.
*/
function mint(address _to, uint256 _amount) external {
function mint(address _to, uint256 _amount) public {
require(isMinter(msg.sender), "ERC20MintBurn: not a minter");

_mint(_to, _amount);
Expand All @@ -58,4 +58,18 @@ abstract contract ERC20MintBurn is IMintableERC20, IBurnableERC20, Ownable, Base

_burn(msg.sender, _value);
}

/**
* @dev Burns pre-approved tokens from the other address.
* Callable only by one of the burner addresses.
* @param _from account to burn tokens from.
* @param _value amount of tokens to burn. Should be less than or equal to caller balance.
*/
function burnFrom(address _from, uint256 _value) external virtual {
require(isBurner(msg.sender), "ERC20MintBurn: not a burner");

_spendAllowance(_from, msg.sender, _value);

_burn(_from, _value);
}
}
161 changes: 161 additions & 0 deletions test/bridge/polygon/PolygonPoSBridge.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// SPDX-License-Identifier: CC0-1.0

pragma solidity 0.8.15;

import "forge-std/Test.sol";
import "../../shared/Env.t.sol";
import "../../../src/bridge/polygon/PolygonBobToken.sol";
import "../../../src/proxy/EIP1967Proxy.sol";

interface IRootChainManager {
function registerPredicate(bytes32 tokenType, address predicate) external;
function mapToken(address rootToken, address childToken, bytes32 tokenType) external;
function depositFor(address user, address rootToken, bytes memory depositData) external;
}

interface IChildChainManager {
function onStateReceive(uint256, bytes calldata data) external;
}

interface IBobPredicate {
function exitTokens(address, address rootToken, bytes memory log) external;
}

contract PolygonPoSBridge is Test {
event LockedERC20(
address indexed depositor, address indexed depositReceiver, address indexed rootToken, uint256 amount
);
event Withdrawn(address indexed account, uint256 value);

BobToken bobMainnet;
PolygonBobToken bobPolygon;

uint256 mainnetFork;
uint256 polygonFork;

address bobPredicate;
address rootChainManager = 0xA0c68C638235ee32657e8f720a23ceC1bFc77C77;
address rootChainManagerOwner = 0xFa7D2a996aC6350f4b56C043112Da0366a59b74c;
address childChainManager = 0xA6FA4fB5f76172d178d61B04b0ecd319C5d1C0aa;
address stateSyncer = 0x0000000000000000000000000000000000001001;

function setUp() public {
mainnetFork = vm.createFork(forkRpcUrlMainnet);
polygonFork = vm.createFork(forkRpcUrlPolygon);

vm.selectFork(mainnetFork);

EIP1967Proxy bobProxy = new EIP1967Proxy(address(this), mockImpl, "");
BobToken bobImpl = new BobToken(address(bobProxy));
bobProxy.upgradeTo(address(bobImpl));
bobMainnet = BobToken(address(bobProxy));

bobMainnet.updateMinter(address(this), true, false);

vm.selectFork(polygonFork);

bobProxy = new EIP1967Proxy(address(this), mockImpl, "");
PolygonBobToken bobImpl2 = new PolygonBobToken(address(bobProxy));
bobProxy.upgradeTo(address(bobImpl2));
bobPolygon = PolygonBobToken(address(bobProxy));

bobPolygon.updateMinter(address(this), true, false);
bobPolygon.updateMinter(childChainManager, true, true);

vm.selectFork(mainnetFork);

vm.etch(rootChainManagerOwner, "");
vm.startPrank(rootChainManagerOwner);
bytes memory predicateCode = bytes.concat(
vm.getCode("out/PolygonERC20MintBurnPredicate.sol/PolygonERC20MintBurnPredicate.json"),
abi.encode(rootChainManager)
);
assembly {
sstore(bobPredicate.slot, create(0, add(predicateCode, 32), mload(predicateCode)))
}
IRootChainManager(rootChainManager).registerPredicate(keccak256("BOB"), bobPredicate);
vm.recordLogs();
IRootChainManager(rootChainManager).mapToken(address(bobMainnet), address(bobPolygon), keccak256("BOB"));
vm.stopPrank();
_syncState();

bobMainnet.updateMinter(bobPredicate, true, true);

vm.label(address(bobMainnet), "BOB");
vm.label(address(bobPolygon), "BOB");
}

function _syncState() internal {
uint256 curFork = vm.activeFork();
Vm.Log[] memory logs = vm.getRecordedLogs();
vm.selectFork(polygonFork);
for (uint256 i = 0; i < logs.length; i++) {
if (logs[i].topics[0] == bytes32(0x103fed9db65eac19c4d870f49ab7520fe03b99f1838e5996caf47e9e43308392)) {
vm.prank(stateSyncer);
IChildChainManager(childChainManager).onStateReceive(0, abi.decode(logs[i].data, (bytes)));
}
}
vm.selectFork(curFork);
}

function testBridgeToPolygon() public {
vm.selectFork(mainnetFork);

bobMainnet.mint(user1, 100 ether);

vm.startPrank(user1);
bobMainnet.approve(bobPredicate, 10 ether);
vm.expectEmit(true, true, true, true, bobPredicate);
emit LockedERC20(user1, user2, address(bobMainnet), 10 ether);
vm.recordLogs();
IRootChainManager(rootChainManager).depositFor(user2, address(bobMainnet), abi.encode(10 ether));
vm.stopPrank();

_syncState();

assertEq(bobMainnet.totalSupply(), 90 ether);
assertEq(bobMainnet.balanceOf(user1), 90 ether);

vm.selectFork(polygonFork);

assertEq(bobPolygon.totalSupply(), 10 ether);
assertEq(bobPolygon.balanceOf(user2), 10 ether);
}

function testBridgeFromPolygon() public {
vm.selectFork(polygonFork);

bobPolygon.mint(user1, 100 ether);

vm.prank(user1);
vm.expectEmit(true, false, false, true, address(bobPolygon));
emit Withdrawn(user1, 10 ether);
bobPolygon.withdraw(10 ether);

vm.selectFork(mainnetFork);

// cast --to-rlp '["<token>", ["<topic0>", "<topic1>"], "<data>"]'
bytes memory logRLP = bytes.concat(
hex"f87a94",
abi.encodePacked(address(bobPolygon)),
hex"f842a0",
keccak256("Withdrawn(address,uint256)"),
hex"a0",
abi.encode(user1),
hex"a0",
abi.encode(10 ether)
);

vm.etch(rootChainManager, "");
vm.prank(rootChainManager);
IBobPredicate(bobPredicate).exitTokens(user1, address(bobMainnet), logRLP);

assertEq(bobMainnet.totalSupply(), 10 ether);
assertEq(bobMainnet.balanceOf(user1), 10 ether);

vm.selectFork(polygonFork);

assertEq(bobPolygon.totalSupply(), 90 ether);
assertEq(bobPolygon.balanceOf(user1), 90 ether);
}
}