Skip to content

Commit

Permalink
feat: modify the ether portal
Browse files Browse the repository at this point in the history
  • Loading branch information
guidanoli committed Oct 26, 2023
1 parent 098e95e commit 1edb974
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 177 deletions.
43 changes: 32 additions & 11 deletions onchain/rollups/contracts/portals/EtherPortal.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ import {InputRelay} from "../inputs/InputRelay.sol";
import {IInputBox} from "../inputs/IInputBox.sol";
import {InputEncoding} from "../common/InputEncoding.sol";

import {Address} from "@openzeppelin/contracts/utils/Address.sol";

/// @title Ether Portal
///
/// @notice This contract allows anyone to perform transfers of
/// Ether to a DApp while informing the off-chain machine.
/// @notice Manages Ether transfers to and from Cartesi DApps.
contract EtherPortal is InputRelay, IEtherPortal {
/// @notice Raised when the Ether transfer fails.
error EtherTransferFailed();
using Address for address payable;

mapping(address => uint256) public balanceOf;

/// @notice Constructs the portal.
/// @param _inputBox The input box used by the portal
Expand All @@ -24,17 +26,36 @@ contract EtherPortal is InputRelay, IEtherPortal {
address _dapp,
bytes calldata _execLayerData
) external payable override {
// We used to call `transfer()` but it's not considered safe,
// as it assumes gas costs are immutable (they are not).
(bool success, ) = _dapp.call{value: msg.value}("");
balanceOf[_dapp] += msg.value;

if (!success) {
revert EtherTransferFailed();
}
addEtherDepositInput(_dapp, msg.value, _execLayerData);
}

function transfer(
address _dapp,
uint256 _value,
bytes calldata _execLayerData
) external {
balanceOf[msg.sender] -= _value;
balanceOf[_dapp] += _value;

addEtherDepositInput(_dapp, _value, _execLayerData);
}

function withdraw(address payable _recipient, uint256 _value) external {
balanceOf[msg.sender] -= _value;

_recipient.sendValue(_value);
}

function addEtherDepositInput(
address _dapp,
uint256 _value,
bytes calldata _execLayerData
) internal {
bytes memory input = InputEncoding.encodeEtherDeposit(
msg.sender,
msg.value,
_value,
_execLayerData
);

Expand Down
2 changes: 2 additions & 0 deletions onchain/rollups/contracts/portals/IEtherPortal.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {IInputRelay} from "../inputs/IInputRelay.sol";
interface IEtherPortal is IInputRelay {
// Permissionless functions

function balanceOf(address _owner) external returns (uint256);

/// @notice Transfer Ether to a DApp and add an input to
/// the DApp's input box to signal such operation.
///
Expand Down
223 changes: 57 additions & 166 deletions onchain/rollups/test/foundry/portals/EtherPortal.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,206 +11,97 @@ import {IInputBox} from "contracts/inputs/IInputBox.sol";
import {InputBox} from "contracts/inputs/InputBox.sol";
import {InputEncoding} from "contracts/common/InputEncoding.sol";

contract BadEtherReceiver {
receive() external payable {
revert("This contract does not accept Ether");
}
}

contract EtherReceiver {
receive() external payable {}
}

contract InputBoxWatcher {
IInputBox inputBox;

event WatchedFallback(
address sender,
uint256 value,
uint256 numberOfInputs
);

constructor(IInputBox _inputBox) {
inputBox = _inputBox;
}

receive() external payable {
uint256 numberOfInputs = inputBox.getNumberOfInputs(address(this));
emit WatchedFallback(msg.sender, msg.value, numberOfInputs);
}
}
import {SimpleEtherReceiver} from "../util/SimpleEtherReceiver.sol";

contract EtherPortalTest is Test {
IInputBox inputBox;
IEtherPortal etherPortal;
address alice;
address dapp;

event InputAdded(
address indexed dapp,
uint256 indexed inputIndex,
address sender,
bytes input
);
event WatchedFallback(
address sender,
uint256 value,
uint256 numberOfInputs
);

function setUp() public {
function setUp() external {
inputBox = new InputBox();
etherPortal = new EtherPortal(inputBox);
alice = address(0xdeadbeef);
dapp = address(0x12345678);
}

function testGetInputBox() public {
assertEq(address(etherPortal.getInputBox()), address(inputBox));
function calculateTVL() internal view returns (uint256) {
return address(etherPortal).balance;
}

function testEtherDeposit(uint256 value, bytes calldata data) public {
// Construct the Ether deposit input
bytes memory input = abi.encodePacked(alice, value, data);

// Transfer Ether to Alice and start impersonating her
startHoax(alice, value);

// Save the Ether balances
uint256 alicesBalanceBefore = alice.balance;
uint256 dappsBalanceBefore = dapp.balance;
uint256 portalsBalanceBefore = address(etherPortal).balance;

// Expect InputAdded to be emitted with the right arguments
function expectInputAdded(address _dapp, bytes memory _input) internal {
uint256 inputIndex = inputBox.getNumberOfInputs(_dapp);
vm.expectEmit(true, true, false, true, address(inputBox));
emit InputAdded(dapp, 0, address(etherPortal), input);

// Deposit Ether in the DApp via the portal
etherPortal.depositEther{value: value}(dapp, data);
emit InputAdded(_dapp, inputIndex, address(etherPortal), _input);
}
}

// Check the balances after the deposit
assertEq(alice.balance, alicesBalanceBefore - value);
assertEq(dapp.balance, dappsBalanceBefore + value);
assertEq(address(etherPortal).balance, portalsBalanceBefore);
contract CoreEtherPortalTest is EtherPortalTest {
function testGetInputBox() external {
assertEq(address(etherPortal.getInputBox()), address(inputBox));
}

// Check the DApp's input box
assertEq(inputBox.getNumberOfInputs(dapp), 1);
function testBalanceOf(address _owner) external {
assertEq(etherPortal.balanceOf(_owner), 0);
}
}

function testRevertsFailedTransfer(
uint256 value,
bytes calldata data
) public {
// Create a contract that reverts when it receives Ether
BadEtherReceiver badEtherReceiver = new BadEtherReceiver();
contract DepositEtherPortalTest is EtherPortalTest {
struct Context {
address depositor;
address receiver;
uint256 value;
bytes execLayerData;
}

startHoax(alice, value);
struct Snapshot {
uint256 tvl;
uint256 receiverBalance;
uint256 depositorBalance;
uint256 numOfInputs;
}

// Expect the deposit to revert with the following message
vm.expectRevert(EtherPortal.EtherTransferFailed.selector);
etherPortal.depositEther{value: value}(address(badEtherReceiver), data);
function takeSnapshot(
Context calldata _ctx
) internal returns (Snapshot memory) {
return
Snapshot({
tvl: calculateTVL(),
receiverBalance: etherPortal.balanceOf(_ctx.receiver),
depositorBalance: _ctx.depositor.balance,
numOfInputs: inputBox.getNumberOfInputs(_ctx.receiver)
});
}

function testNumberOfInputs(uint256 value, bytes calldata data) public {
// Create a contract that records the number of inputs it has received
InputBoxWatcher watcher = new InputBoxWatcher(inputBox);
function testDepositEther(Context calldata _ctx) external {
vm.assume(_ctx.depositor != address(etherPortal));
vm.deal(_ctx.depositor, _ctx.value);

startHoax(alice, value);
Snapshot memory s1 = takeSnapshot(_ctx);

// Expect new contract to have no inputs yet
uint256 numberOfInputsBefore = inputBox.getNumberOfInputs(
address(watcher)
bytes memory input = InputEncoding.encodeEtherDeposit(
_ctx.depositor,
_ctx.value,
_ctx.execLayerData
);

// Expect WatchedFallback to be emitted
vm.expectEmit(false, false, false, true, address(watcher));
emit WatchedFallback(address(etherPortal), value, numberOfInputsBefore);
expectInputAdded(_ctx.receiver, input);

// Transfer Ether to contract
etherPortal.depositEther{value: value}(address(watcher), data);

// Expect new input
assertEq(
inputBox.getNumberOfInputs(address(watcher)),
numberOfInputsBefore + 1
vm.prank(_ctx.depositor);
etherPortal.depositEther{value: _ctx.value}(
_ctx.receiver,
_ctx.execLayerData
);
}
}

contract EtherPortalHandler is Test {
IEtherPortal portal;
IInputBox inputBox;
address[] dapps;
mapping(address => uint256) public dappBalances;
mapping(address => uint256) public dappNumInputs;

constructor(IEtherPortal _portal, address[] memory _dapps) {
portal = _portal;
inputBox = portal.getInputBox();
dapps = _dapps;
}

function depositEther(
uint256 _dappIndex,
uint256 _amount,
bytes calldata _execLayerData
) external {
address sender = msg.sender;
address dapp = dapps[_dappIndex % dapps.length];
_amount = bound(_amount, 0, type(uint128).max);

// fund sender
for (uint256 i; i < dapps.length; ++i) {
if (sender == dapps[i]) {
return;
}
}
vm.deal(sender, _amount);

// balance before the deposit
uint256 senderBalanceBefore = sender.balance;
uint256 dappBalanceBefore = dapp.balance;
// balance of the portal is 0 all the time during tests
assertEq(address(portal).balance, 0);

vm.prank(sender);
portal.depositEther{value: _amount}(dapp, _execLayerData);

// Check the balances after the deposit
assertEq(sender.balance, senderBalanceBefore - _amount);
assertEq(dapp.balance, dappBalanceBefore + _amount);
assertEq(address(portal).balance, 0);

dappBalances[dapp] += _amount;
assertEq(++dappNumInputs[dapp], inputBox.getNumberOfInputs(dapp));
}
}

contract EtherPortalInvariantTest is Test {
InputBox inputBox;
EtherPortal portal;
EtherPortalHandler handler;
uint256 numDapps;
address[] dapps;

function setUp() public {
inputBox = new InputBox();
portal = new EtherPortal(inputBox);
numDapps = 30;
for (uint256 i; i < numDapps; ++i) {
dapps.push(address(new EtherReceiver()));
}
handler = new EtherPortalHandler(portal, dapps);

targetContract(address(handler));
}
Snapshot memory s2 = takeSnapshot(_ctx);

function invariantTests() external {
for (uint256 i; i < numDapps; ++i) {
address dapp = dapps[i];
assertEq(dapp.balance, handler.dappBalances(dapp));
uint256 numInputs = inputBox.getNumberOfInputs(dapp);
assertEq(numInputs, handler.dappNumInputs(dapp));
}
assertEq(s2.tvl, s1.tvl + _ctx.value);
assertEq(s2.depositorBalance, s1.depositorBalance - _ctx.value);
assertEq(s2.numOfInputs, s1.numOfInputs + 1);
assertEq(s2.receiverBalance, s1.receiverBalance + _ctx.value);
}
}

0 comments on commit 1edb974

Please sign in to comment.