Skip to content

Commit

Permalink
Predicate contract for PoS bridge integration
Browse files Browse the repository at this point in the history
  • Loading branch information
k1rill-fedoseev committed Nov 8, 2022
1 parent a1ce123 commit 89ed667
Show file tree
Hide file tree
Showing 8 changed files with 285 additions and 1 deletion.
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);
}
}

0 comments on commit 89ed667

Please sign in to comment.