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

refactor(contracts): align forwarders to new ptoken minting logic #29

Merged
merged 18 commits into from
Feb 21, 2024
Merged
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
110 changes: 110 additions & 0 deletions contracts/forwarder/ForwarderHost.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.17;

import {Context} from "@openzeppelin/contracts/utils/Context.sol";
import {IERC777Recipient} from "@openzeppelin/contracts/token/ERC777/IERC777Recipient.sol";
import {IERC1820Registry} from "@openzeppelin/contracts/interfaces/IERC1820Registry.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IForwarder} from "../interfaces/IForwarder.sol";
import {IPReceiver} from "../interfaces/external/IPReceiver.sol";
import {IPToken} from "../interfaces/external/IPToken.sol";
import {Helpers} from "../libraries/Helpers.sol";
import {BytesLib} from "../libraries/BytesLib.sol";

error CallFailed(address target, bytes data);
error InvalidCallParams(address[] targets, bytes[] data, address caller);
error InvalidOriginAddress(address originAddress);
error InvalidCaller(address caller, address expected);

contract ForwarderHost is IForwarder, IERC777Recipient, Context, Ownable, IPReceiver {
using SafeERC20 for IERC20;

address public immutable token;
mapping(address => bool) private _whitelistedOriginAddresses;

constructor(address _token) {
IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24).setInterfaceImplementer(
address(this),
keccak256("ERC777TokensRecipient"),
address(this)
);
token = _token;
}

/// @inheritdoc IForwarder
function call(uint256 amount, address to, bytes calldata data, bytes4 chainId) external {
address msgSender = _msgSender();
if (amount > 0) {
IERC20(token).safeTransferFrom(msgSender, address(this), amount);
}

bytes memory effectiveUserData = abi.encode(data, msgSender);
uint256 effectiveAmount = amount == 0 ? 1 : amount;

IPToken(token).redeem(effectiveAmount, effectiveUserData, Helpers.addressToAsciiString(to), chainId);
}

function receiveUserData(bytes calldata _metadata) external override {
if (_msgSender() == token)
_processMetadata(_metadata);
}

function tokensReceived(
address /*_operator*/,
address _from,
address /*_to,*/,
uint256 /*_amount*/,
bytes calldata _metadata,
bytes calldata /*_operatorData*/
) external override {
if (_msgSender() == token && _from == address(0))
_processMetadata(_metadata);
}

function whitelistOriginAddress(address originAddress) external onlyOwner {
_whitelistedOriginAddresses[originAddress] = true;
}

