LaminatedProxy
The LaminatedProxy
contract acts as a personalized transaction manager for each user (eg developer, dapp, protocol). It serves as a mempool where users can queue their transaction requests with specific conditions. It is uniquely deployed for every user and is owned by the EOA address that was used to deploy it. The contract manages the execution order, timing, funds and other parameters of the transactions.
Customizable Transaction Execution: Users can set specific conditions for their transactions within the LaminatedProxy
, such as preventing frontruns, specifying delays, or setting order dependencies.
For example, suppose you want to have a simple scheduler that shouldn't be frontrun. You would first create CallObject
s for the job, and then push it into the LaminatedProxy
.
function userLand() public returns (uint256) {
// send proxy some eth
pusherLaminated.transfer(1 ether);
// Userland operations
CallObject[] memory pusherCallObjs = new CallObject[](4);
pusherCallObjs[0] = CallObject({
amount: 0,
addr: address(counter),
gas: 10000000,
callvalue: abi.encodeWithSignature("increment()")
});
pusherCallObjs[1] = CallObject({amount: _tipWei, addr: address(callbreaker), gas: 10000000, callvalue: ""});
CallObject memory callObjectContinueFunctionPointer = CallObject({
amount: 0,
addr: address(counter),
gas: 10000000,
callvalue: abi.encodeWithSignature("shouldContinue()")
});
bytes memory callObjectContinueFnPtr = abi.encode(callObjectContinueFunctionPointer);
pusherCallObjs[2] = CallObject({
amount: 0,
addr: pusherLaminated,
gas: 10000000,
callvalue: abi.encodeWithSignature("copyCurrentJob(uint256,bytes)", _blocksInADay, callObjectContinueFnPtr)
});
pusherCallObjs[3] = CallObject({
amount: 0,
addr: address(counter),
gas: 10000000,
callvalue: abi.encodeWithSignature("frontrunBlocker()")
});
SolverData[] memory dataValues = getDataValues();
return laminator.pushToProxy(abi.encode(pusherCallObjs), 1, SELECTOR, dataValues);
}
The LaminatedProxy
contains a function, push
, that is designed to schedule a deferred function call within a smart contract environment. As referenced in the previous sections, it accepts an encoded array of CallObjects
and a delay parameter specifying the number of blocks to wait before execution.
The function is restricted to be callable only by specific contracts (Laminator or LaminatedProxy) and supports re-entrant calls to facilitate schedulers with tail recursion. Upon invocation, it decodes the input, initializes a CallObjectHolder, assigns a sequence number, and stores the call for delayed execution.
/// @notice Pushes a deferred function call to be executed after a certain delay.
/// @dev Adds a new CallObject to the `deferredCalls` mapping and emits a CallPushed event.
/// The function can only be called by the Laminator or the LaminatedProxy contract itself.
/// It can also be called re-entrantly to enable the contract to do cronjobs with tail recursion.
/// @param input The encoded CallObject containing information about the function call to defer.
/// @param delay The number of blocks to delay before the function call can be executed.
/// @param data Additional data to be associated with the sequence of call objs
/// @return callSequenceNumber The sequence number assigned to this deferred call.
function push(bytes memory input, uint256 delay, SolverData[] memory data)
public
onlyLaminatorOrProxy
returns (uint256 callSequenceNumber)
{
CallObjectHolder memory holder;
holder.callObjs = abi.decode(input, (CallObject[]));
callSequenceNumber = _incrementSequenceNumber();
holder.initialized = true;
holder.executed = false;
holder.nonce = executingNonce;
holder.data = data;
holder.firstCallableBlock = block.number + delay;
_deferredCalls[callSequenceNumber].store(holder);
emit CallableBlock(block.number + delay, block.number);
emit CallPushed(holder.callObjs, callSequenceNumber, data);
}
The pull
function is crucial for executing deferred calls at the appropriate time, ensuring that all conditions and state changes are logged and managed correctly. It sequentially executes all the calls in the CallObjectHolder and emits events to log the executed calls and the sequence number.
/// @param seqNumber The sequence number of the deferred call to be executed.
/// @return returnValue The return value of the executed deferred call.
function pull(uint256 seqNumber) external nonReentrant onlyCallBreaker returns (bytes memory returnValue) {
CallObjectHolderStorage storage cohStorage = _deferredCalls[seqNumber];
_checkPrePush(cohStorage);
cohStorage.executed = true;
_setCurrentlyExecutingSeqNum(seqNumber);
_setExecuting();
CallObjectHolder memory coh = cohStorage.load();
emit CallableBlock(coh.firstCallableBlock, block.number);
returnValue = _executeAll(coh.callObjs);
emit CallPulled(coh.callObjs, seqNumber);
_setFree();
}
There are other functions that assist with the execution of the transactions:
copyCurrentJob: This function allows the proxy to copy the current job and schedule it to run at a later time. It is useful for creating schedulers that should run at a specific interval, such as every day or every week.
function copyCurrentJob(uint256 delay, bytes calldata shouldCopy) external onlyProxy returns (uint256) {
return _copyJob(executingSequenceNumber(), delay, shouldCopy);
}
cancelAllPending: This function allows the proxy to cancel all pending jobs. It is useful for cancelling all pending jobs if the proxy is not working as expected.
function cancelAllPending() external onlyOwner {
emit CancelledAllPendingCalls(executingNonce);
executingNonce++;
}
ETH-Receiving Contract: Each laminatedProxy serves almost as a smart contract wallet, where the laminatedProxy can receive ETH for operations like push / pull to be executed with the balance.
Interface with CallBreaker: The LaminatedProxy
interfaces with the CallBreaker for the actual execution of the transactions. It relays the queued transactions along with their conditions to the CallBreaker
, which then executes them accordingly.