Skip to content

Commit

Permalink
added PoC for support of fully abstracted user meta tx. Continued to …
Browse files Browse the repository at this point in the history
…add in support for intent-centric system. Tweaked try/catch of execution to better capture nonce increments and avoid replay attacks. Lots of work still to do on that front
  • Loading branch information
thogard785 committed Aug 9, 2023
1 parent 2619c93 commit 4a90abf
Show file tree
Hide file tree
Showing 17 changed files with 435 additions and 103 deletions.
80 changes: 55 additions & 25 deletions src/contracts/atlas/Atlas.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity ^0.8.16;

import {Factory} from "./Factory.sol";
import {UserVerifier} from "./UserVerification.sol";

import "../types/CallTypes.sol";
import "../types/VerificationTypes.sol";
Expand All @@ -18,6 +19,13 @@ contract Atlas is Test, Factory {

constructor(uint32 _escrowDuration) Factory(_escrowDuration) {}

function createExecutionEnvironment(ProtocolCall calldata protocolCall) external returns (address environment) {
environment = _setExecutionEnvironment(protocolCall, msg.sender, protocolCall.to.codehash);
//if (userNonces[msg.sender] == 0) {
// unchecked{ ++userNonces[msg.sender];}
//}
}

function metacall(
ProtocolCall calldata protocolCall, // supplied by frontend
UserCall calldata userCall, // set by user
Expand All @@ -28,34 +36,56 @@ contract Atlas is Test, Factory {
uint256 gasMarker = gasleft();

// Verify that the calldata injection came from the protocol frontend
// NOTE: fail result causes function to return rather than revert.
// This allows signature data to be stored, which helps prevent
// replay attacks.
if (!_verifyProtocol(userCall.to, protocolCall, verification)) {
return;
}

// TODO: Make sure all searcher nonces are incremented on fail.
// and that the signatures are valid.
bool valid = _verifyProtocol(userCall.metaTx.to, protocolCall, verification) && _verifyUser(protocolCall, userCall);

// Check that the value of the tx is greater than or equal to the value specified
// NOTE: a msg.value *higher* than user value could be used by the staging call.
// There is a further check in the handler before the usercall to verify.
require(msg.value >= userCall.value, "ERR-H03 ValueExceedsBalance");
require(searcherCalls.length < type(uint8).max - 1, "ERR-F02 TooManySearcherCalls");
require(
block.number <= userCall.deadline && block.number <= verification.proof.deadline, "ERR-F03 DeadlineExceeded"
);

console.log("initial verification gas cost", gasMarker - gasleft());
if(msg.value < userCall.metaTx.value) { valid = false; }
if(searcherCalls.length >= type(uint8).max - 1) { valid = false; }
if(block.number > userCall.metaTx.deadline || block.number > verification.proof.deadline) { valid = false; }
if(tx.gasprice > userCall.metaTx.maxFeePerGas) { valid = false; }
if (!protocolCall.callConfig.allowsZeroSearchers() || protocolCall.callConfig.needsSearcherFullfillment()) {
if (searcherCalls.length == 0) { valid = false;}
}

gasMarker = gasleft();

// Get the execution environment
address environment = _setExecutionEnvironment(protocolCall, userCall.from, verification.proof.controlCodeHash);
address environment = _getExecutionEnvironmentCustom(userCall.metaTx.from, verification.proof.controlCodeHash, protocolCall.to, protocolCall.callConfig);
valid = valid && environment.codehash != bytes32(0);

// Gracefully return if not valid. This allows signature data to be stored, which helps prevent
// replay attacks.
if (!valid) {
return;
}

try this.execute{value: msg.value}(protocolCall, userCall.metaTx, searcherCalls, environment, verification.proof.callChainHash)
returns (uint256 accruedGasRebate) {
// Gas Refund to sender only if execution is successful
_executeGasRefund(msg.sender, accruedGasRebate);

} catch {
// TODO: This portion needs more nuanced logic
if (protocolCall.callConfig.allowsReuseUserOps()) {
revert("ERR-F07 RevertToReuse");
}
}

console.log("contract creation gas cost", gasMarker - gasleft());

gasMarker = gasleft();
console.log("total gas used", gasMarker - gasleft());
}

function execute(
ProtocolCall calldata protocolCall,
UserMetaTx calldata userCall,
SearcherCall[] calldata searcherCalls,
address environment,
bytes32 callChainHash
) external payable returns (uint256 accruedGasRebate) {
// This is a self.call made externally so that it can be used with try/catch
require(msg.sender == address(this), "ERR-F06 InvalidAccess");

// Initialize the locks
_initializeEscrowLocks(protocolCall, environment, uint8(searcherCalls.length));
Expand All @@ -64,20 +94,17 @@ contract Atlas is Test, Factory {
bytes32 callChainHashHead = _execute(protocolCall, userCall, searcherCalls, environment);

// Verify that the meta transactions were executed in the correct sequence
require(callChainHashHead == verification.proof.callChainHash, "ERR-F05 InvalidCallChain");
require(callChainHashHead == callChainHash, "ERR-F05 InvalidCallChain");

// Gas Refund to sender
_executeGasRefund(msg.sender);
accruedGasRebate = _getAccruedGasRebate();

// Release the lock
_releaseEscrowLocks();

console.log("ex contract creation gas cost", gasMarker - gasleft());
}

function _execute(
ProtocolCall calldata protocolCall,
UserCall calldata userCall,
UserMetaTx calldata userCall,
SearcherCall[] calldata searcherCalls,
address environment
) internal returns (bytes32) {
Expand Down Expand Up @@ -114,6 +141,9 @@ contract Atlas is Test, Factory {

// If no searcher was successful, manually transition the lock
if (!auctionAlreadyWon) {
if (protocolCall.callConfig.needsSearcherFullfillment()) {
revert("ERR-F08 UserNotFulfilled");
}
_notMadJustDisappointed();
}

Expand Down
12 changes: 8 additions & 4 deletions src/contracts/atlas/Escrow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,13 @@ contract Escrow is ProtocolVerifier, SafetyLocks, SearcherWrapper {
///////////////////////////////////////////////////
function _executeStagingCall(
ProtocolCall calldata protocolCall,
UserCall calldata userCall,
UserMetaTx calldata userCall,
address environment
) internal stagingLock(protocolCall, environment) returns (bytes memory stagingReturnData) {
stagingReturnData = IExecutionEnvironment(environment).stagingWrapper{value: msg.value}(userCall);
}

function _executeUserCall(UserCall calldata userCall, address environment)
function _executeUserCall(UserMetaTx calldata userCall, address environment)
internal
userLock(userCall, environment)
returns (bytes memory userReturnData)
Expand Down Expand Up @@ -179,8 +179,12 @@ contract Escrow is ProtocolVerifier, SafetyLocks, SearcherWrapper {
IExecutionEnvironment(environment).verificationWrapper(stagingReturnData, userReturnData);
}

function _executeGasRefund(address gasPayor) internal {
uint256 gasRebate = uint256(_escrowKey.gasRefund) * tx.gasprice;
function _getAccruedGasRebate() internal view returns (uint256 accruedGasRebate) {
accruedGasRebate = uint256(_escrowKey.gasRefund);
}

function _executeGasRefund(address gasPayor, uint256 accruedGasRebate) internal {
uint256 gasRebate = accruedGasRebate * tx.gasprice;

/*
emit UserTxResult(
Expand Down
6 changes: 3 additions & 3 deletions src/contracts/atlas/ExecutionEnvironment.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {IProtocolControl} from "../interfaces/IProtocolControl.sol";

import {SafeTransferLib, ERC20} from "solmate/utils/SafeTransferLib.sol";

import {UserCall, ProtocolCall, SearcherCall, BidData} from "../types/CallTypes.sol";
import {UserMetaTx, ProtocolCall, SearcherCall, BidData} from "../types/CallTypes.sol";

import {CallVerification} from "../libraries/CallVerification.sol";
import {CallBits} from "../libraries/CallBits.sol";
Expand Down Expand Up @@ -60,7 +60,7 @@ contract ExecutionEnvironment is Test {
//////////////////////////////////
/// CORE CALL FUNCTIONS ///
//////////////////////////////////
function stagingWrapper(UserCall calldata userCall)
function stagingWrapper(UserMetaTx calldata userCall)
external
returns (bytes memory)
{
Expand Down Expand Up @@ -92,7 +92,7 @@ contract ExecutionEnvironment is Test {

}

function userWrapper(UserCall calldata userCall) external payable returns (bytes memory userData) {
function userWrapper(UserMetaTx calldata userCall) external payable returns (bytes memory userData) {
// msg.sender = atlas
// address(this) = ExecutionEnvironment

Expand Down
2 changes: 1 addition & 1 deletion src/contracts/atlas/Factory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ contract Factory is Test, Escrow, TokenTransfers {
view
returns (address executionEnvironment)
{
executionEnvironment = _getExecutionEnvironment(userCall.from, protocolControl.codehash, protocolControl);
executionEnvironment = _getExecutionEnvironment(userCall.metaTx.from, protocolControl.codehash, protocolControl);
}

function _getExecutionEnvironment(address user, bytes32 controlCodeHash, address protocolControl)
Expand Down
103 changes: 100 additions & 3 deletions src/contracts/atlas/ProtocolVerification.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,19 @@ contract ProtocolVerifier is EIP712, ProtocolIntegration {
"ProtocolProof(address from,address to,uint256 nonce,uint256 deadline,bytes32 userCallHash,bytes32 callChainHash,bytes32 controlCodeHash)"
);

bytes32 public constant USER_TYPE_HASH = keccak256(
"UserMetaTx(address from,address to,uint256 deadline,uint256 gas,uint256 nonce,uint256 maxFeePerGas,uint256 value,address control,bytes32 data)"
);

mapping(address => uint256) public userNonces;

constructor() EIP712("ProtoCallHandler", "0.0.1") {}


//
// PROTOCOL VERIFICATION
//

// Verify that the protocol's front end generated the staging
// information and that it matches the on-chain data.
// Verify that the protocol's front end's data is based on
Expand All @@ -42,7 +53,7 @@ contract ProtocolVerifier is EIP712, ProtocolIntegration {
{
// Verify the signature before storing any data to avoid
// spoof transactions clogging up protocol nonces
require(_verifySignature(verification), "ERR-PV01 InvalidSignature");
require(_verifyProtocolSignature(verification), "ERR-PV01 InvalidSignature");

// NOTE: to avoid replay attacks arising from key management errors,
// the state changes below must be *saved* even if they render the
Expand Down Expand Up @@ -107,10 +118,12 @@ contract ProtocolVerifier is EIP712, ProtocolIntegration {
unchecked {
signatories[verification.proof.from].nonce = uint64(verification.proof.nonce) + 1;
}
} else {
} else if (verification.proof.nonce == signatory.nonce + 1) {
unchecked {
++signatories[verification.proof.from].nonce;
}
} else {
return false;
}
}

Expand All @@ -132,7 +145,7 @@ contract ProtocolVerifier is EIP712, ProtocolIntegration {
);
}

function _verifySignature(Verification calldata verification) internal view returns (bool) {
function _verifyProtocolSignature(Verification calldata verification) internal view returns (bool) {
address signer = _hashTypedDataV4(_getProofHash(verification.proof)).recover(verification.signature);

return signer == verification.proof.from;
Expand All @@ -142,4 +155,88 @@ contract ProtocolVerifier is EIP712, ProtocolIntegration {
function getVerificationPayload(Verification memory verification) public view returns (bytes32 payload) {
payload = _hashTypedDataV4(_getProofHash(verification.proof));
}

//
// USER VERIFICATION
//

// Verify the user's meta transaction
function _verifyUser(ProtocolCall calldata protocolCall, UserCall calldata userCall)
internal
returns (bool)
{

// Verify the signature before storing any data to avoid
// spoof transactions clogging up protocol userNonces
require(_verifyUserSignature(userCall), "ERR-UV01 InvalidSignature");

if (userCall.metaTx.control != protocolCall.to) {
return (false);
}

uint256 userNonce = userNonces[userCall.metaTx.from];

// If the protocol indicated that they only accept sequenced userNonces
// (IE for FCFS execution), check and make sure the order is correct
// NOTE: allowing only sequenced userNonces could create a scenario in
// which builders or validators may be able to profit via censorship.
// Protocols are encouraged to rely on the deadline parameter
// to prevent replay attacks.
if (protocolCall.callConfig.needsSequencedNonces()) {
if (userCall.metaTx.nonce != userNonce + 1) {
return (false);
}
unchecked {
++userNonces[userCall.metaTx.from];
}

// If not sequenced, check to see if this nonce is highest and store
// it if so. This ensures nonce + 1 will always be available.
} else {
if (userCall.metaTx.nonce > userNonce + 1) {
unchecked {
userNonces[userCall.metaTx.from] = userCall.metaTx.nonce + 1;
}
} else if (userCall.metaTx.nonce == userNonce + 1) {
unchecked {
++userNonces[userCall.metaTx.from];
}
} else {
return false;
}
}

return (true);
}

function _getProofHash(UserMetaTx memory metaTx) internal pure returns (bytes32 proofHash) {
proofHash = keccak256(
abi.encode(
USER_TYPE_HASH,
metaTx.from,
metaTx.to,
metaTx.deadline,
metaTx.gas,
metaTx.nonce,
metaTx.maxFeePerGas,
metaTx.value,
metaTx.control,
keccak256(metaTx.data)
)
);
}

function _verifyUserSignature(UserCall calldata userCall) internal view returns (bool) {
address signer = _hashTypedDataV4(_getProofHash(userCall.metaTx)).recover(userCall.signature);

return signer == userCall.metaTx.from;
}

function getUserCallPayload(UserCall memory userCall) public view returns (bytes32 payload) {
payload = _hashTypedDataV4(_getProofHash(userCall.metaTx));
}

function nextUserNonce(address user) external view returns (uint256 nextNonce) {
nextNonce = userNonces[user] + 1;
}
}
5 changes: 2 additions & 3 deletions src/contracts/atlas/SafetyLocks.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {IExecutionEnvironment} from "../interfaces/IExecutionEnvironment.sol";
import {SafetyBits} from "../libraries/SafetyBits.sol";
import {CallBits} from "../libraries/CallBits.sol";

import {ProtocolCall, UserCall} from "../types/CallTypes.sol";
import {ProtocolCall, UserMetaTx} from "../types/CallTypes.sol";
import "../types/LockTypes.sol";
import "../types/EscrowTypes.sol";

Expand Down Expand Up @@ -84,14 +84,13 @@ contract SafetyLocks is Test {
}
}

modifier userLock(UserCall calldata userCall, address environment) {
modifier userLock(UserMetaTx calldata userCall, address environment) {
// msg.sender = user EOA
// address(this) = atlas

EscrowKey memory escrowKey = _escrowKey;

require(escrowKey.isValidUserLock(environment), "ERR-SL032 InvalidLockStage");
require(userCall.from == msg.sender, "ERR-SL070 SenderNotFrom");

// NOTE: the approvedCaller is set to the userCall's to so that it can callback
// into the ExecutionEnvironment if needed.
Expand Down
Loading

0 comments on commit 4a90abf

Please sign in to comment.