function _processMetadata(bytes memory _metadata) private {
(, bytes memory userData, , address originAddress, , , , ) = abi.decode(
_metadata,
(bytes1, bytes, bytes4, address, bytes4, address, bytes, bytes)
);

(bytes memory callsAndTargets, address caller) = abi.decode(userData, (bytes, address));
if (!_whitelistedOriginAddresses[originAddress]) {
revert InvalidOriginAddress(originAddress);
}

(address[] memory targets, bytes[] memory data) = abi.decode(callsAndTargets, (address[], bytes[]));

if (targets.length != data.length) {
revert InvalidCallParams(targets, data, caller);
}

for (uint256 i = 0; i < targets.length; ) {
// NOTE: avoid to check the caller if function is approve
if (bytes4(data[i]) != 0x095ea7b3) {
bytes memory addrSlot = BytesLib.slice(data[i], 4, 36);
address expectedCaller = address(BytesLib.toAddress(addrSlot, 32 - 20));

// NOTE: needed to for example avoid someone to vote for someone else
if (expectedCaller != caller) {
revert InvalidCaller(caller, expectedCaller);
}
}

(bool success, ) = targets[i].call(data[i]);
if (!success) {
revert CallFailed(targets[i], data[i]);
}

unchecked {
++i;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,22 @@ import {BytesLib} from "../libraries/BytesLib.sol";
error CallFailed(address target, bytes data);
error InvalidCallParams(address[] targets, bytes[] data, address caller);
error InvalidOriginAddress(address originAddress);
error InvalidCaller(address caller);
error InvalidCaller(address caller, address expected);

contract Forwarder is IForwarder, IERC777Recipient, Context, Ownable {
contract ForwarderNative is IForwarder, IERC777Recipient, Context, Ownable {
using SafeERC20 for IERC20;

address public immutable sender;
address public immutable token;
address public immutable vault;
mapping(address => bool) private _whitelistedOriginAddresses;

constructor(address _token, address _sender, address _vault) {
constructor(address _token, address _vault) {
IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24).setInterfaceImplementer(
address(this),
keccak256("ERC777TokensRecipient"),
address(this)
);

sender = _sender;
token = _token;
vault = _vault; // set it to 0 on an host chain
}
Expand All @@ -47,7 +45,7 @@ contract Forwarder is IForwarder, IERC777Recipient, Context, Ownable {
bytes calldata _userData,
bytes calldata /*_operatorData*/
) external override {
if (_msgSender() == token && _from == sender) {
if (_msgSender() == token && _from == vault) {
(, bytes memory userData, , address originAddress, , , , ) = abi.decode(
_userData,
(bytes1, bytes, bytes4, address, bytes4, address, bytes, bytes)
Expand Down Expand Up @@ -76,7 +74,7 @@ contract Forwarder is IForwarder, IERC777Recipient, Context, Ownable {

// NOTE: needed to for example avoid someone to vote for someone else
if (expectedCaller != caller) {
revert InvalidCaller(expectedCaller);
revert InvalidCaller(caller, expectedCaller);
}
}

Expand All @@ -102,18 +100,14 @@ contract Forwarder is IForwarder, IERC777Recipient, Context, Ownable {
bytes memory effectiveUserData = abi.encode(data, msgSender);
uint256 effectiveAmount = amount == 0 ? 1 : amount;

if (vault != address(0)) {
IERC20(token).safeApprove(vault, effectiveAmount);
IErc20Vault(vault).pegIn(
effectiveAmount,
token,
Helpers.addressToAsciiString(to),
effectiveUserData,
chainId
);
} else {
IPToken(token).redeem(effectiveAmount, effectiveUserData, Helpers.addressToAsciiString(to), chainId);
}
IERC20(token).safeApprove(vault, effectiveAmount);
IErc20Vault(vault).pegIn(
effectiveAmount,
token,
Helpers.addressToAsciiString(to),
effectiveUserData,
chainId
);
}

function whitelistOriginAddress(address originAddress) external onlyOwner {
Expand Down
18 changes: 18 additions & 0 deletions contracts/interfaces/external/IPReceiver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.17;

/**
* @title IPReceiver
* @author pNetwork
*
* @dev Interface for contracts excpecting cross-chain data
*/
interface IPReceiver {
/*
* @dev Function called when userData.length > 0 when minting the pToken
*
* @param userData
*/
function receiveUserData(bytes calldata userData) external;
}
92 changes: 79 additions & 13 deletions contracts/test/MockPToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
pragma solidity ^0.8.17;

import "@openzeppelin/contracts/token/ERC777/ERC777.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {IPReceiver} from "../interfaces/external/IPReceiver.sol";

contract MockPToken is ERC777 {
contract MockPTokenERC777 is ERC777 {
address public pNetwork;
bytes4 public ORIGIN_CHAIN_ID;

Expand All @@ -19,10 +21,9 @@ contract MockPToken is ERC777 {
constructor(
string memory tokenName,
string memory tokenSymbol,
address[] memory defaultOperators,
address _pnetwork,
bytes4 originChainId
) ERC777(tokenName, tokenSymbol, defaultOperators) {
) ERC777(tokenName, tokenSymbol, new address[](0)) {
pNetwork = _pnetwork;
ORIGIN_CHAIN_ID = originChainId;
}
Expand Down Expand Up @@ -54,17 +55,82 @@ contract MockPToken is ERC777 {
return true;
}

function operatorRedeem(
address account,
function redeem(
uint256 amount,
bytes memory data,
string memory underlyingAssetRecipient,
bytes4 destinationChainId
) public {
_burn(_msgSender(), amount, data, "");
emit Redeem(msg.sender, amount, underlyingAssetRecipient, ORIGIN_CHAIN_ID, destinationChainId);
}

function owner() internal view returns (address) {
return pNetwork;
}
}

contract MockPTokenERC20 is ERC20 {
address public pNetwork;
bytes4 public ORIGIN_CHAIN_ID;

event ReceiveUserDataFailed();

event Redeem(
address indexed redeemer,
uint256 value,
string underlyingAssetRecipient,
bytes userData,
bytes4 originChainId,
bytes4 destinationChainId
);

constructor(
string memory tokenName,
string memory tokenSymbol,
address _pnetwork,
bytes4 originChainId
) ERC20(tokenName, tokenSymbol) {
pNetwork = _pnetwork;
ORIGIN_CHAIN_ID = originChainId;
}

function changePNetwork(address newPNetwork) external {
require(_msgSender() == pNetwork, "Only the pNetwork can change the `pNetwork` account!");
require(newPNetwork != address(0), "pNetwork cannot be the zero address!");
pNetwork = newPNetwork;
}

function mint(
address recipient,
uint256 value,
bytes calldata userData,
bytes calldata
) external returns (bool) {
require(_msgSender() == pNetwork, "Only the pNetwork can mint tokens!");
require(recipient != address(0), "pToken: Cannot mint to the zero address!");
_mint(recipient, value);
if (userData.length > 0) {
// pNetwork aims to deliver cross chain messages successfully regardless of what the user may do with them.
// We do not want this mint transaction reverting if their receiveUserData function reverts,
// and thus we swallow any such errors, emitting a `ReceiveUserDataFailed` event instead.
// The low-level call is used because in the solidity version this contract was written in,
// a try/catch block fails to catch the revert caused if the receiver is not in fact a contract.
// This way, a user also has the option include userData even when minting to an externally owned account.
bytes memory data = abi.encodeWithSelector(IPReceiver.receiveUserData.selector, userData);
(bool success, ) = recipient.call(data);
if (!success) emit ReceiveUserDataFailed();
}
return true;
}

function redeem(
uint256 amount,
bytes calldata data,
bytes calldata operatorData,
string calldata underlyingAssetRecipient,
bytes4 destinationChainId
) external {
require(isOperatorFor(_msgSender(), account), "ERC777: caller is not an operator for holder");
_burn(account, amount, data, operatorData);
emit Redeem(account, amount, underlyingAssetRecipient, ORIGIN_CHAIN_ID, destinationChainId);
) external returns (bool) {
redeem(amount, "", underlyingAssetRecipient, destinationChainId);
return true;
}

function redeem(
Expand All @@ -73,8 +139,8 @@ contract MockPToken is ERC777 {
string memory underlyingAssetRecipient,
bytes4 destinationChainId
) public {
_burn(_msgSender(), amount, data, "");
emit Redeem(msg.sender, amount, underlyingAssetRecipient, ORIGIN_CHAIN_ID, destinationChainId);
_burn(_msgSender(), amount);
emit Redeem(_msgSender(), amount, underlyingAssetRecipient, data, ORIGIN_CHAIN_ID, destinationChainId);
}

function owner() internal view returns (address) {
Expand Down
42 changes: 15 additions & 27 deletions hardhat.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,7 @@ require('@nomicfoundation/hardhat-chai-matchers')
require('hardhat-spdx-license-identifier')
require('hardhat-tracer')

require('./tasks/decode-forwarder-metadata.js')
require('./tasks/acl-assign-permission.js')
require('./tasks/deploy-dao.js')
require('./tasks/deploy_forwarder_bsc.js')
require('./tasks/deploy_forwarder_gnosis.js')
require('./tasks/deploy_forwarder_mainnet.js')
require('./tasks/deploy_forwarder_polygon.js')
require('./tasks/set_permissions.js')
require('./tasks/upgrade.js')
require('./tasks')

const { execSync } = require('child_process')

Expand All @@ -27,12 +19,15 @@ const decodeGpgFile = (_file) =>
encoding: 'utf-8'
}).trim()

const maybeGetAccountsFromGpgFile = (_file) =>
_file
? decodeGpgFile(_file)
.then((_key) => [_key])
.catch((_err) => undefined)
: undefined
const maybeGetAccountsFromGpgFile = (_file) => {
if (!_file) return undefined
try {
const _key = decodeGpgFile(_file)
return [_key]
} catch (_err) {
return undefined
}
}

const accounts = maybeGetAccountsFromGpgFile(getEnvironmentVariable('PK'))
/**
Expand Down Expand Up @@ -89,18 +84,8 @@ module.exports = {
apiKey: {
mainnet: getEnvironmentVariable('ETHERSCAN_API_KEY'),
polygon: getEnvironmentVariable('POLYGONSCAN_API_KEY'),
gnosis: getEnvironmentVariable('GNOSISSCAN_API_KEY')
},
customChains: [
{
network: 'polygon',
chainId: 137,
urls: {
apiURL: 'https://api.polygonscan.com/api',
browserURL: 'https://polygonscan.com'
}
}
]
xdai: getEnvironmentVariable('GNOSISSCAN_API_KEY')
}
},
gasReporter: {
enabled: true
Expand All @@ -109,6 +94,9 @@ module.exports = {
overwrite: false,
runOnCompile: false
},
sourcify: {
enabled: false
},
mocha: {
timeout: 100000000
}
Expand Down
Loading