Skip to content
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
5 changes: 1 addition & 4 deletions contracts/script/DeployERC20Forwarder.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,14 @@ contract DeployERC20Wrapper is Script {

address internal constant _EMERGENCY_COMMITTEE = address(0);

address internal constant _ERC20 = address(0x1111111111111111111111111111111111111111);

bytes32 internal constant _CALLDATA_CARRIER_LOGIC_REF = bytes32(0);

function run() public {
vm.startBroadcast();
new ERC20Forwarder{salt: sha256("ERC20ForwarderExample")}({
protocolAdapter: address(_PROTOCOL_ADAPTER),
emergencyCommittee: _EMERGENCY_COMMITTEE,
calldataCarrierLogicRef: _CALLDATA_CARRIER_LOGIC_REF,
erc20: _ERC20
calldataCarrierLogicRef: _CALLDATA_CARRIER_LOGIC_REF
});
vm.stopBroadcast();
}
Expand Down
36 changes: 20 additions & 16 deletions contracts/src/ProtocolAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {CommitmentAccumulator} from "./state/CommitmentAccumulator.sol";

import {NullifierSet} from "./state/NullifierSet.sol";

import {Action, ForwarderCalldata, Resource, Transaction} from "./Types.sol";
import {Action, Resource, Transaction} from "./Types.sol";

string constant PROTOCOL_ADAPTER_VERSION = "1.0.0-beta";

Expand Down Expand Up @@ -179,18 +179,21 @@ contract ProtocolAdapter is IProtocolAdapter, ReentrancyGuardTransient, Commitme

/// @notice Executes a call to a forwarder contracts.
/// @param carrierLogicRef The logic reference of the carrier resource.
/// @param call The calldata to conduct the forwarder call.
function _executeForwarderCall(bytes32 carrierLogicRef, ForwarderCalldata memory call) internal {
/// @param callBlob The blob containing the call instruction.
function _executeForwarderCall(bytes32 carrierLogicRef, bytes memory callBlob) internal {
(address untrustedForwarder, bytes memory input, bytes memory expectedOutput) =
abi.decode(callBlob, (address, bytes, bytes));

// slither-disable-next-line calls-loop
bytes memory output =
IForwarder(call.untrustedForwarder).forwardCall({logicRef: carrierLogicRef, input: call.input});
bytes memory actualOutput =
IForwarder(untrustedForwarder).forwardCall({logicRef: carrierLogicRef, input: input});

if (keccak256(output) != keccak256(call.output)) {
revert ForwarderCallOutputMismatch({expected: call.output, actual: output});
if (keccak256(actualOutput) != keccak256(expectedOutput)) {
revert ForwarderCallOutputMismatch({expected: expectedOutput, actual: actualOutput});
}

// solhint-disable-next-line max-line-length
emit ForwarderCallExecuted({untrustedForwarder: call.untrustedForwarder, input: call.input, output: call.output});
emit ForwarderCallExecuted({untrustedForwarder: untrustedForwarder, input: input, output: actualOutput});
}

/// @notice The function processing the state checks and updates
Expand Down Expand Up @@ -237,20 +240,21 @@ contract ProtocolAdapter is IProtocolAdapter, ReentrancyGuardTransient, Commitme
}

/// @notice Processes forwarder calls by verifying and executing them.
/// @param input The logic verifier input of a resource making the call.
/// @param verifierInput The logic verifier input of a resource making the call.
/// @param consumed A flag indicating whether the resource is consumed or not.
function _processForwarderCalls(Logic.VerifierInput calldata input, bool consumed) internal {
function _processForwarderCalls(Logic.VerifierInput calldata verifierInput, bool consumed) internal {
_verifyForwarderCalls({
carrierBlob: input.appData.resourcePayload[0].blob,
expectedTag: input.tag,
carrierBlob: verifierInput.appData.resourcePayload[0].blob,
expectedTag: verifierInput.tag,
consumed: consumed
});

uint256 nCalls = input.appData.externalPayload.length;
uint256 nCalls = verifierInput.appData.externalPayload.length;
for (uint256 i = 0; i < nCalls; ++i) {
ForwarderCalldata memory call = abi.decode(input.appData.externalPayload[i].blob, (ForwarderCalldata));

_executeForwarderCall({carrierLogicRef: input.verifyingKey, call: call});
_executeForwarderCall({
carrierLogicRef: verifierInput.verifyingKey,
callBlob: verifierInput.appData.externalPayload[i].blob
});
}
}

Expand Down
11 changes: 0 additions & 11 deletions contracts/src/Types.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,3 @@ struct Action {
Logic.VerifierInput[] logicVerifierInputs;
Compliance.VerifierInput[] complianceVerifierInputs;
}

/// @notice A data structure containing the input data to be forwarded to the untrusted forwarder contract
/// and the anticipated output data.
/// @param untrustedForwarder The forwarder contract forwarding the call.
/// @param input The input data for the forwarded call that might or might not include the `bytes4` function selector.
/// @param output The anticipated output data from the forwarded call.
struct ForwarderCalldata {
address untrustedForwarder;
bytes input;
bytes output;
}
86 changes: 36 additions & 50 deletions contracts/src/forwarders/ERC20Forwarder.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ pragma solidity ^0.8.30;

import {IERC20} from "@openzeppelin-contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol";
import {ISignatureTransfer} from "@permit2/src/interfaces/IPermit2.sol";
import {IPermit2, ISignatureTransfer} from "@permit2/src/interfaces/IPermit2.sol";
import {Permit2Lib} from "@permit2/src/libraries/Permit2Lib.sol";

import {EmergencyMigratableForwarderBase} from "./EmergencyMigratableForwarderBase.sol";

Expand All @@ -13,96 +14,81 @@ import {ERC20ForwarderInput} from "./ERC20ForwarderInput.sol";
/// @author Anoma Foundation, 2025
/// @notice A forwarder contract forwarding calls and holding funds to wrap and unwrap ERC-20 tokens as resources.
/// @custom:security-contact security@anoma.foundation
contract ERC20Forwarder is EmergencyMigratableForwarderBase, ERC20ForwarderInput {
contract ERC20Forwarder is EmergencyMigratableForwarderBase {
using ERC20ForwarderInput for bytes;
using SafeERC20 for IERC20;

/// @notice The ERC-20 token contract address to forward calls to.
IERC20 internal immutable _ERC20;
/// @notice The canonical Uniswap Permit2 contract deployed at the same address on all supported chains
/// (see [Uniswap's announcement](https://blog.uniswap.org/permit2-and-universal-router)).
IPermit2 internal constant _PERMIT2 = IPermit2(address(Permit2Lib.PERMIT2));

// solhint-disable gas-indexed-events
/// @notice Emitted when ERC20 tokens get wrapped.
/// @param token The ERC20 token address.
/// @param from The address from which tokens were withdrawn.
/// @param value The token amount being deposited into the ERC20 forwarder contract.
event Wrapped(address indexed from, uint256 value); // solhint-disable-line gas-indexed-events
event Wrapped(address indexed token, address indexed from, uint256 value);

/// @notice Emitted when ERC20 tokens get unwrapped.
/// @param token The ERC20 token address.
/// @param to The address to which tokens were deposited.
/// @param value The token amount being withdrawn from the ERC20 forwarder contract.
event Unwrapped(address indexed to, uint256 value); // solhint-disable-line gas-indexed-events
event Unwrapped(address indexed token, address indexed to, uint256 value);
// solhint-enable gas-indexed-events

error TokenMismatch(address expected, address actual);
error ValueMismatch(uint256 expected, uint256 actual);
error CallTypeInvalid();
error TypeOverflow(uint256 limit, uint256 actual);

/// @notice Initializes the ERC-20 forwarder contract.
/// @param protocolAdapter The protocol adapter contract that is allowed to forward calls.
/// @param calldataCarrierLogicRef The resource logic function of the calldata carrier resource.
/// @param emergencyCommittee The emergency committee address that is allowed to set the emergency caller if the
/// RISC Zero verifier has been stopped.
/// @param erc20 The ERC-20 token contract to forward calls to.
constructor(address protocolAdapter, bytes32 calldataCarrierLogicRef, address emergencyCommittee, address erc20)
constructor(address protocolAdapter, bytes32 calldataCarrierLogicRef, address emergencyCommittee)
EmergencyMigratableForwarderBase(protocolAdapter, calldataCarrierLogicRef, emergencyCommittee)
{
if (erc20 == address(0)) {
revert ZeroNotAllowed();
}
_ERC20 = IERC20(erc20);
}
{}

/// @notice Forwards calls.
/// @param input The `bytes` encoded input of the call.
/// @return output The `bytes` encoded output of the call.
function _forwardCall(bytes calldata input) internal override returns (bytes memory output) {
CallType callType = CallType(uint8(input[31]));
ERC20ForwarderInput.CallType callType = ERC20ForwarderInput.CallType(uint8(input[31]));

if (callType == CallType.Transfer) {
if (callType == ERC20ForwarderInput.CallType.Unwrap) {
// slither-disable-next-line unused-return
(
, // CallType
address token,
address to,
uint256 value
) = decodeTransfer(input);
) = input.decodeUnwrap();

emit Unwrapped({to: to, value: value});
emit Unwrapped({token: token, to: to, value: value});

_ERC20.safeTransfer({to: to, value: value});
} else if (callType == CallType.TransferFrom) {
IERC20(token).safeTransfer({to: to, value: value});
} else if (callType == ERC20ForwarderInput.CallType.Wrap) {
// slither-disable-next-line unused-return
(
, // CallType
address from,
uint256 value
) = decodeTransferFrom(input);

emit Wrapped({from: from, value: value});

// slither-disable-next-line arbitrary-send-erc20
_ERC20.safeTransferFrom({from: from, to: address(this), value: value});
} else if (callType == CallType.PermitWitnessTransferFrom) {
(
, // CallType
address from,
uint256 value,
ISignatureTransfer.PermitTransferFrom memory permit,
bytes32 witness,
bytes memory signature
) = decodePermitWitnessTransferFrom(input);

// NOTE: The following checks could be conducted on the carrier resource logic.
{
// Check that the permitted token address matches the ERC20 token this contract is forwarding calls to.
if (permit.permitted.token != address(_ERC20)) {
revert TokenMismatch({expected: address(_ERC20), actual: permit.permitted.token});
}

// Check that the permitted and transferred amounts are exactly the same.
if (permit.permitted.amount != value) {
revert ValueMismatch({expected: value, actual: permit.permitted.amount});
}
) = input.decodeWrap();

if (permit.permitted.amount > type(uint128).max) {
revert TypeOverflow({limit: type(uint128).max, actual: permit.permitted.amount});
}

emit Wrapped({from: from, value: value});
emit Wrapped({token: permit.permitted.token, from: from, value: permit.permitted.amount});

_PERMIT2.permitWitnessTransferFrom({
permit: permit,
// solhint-disable-next-line max-line-length
transferDetails: ISignatureTransfer.SignatureTransferDetails({to: address(this), requestedAmount: value}),
transferDetails: ISignatureTransfer.SignatureTransferDetails({
to: address(this),
requestedAmount: permit.permitted.amount
}),
owner: from,
witness: witness,
witnessTypeString: "bytes32 witness",
Expand All @@ -114,7 +100,7 @@ contract ERC20Forwarder is EmergencyMigratableForwarderBase, ERC20ForwarderInput
revert CallTypeInvalid();
}

output = abi.encode(true);
output = "";
}

/// @notice Forwards emergency calls.
Expand Down
Loading
Loading