Transaction Hash:
Block:
24701458 at Mar-20-2026 09:39:23 PM +UTC
Transaction Fee:
0.000012973759109756 ETH
$0.03
Gas Used:
116,629 Gas / 0.111239564 Gwei
Emitted Events:
| 574 |
EntryPoint.Deposited( account=0xfd723b24ee639d12ac1d612f6f04a0c634835354, totalDeposit=26221813596626 )
|
| 575 |
EntryPoint.BeforeExecution( )
|
| 576 |
Ondo.Approval( owner=0xfd723b24ee639d12ac1d612f6f04a0c634835354, spender=0x69460570...25f0Ca22d, amount=404428195392591647006 )
|
| 577 |
EntryPoint.UserOperationEvent( userOpHash=11F9629AA814F0B48A0A591C42880507144F28961F23D53A341C27E89752C2A4, sender=0xfd723b24ee639d12ac1d612f6f04a0c634835354, paymaster=0x00000000...000000000, nonce=1, success=True, actualGasCost=14414293003524, actualGasUsed=118891 )
|
Account State Difference:
| Address | Before | After | State Difference | ||
|---|---|---|---|---|---|
| 0x00000000...6f37da032 | (Entry Point 0.7.0) | 81.724616694345584847 Eth | 81.724617799693843768 Eth | 0.000001105348258921 | |
| 0x3f5EADFD...9C66E9F5D | (Bundler: 0x3f5...f5d) |
1.561843805055057038 Eth
Nonce: 15316
|
1.561845245588950806 Eth
Nonce: 15317
| 0.000001440533893768 | |
|
0x4838B106...B0BAD5f97
Miner
| (Titan Builder) | 16.312276294513112236 Eth | 16.312282125963112236 Eth | 0.00000583145 | |
| 0xfAbA6f8e...577269BE3 | |||||
| 0xfD723b24...634835354 | 0.137655827990928768 Eth | 0.137640308349666323 Eth | 0.000015519641262445 |
Execution Trace
EntryPoint.handleOps( ops=, beneficiary=0x3f5EADFD61fd2c770f8E8E02bb6c2849C66E9F5D )
0xfd723b24ee639d12ac1d612f6f04a0c634835354.19822f7c( )-
BizGuard.STATICCALL( ) -
Null: 0x000...001.d05279b2( ) - ETH 0.000015519641262445
EntryPoint.CALL( )
-
EntryPoint.innerHandleOp( callData=0x26DA7D88000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000FABA6F8E4A5E8AB82F62FE7C39859FA577269BE3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044095EA7B300000000000000000000000069460570C93F9DE5E2EDBC3052BF10125F0CA22D000000000000000000000000000000000000000000000015EC9194F507B2511E00000000000000000000000000000000000000000000000000000000, opInfo=[{name:mUserOp, type:tuple, order:1, indexed:false, value:[{name:sender, type:address, order:1, indexed:false, value:0xfD723b24EE639d12AC1d612f6f04a0C634835354, valueString:0xfD723b24EE639d12AC1d612f6f04a0C634835354}, {name:nonce, type:uint256, order:2, indexed:false, value:1, valueString:1}, {name:verificationGasLimit, type:uint256, order:3, indexed:false, value:68236, valueString:68236}, {name:callGasLimit, type:uint256, order:4, indexed:false, value:51823, valueString:51823}, {name:paymasterVerificationGasLimit, type:uint256, order:5, indexed:false, value:0, valueString:0}, {name:paymasterPostOpGasLimit, type:uint256, order:6, indexed:false, value:0, valueString:0}, {name:preVerificationGas, type:uint256, order:7, indexed:false, value:46452, valueString:46452}, {name:paymaster, type:address, order:8, indexed:false, value:0x0000000000000000000000000000000000000000, valueString:0x0000000000000000000000000000000000000000}, {name:maxFeePerGas, type:uint256, order:9, indexed:false, value:157477966, valueString:157477966}, {name:maxPriorityFeePerGas, type:uint256, order:10, indexed:false, value:60000000, valueString:60000000}], valueString:[{name:sender, type:address, order:1, indexed:false, value:0xfD723b24EE639d12AC1d612f6f04a0C634835354, valueString:0xfD723b24EE639d12AC1d612f6f04a0C634835354}, {name:nonce, type:uint256, order:2, indexed:false, value:1, valueString:1}, {name:verificationGasLimit, type:uint256, order:3, indexed:false, value:68236, valueString:68236}, {name:callGasLimit, type:uint256, order:4, indexed:false, value:51823, valueString:51823}, {name:paymasterVerificationGasLimit, type:uint256, order:5, indexed:false, value:0, valueString:0}, {name:paymasterPostOpGasLimit, type:uint256, order:6, indexed:false, value:0, valueString:0}, {name:preVerificationGas, type:uint256, order:7, indexed:false, value:46452, valueString:46452}, {name:paymaster, type:address, order:8, indexed:false, value:0x0000000000000000000000000000000000000000, valueString:0x0000000000000000000000000000000000000000}, {name:maxFeePerGas, type:uint256, order:9, indexed:false, value:157477966, valueString:157477966}, {name:maxPriorityFeePerGas, type:uint256, order:10, indexed:false, value:60000000, valueString:60000000}]}, {name:userOpHash, type:bytes32, order:2, indexed:false, value:11F9629AA814F0B48A0A591C42880507144F28961F23D53A341C27E89752C2A4, valueString:11F9629AA814F0B48A0A591C42880507144F28961F23D53A341C27E89752C2A4}, {name:prefund, type:uint256, order:3, indexed:false, value:26221813596626, valueString:26221813596626}, {name:contextOffset, type:uint256, order:4, indexed:false, value:96, valueString:96}, {name:preOpGas, type:uint256, order:5, indexed:false, value:86591, valueString:86591}], context=0x ) => ( actualGasCost=14414293003524 )0xfd723b24ee639d12ac1d612f6f04a0c634835354.26da7d88( )-
Ondo.approve( spender=0x69460570c93f9DE5E2edbC3052bf10125f0Ca22d, rawAmount=404428195392591647006 ) => ( True )
-
- ETH 0.000014414293003524
Bundler: 0x3f5...f5d.CALL( )
File 1 of 3: EntryPoint
File 2 of 3: Ondo
File 3 of 3: BizGuard
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/introspection/ERC165.sol)
pragma solidity ^0.8.20;
import {IERC165} from "./IERC165.sol";
/**
* @dev Implementation of the {IERC165} interface.
*
* Contracts that want to implement ERC165 should inherit from this contract and override {supportsInterface} to check
* for the additional interface id that will be supported. For example:
*
* ```solidity
* function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
* return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId);
* }
* ```
*/
abstract contract ERC165 is IERC165 {
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {
return interfaceId == type(IERC165).interfaceId;
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/introspection/IERC165.sol)
pragma solidity ^0.8.20;
/**
* @dev Interface of the ERC165 standard, as defined in the
* https://eips.ethereum.org/EIPS/eip-165[EIP].
*
* Implementers can declare support of contract interfaces, which can then be
* queried by others ({ERC165Checker}).
*
* For an implementation, see {ERC165}.
*/
interface IERC165 {
/**
* @dev Returns true if this contract implements the interface defined by
* `interfaceId`. See the corresponding
* https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
* to learn more about how these ids are created.
*
* This function call must use less than 30 000 gas.
*/
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/ReentrancyGuard.sol)
pragma solidity ^0.8.20;
/**
* @dev Contract module that helps prevent reentrant calls to a function.
*
* Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
* available, which can be applied to functions to make sure there are no nested
* (reentrant) calls to them.
*
* Note that because there is a single `nonReentrant` guard, functions marked as
* `nonReentrant` may not call one another. This can be worked around by making
* those functions `private`, and then adding `external` `nonReentrant` entry
* points to them.
*
* TIP: If you would like to learn more about reentrancy and alternative ways
* to protect against it, check out our blog post
* https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
*/
abstract contract ReentrancyGuard {
// Booleans are more expensive than uint256 or any type that takes up a full
// word because each write operation emits an extra SLOAD to first read the
// slot's contents, replace the bits taken up by the boolean, and then write
// back. This is the compiler's defense against contract upgrades and
// pointer aliasing, and it cannot be disabled.
// The values being non-zero value makes deployment a bit more expensive,
// but in exchange the refund on every call to nonReentrant will be lower in
// amount. Since refunds are capped to a percentage of the total
// transaction's gas, it is best to keep them low in cases like this one, to
// increase the likelihood of the full refund coming into effect.
uint256 private constant NOT_ENTERED = 1;
uint256 private constant ENTERED = 2;
uint256 private _status;
/**
* @dev Unauthorized reentrant call.
*/
error ReentrancyGuardReentrantCall();
constructor() {
_status = NOT_ENTERED;
}
/**
* @dev Prevents a contract from calling itself, directly or indirectly.
* Calling a `nonReentrant` function from another `nonReentrant`
* function is not supported. It is possible to prevent this from happening
* by making the `nonReentrant` function external, and making it call a
* `private` function that does the actual work.
*/
modifier nonReentrant() {
_nonReentrantBefore();
_;
_nonReentrantAfter();
}
function _nonReentrantBefore() private {
// On the first call to nonReentrant, _status will be NOT_ENTERED
if (_status == ENTERED) {
revert ReentrancyGuardReentrantCall();
}
// Any calls to nonReentrant after this point will fail
_status = ENTERED;
}
function _nonReentrantAfter() private {
// By storing the original value once again, a refund is triggered (see
// https://eips.ethereum.org/EIPS/eip-2200)
_status = NOT_ENTERED;
}
/**
* @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
* `nonReentrant` function in the call stack.
*/
function _reentrancyGuardEntered() internal view returns (bool) {
return _status == ENTERED;
}
}
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.23;
/* solhint-disable avoid-low-level-calls */
/* solhint-disable no-inline-assembly */
import "../interfaces/IAccount.sol";
import "../interfaces/IAccountExecute.sol";
import "../interfaces/IPaymaster.sol";
import "../interfaces/IEntryPoint.sol";
import "../utils/Exec.sol";
import "./StakeManager.sol";
import "./SenderCreator.sol";
import "./Helpers.sol";
import "./NonceManager.sol";
import "./UserOperationLib.sol";
import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
/*
* Account-Abstraction (EIP-4337) singleton EntryPoint implementation.
* Only one instance required on each chain.
*/
/// @custom:security-contact https://bounty.ethereum.org
contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, ERC165 {
using UserOperationLib for PackedUserOperation;
SenderCreator private immutable _senderCreator = new SenderCreator();
function senderCreator() internal view virtual returns (SenderCreator) {
return _senderCreator;
}
//compensate for innerHandleOps' emit message and deposit refund.
// allow some slack for future gas price changes.
uint256 private constant INNER_GAS_OVERHEAD = 10000;
// Marker for inner call revert on out of gas
bytes32 private constant INNER_OUT_OF_GAS = hex"deaddead";
bytes32 private constant INNER_REVERT_LOW_PREFUND = hex"deadaa51";
uint256 private constant REVERT_REASON_MAX_LEN = 2048;
uint256 private constant PENALTY_PERCENT = 10;
/// @inheritdoc IERC165
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
// note: solidity "type(IEntryPoint).interfaceId" is without inherited methods but we want to check everything
return interfaceId == (type(IEntryPoint).interfaceId ^ type(IStakeManager).interfaceId ^ type(INonceManager).interfaceId) ||
interfaceId == type(IEntryPoint).interfaceId ||
interfaceId == type(IStakeManager).interfaceId ||
interfaceId == type(INonceManager).interfaceId ||
super.supportsInterface(interfaceId);
}
/**
* Compensate the caller's beneficiary address with the collected fees of all UserOperations.
* @param beneficiary - The address to receive the fees.
* @param amount - Amount to transfer.
*/
function _compensate(address payable beneficiary, uint256 amount) internal {
require(beneficiary != address(0), "AA90 invalid beneficiary");
(bool success, ) = beneficiary.call{value: amount}("");
require(success, "AA91 failed send to beneficiary");
}
/**
* Execute a user operation.
* @param opIndex - Index into the opInfo array.
* @param userOp - The userOp to execute.
* @param opInfo - The opInfo filled by validatePrepayment for this userOp.
* @return collected - The total amount this userOp paid.
*/
function _executeUserOp(
uint256 opIndex,
PackedUserOperation calldata userOp,
UserOpInfo memory opInfo
)
internal
returns
(uint256 collected) {
uint256 preGas = gasleft();
bytes memory context = getMemoryBytesFromOffset(opInfo.contextOffset);
bool success;
{
uint256 saveFreePtr;
assembly ("memory-safe") {
saveFreePtr := mload(0x40)
}
bytes calldata callData = userOp.callData;
bytes memory innerCall;
bytes4 methodSig;
assembly {
let len := callData.length
if gt(len, 3) {
methodSig := calldataload(callData.offset)
}
}
if (methodSig == IAccountExecute.executeUserOp.selector) {
bytes memory executeUserOp = abi.encodeCall(IAccountExecute.executeUserOp, (userOp, opInfo.userOpHash));
innerCall = abi.encodeCall(this.innerHandleOp, (executeUserOp, opInfo, context));
} else
{
innerCall = abi.encodeCall(this.innerHandleOp, (callData, opInfo, context));
}
assembly ("memory-safe") {
success := call(gas(), address(), 0, add(innerCall, 0x20), mload(innerCall), 0, 32)
collected := mload(0)
mstore(0x40, saveFreePtr)
}
}
if (!success) {
bytes32 innerRevertCode;
assembly ("memory-safe") {
let len := returndatasize()
if eq(32,len) {
returndatacopy(0, 0, 32)
innerRevertCode := mload(0)
}
}
if (innerRevertCode == INNER_OUT_OF_GAS) {
// handleOps was called with gas limit too low. abort entire bundle.
//can only be caused by bundler (leaving not enough gas for inner call)
revert FailedOp(opIndex, "AA95 out of gas");
} else if (innerRevertCode == INNER_REVERT_LOW_PREFUND) {
// innerCall reverted on prefund too low. treat entire prefund as "gas cost"
uint256 actualGas = preGas - gasleft() + opInfo.preOpGas;
uint256 actualGasCost = opInfo.prefund;
emitPrefundTooLow(opInfo);
emitUserOperationEvent(opInfo, false, actualGasCost, actualGas);
collected = actualGasCost;
} else {
emit PostOpRevertReason(
opInfo.userOpHash,
opInfo.mUserOp.sender,
opInfo.mUserOp.nonce,
Exec.getReturnData(REVERT_REASON_MAX_LEN)
);
uint256 actualGas = preGas - gasleft() + opInfo.preOpGas;
collected = _postExecution(
IPaymaster.PostOpMode.postOpReverted,
opInfo,
context,
actualGas
);
}
}
}
function emitUserOperationEvent(UserOpInfo memory opInfo, bool success, uint256 actualGasCost, uint256 actualGas) internal virtual {
emit UserOperationEvent(
opInfo.userOpHash,
opInfo.mUserOp.sender,
opInfo.mUserOp.paymaster,
opInfo.mUserOp.nonce,
success,
actualGasCost,
actualGas
);
}
function emitPrefundTooLow(UserOpInfo memory opInfo) internal virtual {
emit UserOperationPrefundTooLow(
opInfo.userOpHash,
opInfo.mUserOp.sender,
opInfo.mUserOp.nonce
);
}
/// @inheritdoc IEntryPoint
function handleOps(
PackedUserOperation[] calldata ops,
address payable beneficiary
) public nonReentrant {
uint256 opslen = ops.length;
UserOpInfo[] memory opInfos = new UserOpInfo[](opslen);
unchecked {
for (uint256 i = 0; i < opslen; i++) {
UserOpInfo memory opInfo = opInfos[i];
(
uint256 validationData,
uint256 pmValidationData
) = _validatePrepayment(i, ops[i], opInfo);
_validateAccountAndPaymasterValidationData(
i,
validationData,
pmValidationData,
address(0)
);
}
uint256 collected = 0;
emit BeforeExecution();
for (uint256 i = 0; i < opslen; i++) {
collected += _executeUserOp(i, ops[i], opInfos[i]);
}
_compensate(beneficiary, collected);
}
}
/// @inheritdoc IEntryPoint
function handleAggregatedOps(
UserOpsPerAggregator[] calldata opsPerAggregator,
address payable beneficiary
) public nonReentrant {
uint256 opasLen = opsPerAggregator.length;
uint256 totalOps = 0;
for (uint256 i = 0; i < opasLen; i++) {
UserOpsPerAggregator calldata opa = opsPerAggregator[i];
PackedUserOperation[] calldata ops = opa.userOps;
IAggregator aggregator = opa.aggregator;
//address(1) is special marker of "signature error"
require(
address(aggregator) != address(1),
"AA96 invalid aggregator"
);
if (address(aggregator) != address(0)) {
// solhint-disable-next-line no-empty-blocks
try aggregator.validateSignatures(ops, opa.signature) {} catch {
revert SignatureValidationFailed(address(aggregator));
}
}
totalOps += ops.length;
}
UserOpInfo[] memory opInfos = new UserOpInfo[](totalOps);
uint256 opIndex = 0;
for (uint256 a = 0; a < opasLen; a++) {
UserOpsPerAggregator calldata opa = opsPerAggregator[a];
PackedUserOperation[] calldata ops = opa.userOps;
IAggregator aggregator = opa.aggregator;
uint256 opslen = ops.length;
for (uint256 i = 0; i < opslen; i++) {
UserOpInfo memory opInfo = opInfos[opIndex];
(
uint256 validationData,
uint256 paymasterValidationData
) = _validatePrepayment(opIndex, ops[i], opInfo);
_validateAccountAndPaymasterValidationData(
i,
validationData,
paymasterValidationData,
address(aggregator)
);
opIndex++;
}
}
emit BeforeExecution();
uint256 collected = 0;
opIndex = 0;
for (uint256 a = 0; a < opasLen; a++) {
UserOpsPerAggregator calldata opa = opsPerAggregator[a];
emit SignatureAggregatorChanged(address(opa.aggregator));
PackedUserOperation[] calldata ops = opa.userOps;
uint256 opslen = ops.length;
for (uint256 i = 0; i < opslen; i++) {
collected += _executeUserOp(opIndex, ops[i], opInfos[opIndex]);
opIndex++;
}
}
emit SignatureAggregatorChanged(address(0));
_compensate(beneficiary, collected);
}
/**
* A memory copy of UserOp static fields only.
* Excluding: callData, initCode and signature. Replacing paymasterAndData with paymaster.
*/
struct MemoryUserOp {
address sender;
uint256 nonce;
uint256 verificationGasLimit;
uint256 callGasLimit;
uint256 paymasterVerificationGasLimit;
uint256 paymasterPostOpGasLimit;
uint256 preVerificationGas;
address paymaster;
uint256 maxFeePerGas;
uint256 maxPriorityFeePerGas;
}
struct UserOpInfo {
MemoryUserOp mUserOp;
bytes32 userOpHash;
uint256 prefund;
uint256 contextOffset;
uint256 preOpGas;
}
/**
* Inner function to handle a UserOperation.
* Must be declared "external" to open a call context, but it can only be called by handleOps.
* @param callData - The callData to execute.
* @param opInfo - The UserOpInfo struct.
* @param context - The context bytes.
* @return actualGasCost - the actual cost in eth this UserOperation paid for gas
*/
function innerHandleOp(
bytes memory callData,
UserOpInfo memory opInfo,
bytes calldata context
) external returns (uint256 actualGasCost) {
uint256 preGas = gasleft();
require(msg.sender == address(this), "AA92 internal call only");
MemoryUserOp memory mUserOp = opInfo.mUserOp;
uint256 callGasLimit = mUserOp.callGasLimit;
unchecked {
// handleOps was called with gas limit too low. abort entire bundle.
if (
gasleft() * 63 / 64 <
callGasLimit +
mUserOp.paymasterPostOpGasLimit +
INNER_GAS_OVERHEAD
) {
assembly ("memory-safe") {
mstore(0, INNER_OUT_OF_GAS)
revert(0, 32)
}
}
}
IPaymaster.PostOpMode mode = IPaymaster.PostOpMode.opSucceeded;
if (callData.length > 0) {
bool success = Exec.call(mUserOp.sender, 0, callData, callGasLimit);
if (!success) {
bytes memory result = Exec.getReturnData(REVERT_REASON_MAX_LEN);
if (result.length > 0) {
emit UserOperationRevertReason(
opInfo.userOpHash,
mUserOp.sender,
mUserOp.nonce,
result
);
}
mode = IPaymaster.PostOpMode.opReverted;
}
}
unchecked {
uint256 actualGas = preGas - gasleft() + opInfo.preOpGas;
return _postExecution(mode, opInfo, context, actualGas);
}
}
/// @inheritdoc IEntryPoint
function getUserOpHash(
PackedUserOperation calldata userOp
) public view returns (bytes32) {
return
keccak256(abi.encode(userOp.hash(), address(this), block.chainid));
}
/**
* Copy general fields from userOp into the memory opInfo structure.
* @param userOp - The user operation.
* @param mUserOp - The memory user operation.
*/
function _copyUserOpToMemory(
PackedUserOperation calldata userOp,
MemoryUserOp memory mUserOp
) internal pure {
mUserOp.sender = userOp.sender;
mUserOp.nonce = userOp.nonce;
(mUserOp.verificationGasLimit, mUserOp.callGasLimit) = UserOperationLib.unpackUints(userOp.accountGasLimits);
mUserOp.preVerificationGas = userOp.preVerificationGas;
(mUserOp.maxPriorityFeePerGas, mUserOp.maxFeePerGas) = UserOperationLib.unpackUints(userOp.gasFees);
bytes calldata paymasterAndData = userOp.paymasterAndData;
if (paymasterAndData.length > 0) {
require(
paymasterAndData.length >= UserOperationLib.PAYMASTER_DATA_OFFSET,
"AA93 invalid paymasterAndData"
);
(mUserOp.paymaster, mUserOp.paymasterVerificationGasLimit, mUserOp.paymasterPostOpGasLimit) = UserOperationLib.unpackPaymasterStaticFields(paymasterAndData);
} else {
mUserOp.paymaster = address(0);
mUserOp.paymasterVerificationGasLimit = 0;
mUserOp.paymasterPostOpGasLimit = 0;
}
}
/**
* Get the required prefunded gas fee amount for an operation.
* @param mUserOp - The user operation in memory.
*/
function _getRequiredPrefund(
MemoryUserOp memory mUserOp
) internal pure returns (uint256 requiredPrefund) {
unchecked {
uint256 requiredGas = mUserOp.verificationGasLimit +
mUserOp.callGasLimit +
mUserOp.paymasterVerificationGasLimit +
mUserOp.paymasterPostOpGasLimit +
mUserOp.preVerificationGas;
requiredPrefund = requiredGas * mUserOp.maxFeePerGas;
}
}
/**
* Create sender smart contract account if init code is provided.
* @param opIndex - The operation index.
* @param opInfo - The operation info.
* @param initCode - The init code for the smart contract account.
*/
function _createSenderIfNeeded(
uint256 opIndex,
UserOpInfo memory opInfo,
bytes calldata initCode
) internal {
if (initCode.length != 0) {
address sender = opInfo.mUserOp.sender;
if (sender.code.length != 0)
revert FailedOp(opIndex, "AA10 sender already constructed");
address sender1 = senderCreator().createSender{
gas: opInfo.mUserOp.verificationGasLimit
}(initCode);
if (sender1 == address(0))
revert FailedOp(opIndex, "AA13 initCode failed or OOG");
if (sender1 != sender)
revert FailedOp(opIndex, "AA14 initCode must return sender");
if (sender1.code.length == 0)
revert FailedOp(opIndex, "AA15 initCode must create sender");
address factory = address(bytes20(initCode[0:20]));
emit AccountDeployed(
opInfo.userOpHash,
sender,
factory,
opInfo.mUserOp.paymaster
);
}
}
/// @inheritdoc IEntryPoint
function getSenderAddress(bytes calldata initCode) public {
address sender = senderCreator().createSender(initCode);
revert SenderAddressResult(sender);
}
/**
* Call account.validateUserOp.
* Revert (with FailedOp) in case validateUserOp reverts, or account didn't send required prefund.
* Decrement account's deposit if needed.
* @param opIndex - The operation index.
* @param op - The user operation.
* @param opInfo - The operation info.
* @param requiredPrefund - The required prefund amount.
*/
function _validateAccountPrepayment(
uint256 opIndex,
PackedUserOperation calldata op,
UserOpInfo memory opInfo,
uint256 requiredPrefund,
uint256 verificationGasLimit
)
internal
returns (
uint256 validationData
)
{
unchecked {
MemoryUserOp memory mUserOp = opInfo.mUserOp;
address sender = mUserOp.sender;
_createSenderIfNeeded(opIndex, opInfo, op.initCode);
address paymaster = mUserOp.paymaster;
uint256 missingAccountFunds = 0;
if (paymaster == address(0)) {
uint256 bal = balanceOf(sender);
missingAccountFunds = bal > requiredPrefund
? 0
: requiredPrefund - bal;
}
try
IAccount(sender).validateUserOp{
gas: verificationGasLimit
}(op, opInfo.userOpHash, missingAccountFunds)
returns (uint256 _validationData) {
validationData = _validationData;
} catch {
revert FailedOpWithRevert(opIndex, "AA23 reverted", Exec.getReturnData(REVERT_REASON_MAX_LEN));
}
if (paymaster == address(0)) {
DepositInfo storage senderInfo = deposits[sender];
uint256 deposit = senderInfo.deposit;
if (requiredPrefund > deposit) {
revert FailedOp(opIndex, "AA21 didn't pay prefund");
}
senderInfo.deposit = deposit - requiredPrefund;
}
}
}
/**
* In case the request has a paymaster:
* - Validate paymaster has enough deposit.
* - Call paymaster.validatePaymasterUserOp.
* - Revert with proper FailedOp in case paymaster reverts.
* - Decrement paymaster's deposit.
* @param opIndex - The operation index.
* @param op - The user operation.
* @param opInfo - The operation info.
* @param requiredPreFund - The required prefund amount.
*/
function _validatePaymasterPrepayment(
uint256 opIndex,
PackedUserOperation calldata op,
UserOpInfo memory opInfo,
uint256 requiredPreFund
) internal returns (bytes memory context, uint256 validationData) {
unchecked {
uint256 preGas = gasleft();
MemoryUserOp memory mUserOp = opInfo.mUserOp;
address paymaster = mUserOp.paymaster;
DepositInfo storage paymasterInfo = deposits[paymaster];
uint256 deposit = paymasterInfo.deposit;
if (deposit < requiredPreFund) {
revert FailedOp(opIndex, "AA31 paymaster deposit too low");
}
paymasterInfo.deposit = deposit - requiredPreFund;
uint256 pmVerificationGasLimit = mUserOp.paymasterVerificationGasLimit;
try
IPaymaster(paymaster).validatePaymasterUserOp{gas: pmVerificationGasLimit}(
op,
opInfo.userOpHash,
requiredPreFund
)
returns (bytes memory _context, uint256 _validationData) {
context = _context;
validationData = _validationData;
} catch {
revert FailedOpWithRevert(opIndex, "AA33 reverted", Exec.getReturnData(REVERT_REASON_MAX_LEN));
}
if (preGas - gasleft() > pmVerificationGasLimit) {
revert FailedOp(opIndex, "AA36 over paymasterVerificationGasLimit");
}
}
}
/**
* Revert if either account validationData or paymaster validationData is expired.
* @param opIndex - The operation index.
* @param validationData - The account validationData.
* @param paymasterValidationData - The paymaster validationData.
* @param expectedAggregator - The expected aggregator.
*/
function _validateAccountAndPaymasterValidationData(
uint256 opIndex,
uint256 validationData,
uint256 paymasterValidationData,
address expectedAggregator
) internal view {
(address aggregator, bool outOfTimeRange) = _getValidationData(
validationData
);
if (expectedAggregator != aggregator) {
revert FailedOp(opIndex, "AA24 signature error");
}
if (outOfTimeRange) {
revert FailedOp(opIndex, "AA22 expired or not due");
}
// pmAggregator is not a real signature aggregator: we don't have logic to handle it as address.
// Non-zero address means that the paymaster fails due to some signature check (which is ok only during estimation).
address pmAggregator;
(pmAggregator, outOfTimeRange) = _getValidationData(
paymasterValidationData
);
if (pmAggregator != address(0)) {
revert FailedOp(opIndex, "AA34 signature error");
}
if (outOfTimeRange) {
revert FailedOp(opIndex, "AA32 paymaster expired or not due");
}
}
/**
* Parse validationData into its components.
* @param validationData - The packed validation data (sigFailed, validAfter, validUntil).
* @return aggregator the aggregator of the validationData
* @return outOfTimeRange true if current time is outside the time range of this validationData.
*/
function _getValidationData(
uint256 validationData
) internal view returns (address aggregator, bool outOfTimeRange) {
if (validationData == 0) {
return (address(0), false);
}
ValidationData memory data = _parseValidationData(validationData);
// solhint-disable-next-line not-rely-on-time
outOfTimeRange = block.timestamp > data.validUntil || block.timestamp < data.validAfter;
aggregator = data.aggregator;
}
/**
* Validate account and paymaster (if defined) and
* also make sure total validation doesn't exceed verificationGasLimit.
* This method is called off-chain (simulateValidation()) and on-chain (from handleOps)
* @param opIndex - The index of this userOp into the "opInfos" array.
* @param userOp - The userOp to validate.
*/
function _validatePrepayment(
uint256 opIndex,
PackedUserOperation calldata userOp,
UserOpInfo memory outOpInfo
)
internal
returns (uint256 validationData, uint256 paymasterValidationData)
{
uint256 preGas = gasleft();
MemoryUserOp memory mUserOp = outOpInfo.mUserOp;
_copyUserOpToMemory(userOp, mUserOp);
outOpInfo.userOpHash = getUserOpHash(userOp);
// Validate all numeric values in userOp are well below 128 bit, so they can safely be added
// and multiplied without causing overflow.
uint256 verificationGasLimit = mUserOp.verificationGasLimit;
uint256 maxGasValues = mUserOp.preVerificationGas |
verificationGasLimit |
mUserOp.callGasLimit |
mUserOp.paymasterVerificationGasLimit |
mUserOp.paymasterPostOpGasLimit |
mUserOp.maxFeePerGas |
mUserOp.maxPriorityFeePerGas;
require(maxGasValues <= type(uint120).max, "AA94 gas values overflow");
uint256 requiredPreFund = _getRequiredPrefund(mUserOp);
validationData = _validateAccountPrepayment(
opIndex,
userOp,
outOpInfo,
requiredPreFund,
verificationGasLimit
);
if (!_validateAndUpdateNonce(mUserOp.sender, mUserOp.nonce)) {
revert FailedOp(opIndex, "AA25 invalid account nonce");
}
unchecked {
if (preGas - gasleft() > verificationGasLimit) {
revert FailedOp(opIndex, "AA26 over verificationGasLimit");
}
}
bytes memory context;
if (mUserOp.paymaster != address(0)) {
(context, paymasterValidationData) = _validatePaymasterPrepayment(
opIndex,
userOp,
outOpInfo,
requiredPreFund
);
}
unchecked {
outOpInfo.prefund = requiredPreFund;
outOpInfo.contextOffset = getOffsetOfMemoryBytes(context);
outOpInfo.preOpGas = preGas - gasleft() + userOp.preVerificationGas;
}
}
/**
* Process post-operation, called just after the callData is executed.
* If a paymaster is defined and its validation returned a non-empty context, its postOp is called.
* The excess amount is refunded to the account (or paymaster - if it was used in the request).
* @param mode - Whether is called from innerHandleOp, or outside (postOpReverted).
* @param opInfo - UserOp fields and info collected during validation.
* @param context - The context returned in validatePaymasterUserOp.
* @param actualGas - The gas used so far by this user operation.
*/
function _postExecution(
IPaymaster.PostOpMode mode,
UserOpInfo memory opInfo,
bytes memory context,
uint256 actualGas
) private returns (uint256 actualGasCost) {
uint256 preGas = gasleft();
unchecked {
address refundAddress;
MemoryUserOp memory mUserOp = opInfo.mUserOp;
uint256 gasPrice = getUserOpGasPrice(mUserOp);
address paymaster = mUserOp.paymaster;
if (paymaster == address(0)) {
refundAddress = mUserOp.sender;
} else {
refundAddress = paymaster;
if (context.length > 0) {
actualGasCost = actualGas * gasPrice;
if (mode != IPaymaster.PostOpMode.postOpReverted) {
try IPaymaster(paymaster).postOp{
gas: mUserOp.paymasterPostOpGasLimit
}(mode, context, actualGasCost, gasPrice)
// solhint-disable-next-line no-empty-blocks
{} catch {
bytes memory reason = Exec.getReturnData(REVERT_REASON_MAX_LEN);
revert PostOpReverted(reason);
}
}
}
}
actualGas += preGas - gasleft();
// Calculating a penalty for unused execution gas
{
uint256 executionGasLimit = mUserOp.callGasLimit + mUserOp.paymasterPostOpGasLimit;
uint256 executionGasUsed = actualGas - opInfo.preOpGas;
// this check is required for the gas used within EntryPoint and not covered by explicit gas limits
if (executionGasLimit > executionGasUsed) {
uint256 unusedGas = executionGasLimit - executionGasUsed;
uint256 unusedGasPenalty = (unusedGas * PENALTY_PERCENT) / 100;
actualGas += unusedGasPenalty;
}
}
actualGasCost = actualGas * gasPrice;
uint256 prefund = opInfo.prefund;
if (prefund < actualGasCost) {
if (mode == IPaymaster.PostOpMode.postOpReverted) {
actualGasCost = prefund;
emitPrefundTooLow(opInfo);
emitUserOperationEvent(opInfo, false, actualGasCost, actualGas);
} else {
assembly ("memory-safe") {
mstore(0, INNER_REVERT_LOW_PREFUND)
revert(0, 32)
}
}
} else {
uint256 refund = prefund - actualGasCost;
_incrementDeposit(refundAddress, refund);
bool success = mode == IPaymaster.PostOpMode.opSucceeded;
emitUserOperationEvent(opInfo, success, actualGasCost, actualGas);
}
} // unchecked
}
/**
* The gas price this UserOp agrees to pay.
* Relayer/block builder might submit the TX with higher priorityFee, but the user should not.
* @param mUserOp - The userOp to get the gas price from.
*/
function getUserOpGasPrice(
MemoryUserOp memory mUserOp
) internal view returns (uint256) {
unchecked {
uint256 maxFeePerGas = mUserOp.maxFeePerGas;
uint256 maxPriorityFeePerGas = mUserOp.maxPriorityFeePerGas;
if (maxFeePerGas == maxPriorityFeePerGas) {
//legacy mode (for networks that don't support basefee opcode)
return maxFeePerGas;
}
return min(maxFeePerGas, maxPriorityFeePerGas + block.basefee);
}
}
/**
* The offset of the given bytes in memory.
* @param data - The bytes to get the offset of.
*/
function getOffsetOfMemoryBytes(
bytes memory data
) internal pure returns (uint256 offset) {
assembly {
offset := data
}
}
/**
* The bytes in memory at the given offset.
* @param offset - The offset to get the bytes from.
*/
function getMemoryBytesFromOffset(
uint256 offset
) internal pure returns (bytes memory data) {
assembly ("memory-safe") {
data := offset
}
}
/// @inheritdoc IEntryPoint
function delegateAndRevert(address target, bytes calldata data) external {
(bool success, bytes memory ret) = target.delegatecall(data);
revert DelegateAndRevert(success, ret);
}
}
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.23;
/* solhint-disable no-inline-assembly */
/*
* For simulation purposes, validateUserOp (and validatePaymasterUserOp)
* must return this value in case of signature failure, instead of revert.
*/
uint256 constant SIG_VALIDATION_FAILED = 1;
/*
* For simulation purposes, validateUserOp (and validatePaymasterUserOp)
* return this value on success.
*/
uint256 constant SIG_VALIDATION_SUCCESS = 0;
/**
* Returned data from validateUserOp.
* validateUserOp returns a uint256, which is created by `_packedValidationData` and
* parsed by `_parseValidationData`.
* @param aggregator - address(0) - The account validated the signature by itself.
* address(1) - The account failed to validate the signature.
* otherwise - This is an address of a signature aggregator that must
* be used to validate the signature.
* @param validAfter - This UserOp is valid only after this timestamp.
* @param validaUntil - This UserOp is valid only up to this timestamp.
*/
struct ValidationData {
address aggregator;
uint48 validAfter;
uint48 validUntil;
}
/**
* Extract sigFailed, validAfter, validUntil.
* Also convert zero validUntil to type(uint48).max.
* @param validationData - The packed validation data.
*/
function _parseValidationData(
uint256 validationData
) pure returns (ValidationData memory data) {
address aggregator = address(uint160(validationData));
uint48 validUntil = uint48(validationData >> 160);
if (validUntil == 0) {
validUntil = type(uint48).max;
}
uint48 validAfter = uint48(validationData >> (48 + 160));
return ValidationData(aggregator, validAfter, validUntil);
}
/**
* Helper to pack the return value for validateUserOp.
* @param data - The ValidationData to pack.
*/
function _packValidationData(
ValidationData memory data
) pure returns (uint256) {
return
uint160(data.aggregator) |
(uint256(data.validUntil) << 160) |
(uint256(data.validAfter) << (160 + 48));
}
/**
* Helper to pack the return value for validateUserOp, when not using an aggregator.
* @param sigFailed - True for signature failure, false for success.
* @param validUntil - Last timestamp this UserOperation is valid (or zero for infinite).
* @param validAfter - First timestamp this UserOperation is valid.
*/
function _packValidationData(
bool sigFailed,
uint48 validUntil,
uint48 validAfter
) pure returns (uint256) {
return
(sigFailed ? 1 : 0) |
(uint256(validUntil) << 160) |
(uint256(validAfter) << (160 + 48));
}
/**
* keccak function over calldata.
* @dev copy calldata into memory, do keccak and drop allocated memory. Strangely, this is more efficient than letting solidity do it.
*/
function calldataKeccak(bytes calldata data) pure returns (bytes32 ret) {
assembly ("memory-safe") {
let mem := mload(0x40)
let len := data.length
calldatacopy(mem, data.offset, len)
ret := keccak256(mem, len)
}
}
/**
* The minimum of two numbers.
* @param a - First number.
* @param b - Second number.
*/
function min(uint256 a, uint256 b) pure returns (uint256) {
return a < b ? a : b;
}
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.23;
import "../interfaces/INonceManager.sol";
/**
* nonce management functionality
*/
abstract contract NonceManager is INonceManager {
/**
* The next valid sequence number for a given nonce key.
*/
mapping(address => mapping(uint192 => uint256)) public nonceSequenceNumber;
/// @inheritdoc INonceManager
function getNonce(address sender, uint192 key)
public view override returns (uint256 nonce) {
return nonceSequenceNumber[sender][key] | (uint256(key) << 64);
}
// allow an account to manually increment its own nonce.
// (mainly so that during construction nonce can be made non-zero,
// to "absorb" the gas cost of first nonce increment to 1st transaction (construction),
// not to 2nd transaction)
function incrementNonce(uint192 key) public override {
nonceSequenceNumber[msg.sender][key]++;
}
/**
* validate nonce uniqueness for this account.
* called just after validateUserOp()
* @return true if the nonce was incremented successfully.
* false if the current nonce doesn't match the given one.
*/
function _validateAndUpdateNonce(address sender, uint256 nonce) internal returns (bool) {
uint192 key = uint192(nonce >> 64);
uint64 seq = uint64(nonce);
return nonceSequenceNumber[sender][key]++ == seq;
}
}
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.23;
/**
* Helper contract for EntryPoint, to call userOp.initCode from a "neutral" address,
* which is explicitly not the entryPoint itself.
*/
contract SenderCreator {
/**
* Call the "initCode" factory to create and return the sender account address.
* @param initCode - The initCode value from a UserOp. contains 20 bytes of factory address,
* followed by calldata.
* @return sender - The returned address of the created account, or zero address on failure.
*/
function createSender(
bytes calldata initCode
) external returns (address sender) {
address factory = address(bytes20(initCode[0:20]));
bytes memory initCallData = initCode[20:];
bool success;
/* solhint-disable no-inline-assembly */
assembly ("memory-safe") {
success := call(
gas(),
factory,
0,
add(initCallData, 0x20),
mload(initCallData),
0,
32
)
sender := mload(0)
}
if (!success) {
sender = address(0);
}
}
}
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.23;
import "../interfaces/IStakeManager.sol";
/* solhint-disable avoid-low-level-calls */
/* solhint-disable not-rely-on-time */
/**
* Manage deposits and stakes.
* Deposit is just a balance used to pay for UserOperations (either by a paymaster or an account).
* Stake is value locked for at least "unstakeDelay" by a paymaster.
*/
abstract contract StakeManager is IStakeManager {
/// maps paymaster to their deposits and stakes
mapping(address => DepositInfo) public deposits;
/// @inheritdoc IStakeManager
function getDepositInfo(
address account
) public view returns (DepositInfo memory info) {
return deposits[account];
}
/**
* Internal method to return just the stake info.
* @param addr - The account to query.
*/
function _getStakeInfo(
address addr
) internal view returns (StakeInfo memory info) {
DepositInfo storage depositInfo = deposits[addr];
info.stake = depositInfo.stake;
info.unstakeDelaySec = depositInfo.unstakeDelaySec;
}
/// @inheritdoc IStakeManager
function balanceOf(address account) public view returns (uint256) {
return deposits[account].deposit;
}
receive() external payable {
depositTo(msg.sender);
}
/**
* Increments an account's deposit.
* @param account - The account to increment.
* @param amount - The amount to increment by.
* @return the updated deposit of this account
*/
function _incrementDeposit(address account, uint256 amount) internal returns (uint256) {
DepositInfo storage info = deposits[account];
uint256 newAmount = info.deposit + amount;
info.deposit = newAmount;
return newAmount;
}
/**
* Add to the deposit of the given account.
* @param account - The account to add to.
*/
function depositTo(address account) public virtual payable {
uint256 newDeposit = _incrementDeposit(account, msg.value);
emit Deposited(account, newDeposit);
}
/**
* Add to the account's stake - amount and delay
* any pending unstake is first cancelled.
* @param unstakeDelaySec The new lock duration before the deposit can be withdrawn.
*/
function addStake(uint32 unstakeDelaySec) public payable {
DepositInfo storage info = deposits[msg.sender];
require(unstakeDelaySec > 0, "must specify unstake delay");
require(
unstakeDelaySec >= info.unstakeDelaySec,
"cannot decrease unstake time"
);
uint256 stake = info.stake + msg.value;
require(stake > 0, "no stake specified");
require(stake <= type(uint112).max, "stake overflow");
deposits[msg.sender] = DepositInfo(
info.deposit,
true,
uint112(stake),
unstakeDelaySec,
0
);
emit StakeLocked(msg.sender, stake, unstakeDelaySec);
}
/**
* Attempt to unlock the stake.
* The value can be withdrawn (using withdrawStake) after the unstake delay.
*/
function unlockStake() external {
DepositInfo storage info = deposits[msg.sender];
require(info.unstakeDelaySec != 0, "not staked");
require(info.staked, "already unstaking");
uint48 withdrawTime = uint48(block.timestamp) + info.unstakeDelaySec;
info.withdrawTime = withdrawTime;
info.staked = false;
emit StakeUnlocked(msg.sender, withdrawTime);
}
/**
* Withdraw from the (unlocked) stake.
* Must first call unlockStake and wait for the unstakeDelay to pass.
* @param withdrawAddress - The address to send withdrawn value.
*/
function withdrawStake(address payable withdrawAddress) external {
DepositInfo storage info = deposits[msg.sender];
uint256 stake = info.stake;
require(stake > 0, "No stake to withdraw");
require(info.withdrawTime > 0, "must call unlockStake() first");
require(
info.withdrawTime <= block.timestamp,
"Stake withdrawal is not due"
);
info.unstakeDelaySec = 0;
info.withdrawTime = 0;
info.stake = 0;
emit StakeWithdrawn(msg.sender, withdrawAddress, stake);
(bool success,) = withdrawAddress.call{value: stake}("");
require(success, "failed to withdraw stake");
}
/**
* Withdraw from the deposit.
* @param withdrawAddress - The address to send withdrawn value.
* @param withdrawAmount - The amount to withdraw.
*/
function withdrawTo(
address payable withdrawAddress,
uint256 withdrawAmount
) external {
DepositInfo storage info = deposits[msg.sender];
require(withdrawAmount <= info.deposit, "Withdraw amount too large");
info.deposit = info.deposit - withdrawAmount;
emit Withdrawn(msg.sender, withdrawAddress, withdrawAmount);
(bool success,) = withdrawAddress.call{value: withdrawAmount}("");
require(success, "failed to withdraw");
}
}
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.23;
/* solhint-disable no-inline-assembly */
import "../interfaces/PackedUserOperation.sol";
import {calldataKeccak, min} from "./Helpers.sol";
/**
* Utility functions helpful when working with UserOperation structs.
*/
library UserOperationLib {
uint256 public constant PAYMASTER_VALIDATION_GAS_OFFSET = 20;
uint256 public constant PAYMASTER_POSTOP_GAS_OFFSET = 36;
uint256 public constant PAYMASTER_DATA_OFFSET = 52;
/**
* Get sender from user operation data.
* @param userOp - The user operation data.
*/
function getSender(
PackedUserOperation calldata userOp
) internal pure returns (address) {
address data;
//read sender from userOp, which is first userOp member (saves 800 gas...)
assembly {
data := calldataload(userOp)
}
return address(uint160(data));
}
/**
* Relayer/block builder might submit the TX with higher priorityFee,
* but the user should not pay above what he signed for.
* @param userOp - The user operation data.
*/
function gasPrice(
PackedUserOperation calldata userOp
) internal view returns (uint256) {
unchecked {
(uint256 maxPriorityFeePerGas, uint256 maxFeePerGas) = unpackUints(userOp.gasFees);
if (maxFeePerGas == maxPriorityFeePerGas) {
//legacy mode (for networks that don't support basefee opcode)
return maxFeePerGas;
}
return min(maxFeePerGas, maxPriorityFeePerGas + block.basefee);
}
}
/**
* Pack the user operation data into bytes for hashing.
* @param userOp - The user operation data.
*/
function encode(
PackedUserOperation calldata userOp
) internal pure returns (bytes memory ret) {
address sender = getSender(userOp);
uint256 nonce = userOp.nonce;
bytes32 hashInitCode = calldataKeccak(userOp.initCode);
bytes32 hashCallData = calldataKeccak(userOp.callData);
bytes32 accountGasLimits = userOp.accountGasLimits;
uint256 preVerificationGas = userOp.preVerificationGas;
bytes32 gasFees = userOp.gasFees;
bytes32 hashPaymasterAndData = calldataKeccak(userOp.paymasterAndData);
return abi.encode(
sender, nonce,
hashInitCode, hashCallData,
accountGasLimits, preVerificationGas, gasFees,
hashPaymasterAndData
);
}
function unpackUints(
bytes32 packed
) internal pure returns (uint256 high128, uint256 low128) {
return (uint128(bytes16(packed)), uint128(uint256(packed)));
}
//unpack just the high 128-bits from a packed value
function unpackHigh128(bytes32 packed) internal pure returns (uint256) {
return uint256(packed) >> 128;
}
// unpack just the low 128-bits from a packed value
function unpackLow128(bytes32 packed) internal pure returns (uint256) {
return uint128(uint256(packed));
}
function unpackMaxPriorityFeePerGas(PackedUserOperation calldata userOp)
internal pure returns (uint256) {
return unpackHigh128(userOp.gasFees);
}
function unpackMaxFeePerGas(PackedUserOperation calldata userOp)
internal pure returns (uint256) {
return unpackLow128(userOp.gasFees);
}
function unpackVerificationGasLimit(PackedUserOperation calldata userOp)
internal pure returns (uint256) {
return unpackHigh128(userOp.accountGasLimits);
}
function unpackCallGasLimit(PackedUserOperation calldata userOp)
internal pure returns (uint256) {
return unpackLow128(userOp.accountGasLimits);
}
function unpackPaymasterVerificationGasLimit(PackedUserOperation calldata userOp)
internal pure returns (uint256) {
return uint128(bytes16(userOp.paymasterAndData[PAYMASTER_VALIDATION_GAS_OFFSET : PAYMASTER_POSTOP_GAS_OFFSET]));
}
function unpackPostOpGasLimit(PackedUserOperation calldata userOp)
internal pure returns (uint256) {
return uint128(bytes16(userOp.paymasterAndData[PAYMASTER_POSTOP_GAS_OFFSET : PAYMASTER_DATA_OFFSET]));
}
function unpackPaymasterStaticFields(
bytes calldata paymasterAndData
) internal pure returns (address paymaster, uint256 validationGasLimit, uint256 postOpGasLimit) {
return (
address(bytes20(paymasterAndData[: PAYMASTER_VALIDATION_GAS_OFFSET])),
uint128(bytes16(paymasterAndData[PAYMASTER_VALIDATION_GAS_OFFSET : PAYMASTER_POSTOP_GAS_OFFSET])),
uint128(bytes16(paymasterAndData[PAYMASTER_POSTOP_GAS_OFFSET : PAYMASTER_DATA_OFFSET]))
);
}
/**
* Hash the user operation data.
* @param userOp - The user operation data.
*/
function hash(
PackedUserOperation calldata userOp
) internal pure returns (bytes32) {
return keccak256(encode(userOp));
}
}
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.5;
import "./PackedUserOperation.sol";
interface IAccount {
/**
* Validate user's signature and nonce
* the entryPoint will make the call to the recipient only if this validation call returns successfully.
* signature failure should be reported by returning SIG_VALIDATION_FAILED (1).
* This allows making a "simulation call" without a valid signature
* Other failures (e.g. nonce mismatch, or invalid signature format) should still revert to signal failure.
*
* @dev Must validate caller is the entryPoint.
* Must validate the signature and nonce
* @param userOp - The operation that is about to be executed.
* @param userOpHash - Hash of the user's request data. can be used as the basis for signature.
* @param missingAccountFunds - Missing funds on the account's deposit in the entrypoint.
* This is the minimum amount to transfer to the sender(entryPoint) to be
* able to make the call. The excess is left as a deposit in the entrypoint
* for future calls. Can be withdrawn anytime using "entryPoint.withdrawTo()".
* In case there is a paymaster in the request (or the current deposit is high
* enough), this value will be zero.
* @return validationData - Packaged ValidationData structure. use `_packValidationData` and
* `_unpackValidationData` to encode and decode.
* <20-byte> sigAuthorizer - 0 for valid signature, 1 to mark signature failure,
* otherwise, an address of an "authorizer" contract.
* <6-byte> validUntil - Last timestamp this operation is valid. 0 for "indefinite"
* <6-byte> validAfter - First timestamp this operation is valid
* If an account doesn't use time-range, it is enough to
* return SIG_VALIDATION_FAILED value (1) for signature failure.
* Note that the validation code cannot use block.timestamp (or block.number) directly.
*/
function validateUserOp(
PackedUserOperation calldata userOp,
bytes32 userOpHash,
uint256 missingAccountFunds
) external returns (uint256 validationData);
}
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.5;
import "./PackedUserOperation.sol";
interface IAccountExecute {
/**
* Account may implement this execute method.
* passing this methodSig at the beginning of callData will cause the entryPoint to pass the full UserOp (and hash)
* to the account.
* The account should skip the methodSig, and use the callData (and optionally, other UserOp fields)
*
* @param userOp - The operation that was just validated.
* @param userOpHash - Hash of the user's request data.
*/
function executeUserOp(
PackedUserOperation calldata userOp,
bytes32 userOpHash
) external;
}
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.5;
import "./PackedUserOperation.sol";
/**
* Aggregated Signatures validator.
*/
interface IAggregator {
/**
* Validate aggregated signature.
* Revert if the aggregated signature does not match the given list of operations.
* @param userOps - Array of UserOperations to validate the signature for.
* @param signature - The aggregated signature.
*/
function validateSignatures(
PackedUserOperation[] calldata userOps,
bytes calldata signature
) external view;
/**
* Validate signature of a single userOp.
* This method should be called by bundler after EntryPointSimulation.simulateValidation() returns
* the aggregator this account uses.
* First it validates the signature over the userOp. Then it returns data to be used when creating the handleOps.
* @param userOp - The userOperation received from the user.
* @return sigForUserOp - The value to put into the signature field of the userOp when calling handleOps.
* (usually empty, unless account and aggregator support some kind of "multisig".
*/
function validateUserOpSignature(
PackedUserOperation calldata userOp
) external view returns (bytes memory sigForUserOp);
/**
* Aggregate multiple signatures into a single value.
* This method is called off-chain to calculate the signature to pass with handleOps()
* bundler MAY use optimized custom code perform this aggregation.
* @param userOps - Array of UserOperations to collect the signatures from.
* @return aggregatedSignature - The aggregated signature.
*/
function aggregateSignatures(
PackedUserOperation[] calldata userOps
) external view returns (bytes memory aggregatedSignature);
}
/**
** Account-Abstraction (EIP-4337) singleton EntryPoint implementation.
** Only one instance required on each chain.
**/
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.5;
/* solhint-disable avoid-low-level-calls */
/* solhint-disable no-inline-assembly */
/* solhint-disable reason-string */
import "./PackedUserOperation.sol";
import "./IStakeManager.sol";
import "./IAggregator.sol";
import "./INonceManager.sol";
interface IEntryPoint is IStakeManager, INonceManager {
/***
* An event emitted after each successful request.
* @param userOpHash - Unique identifier for the request (hash its entire content, except signature).
* @param sender - The account that generates this request.
* @param paymaster - If non-null, the paymaster that pays for this request.
* @param nonce - The nonce value from the request.
* @param success - True if the sender transaction succeeded, false if reverted.
* @param actualGasCost - Actual amount paid (by account or paymaster) for this UserOperation.
* @param actualGasUsed - Total gas used by this UserOperation (including preVerification, creation,
* validation and execution).
*/
event UserOperationEvent(
bytes32 indexed userOpHash,
address indexed sender,
address indexed paymaster,
uint256 nonce,
bool success,
uint256 actualGasCost,
uint256 actualGasUsed
);
/**
* Account "sender" was deployed.
* @param userOpHash - The userOp that deployed this account. UserOperationEvent will follow.
* @param sender - The account that is deployed
* @param factory - The factory used to deploy this account (in the initCode)
* @param paymaster - The paymaster used by this UserOp
*/
event AccountDeployed(
bytes32 indexed userOpHash,
address indexed sender,
address factory,
address paymaster
);
/**
* An event emitted if the UserOperation "callData" reverted with non-zero length.
* @param userOpHash - The request unique identifier.
* @param sender - The sender of this request.
* @param nonce - The nonce used in the request.
* @param revertReason - The return bytes from the (reverted) call to "callData".
*/
event UserOperationRevertReason(
bytes32 indexed userOpHash,
address indexed sender,
uint256 nonce,
bytes revertReason
);
/**
* An event emitted if the UserOperation Paymaster's "postOp" call reverted with non-zero length.
* @param userOpHash - The request unique identifier.
* @param sender - The sender of this request.
* @param nonce - The nonce used in the request.
* @param revertReason - The return bytes from the (reverted) call to "callData".
*/
event PostOpRevertReason(
bytes32 indexed userOpHash,
address indexed sender,
uint256 nonce,
bytes revertReason
);
/**
* UserOp consumed more than prefund. The UserOperation is reverted, and no refund is made.
* @param userOpHash - The request unique identifier.
* @param sender - The sender of this request.
* @param nonce - The nonce used in the request.
*/
event UserOperationPrefundTooLow(
bytes32 indexed userOpHash,
address indexed sender,
uint256 nonce
);
/**
* An event emitted by handleOps(), before starting the execution loop.
* Any event emitted before this event, is part of the validation.
*/
event BeforeExecution();
/**
* Signature aggregator used by the following UserOperationEvents within this bundle.
* @param aggregator - The aggregator used for the following UserOperationEvents.
*/
event SignatureAggregatorChanged(address indexed aggregator);
/**
* A custom revert error of handleOps, to identify the offending op.
* Should be caught in off-chain handleOps simulation and not happen on-chain.
* Useful for mitigating DoS attempts against batchers or for troubleshooting of factory/account/paymaster reverts.
* NOTE: If simulateValidation passes successfully, there should be no reason for handleOps to fail on it.
* @param opIndex - Index into the array of ops to the failed one (in simulateValidation, this is always zero).
* @param reason - Revert reason. The string starts with a unique code "AAmn",
* where "m" is "1" for factory, "2" for account and "3" for paymaster issues,
* so a failure can be attributed to the correct entity.
*/
error FailedOp(uint256 opIndex, string reason);
/**
* A custom revert error of handleOps, to report a revert by account or paymaster.
* @param opIndex - Index into the array of ops to the failed one (in simulateValidation, this is always zero).
* @param reason - Revert reason. see FailedOp(uint256,string), above
* @param inner - data from inner cought revert reason
* @dev note that inner is truncated to 2048 bytes
*/
error FailedOpWithRevert(uint256 opIndex, string reason, bytes inner);
error PostOpReverted(bytes returnData);
/**
* Error case when a signature aggregator fails to verify the aggregated signature it had created.
* @param aggregator The aggregator that failed to verify the signature
*/
error SignatureValidationFailed(address aggregator);
// Return value of getSenderAddress.
error SenderAddressResult(address sender);
// UserOps handled, per aggregator.
struct UserOpsPerAggregator {
PackedUserOperation[] userOps;
// Aggregator address
IAggregator aggregator;
// Aggregated signature
bytes signature;
}
/**
* Execute a batch of UserOperations.
* No signature aggregator is used.
* If any account requires an aggregator (that is, it returned an aggregator when
* performing simulateValidation), then handleAggregatedOps() must be used instead.
* @param ops - The operations to execute.
* @param beneficiary - The address to receive the fees.
*/
function handleOps(
PackedUserOperation[] calldata ops,
address payable beneficiary
) external;
/**
* Execute a batch of UserOperation with Aggregators
* @param opsPerAggregator - The operations to execute, grouped by aggregator (or address(0) for no-aggregator accounts).
* @param beneficiary - The address to receive the fees.
*/
function handleAggregatedOps(
UserOpsPerAggregator[] calldata opsPerAggregator,
address payable beneficiary
) external;
/**
* Generate a request Id - unique identifier for this request.
* The request ID is a hash over the content of the userOp (except the signature), the entrypoint and the chainid.
* @param userOp - The user operation to generate the request ID for.
* @return hash the hash of this UserOperation
*/
function getUserOpHash(
PackedUserOperation calldata userOp
) external view returns (bytes32);
/**
* Gas and return values during simulation.
* @param preOpGas - The gas used for validation (including preValidationGas)
* @param prefund - The required prefund for this operation
* @param accountValidationData - returned validationData from account.
* @param paymasterValidationData - return validationData from paymaster.
* @param paymasterContext - Returned by validatePaymasterUserOp (to be passed into postOp)
*/
struct ReturnInfo {
uint256 preOpGas;
uint256 prefund;
uint256 accountValidationData;
uint256 paymasterValidationData;
bytes paymasterContext;
}
/**
* Returned aggregated signature info:
* The aggregator returned by the account, and its current stake.
*/
struct AggregatorStakeInfo {
address aggregator;
StakeInfo stakeInfo;
}
/**
* Get counterfactual sender address.
* Calculate the sender contract address that will be generated by the initCode and salt in the UserOperation.
* This method always revert, and returns the address in SenderAddressResult error
* @param initCode - The constructor code to be passed into the UserOperation.
*/
function getSenderAddress(bytes memory initCode) external;
error DelegateAndRevert(bool success, bytes ret);
/**
* Helper method for dry-run testing.
* @dev calling this method, the EntryPoint will make a delegatecall to the given data, and report (via revert) the result.
* The method always revert, so is only useful off-chain for dry run calls, in cases where state-override to replace
* actual EntryPoint code is less convenient.
* @param target a target contract to make a delegatecall from entrypoint
* @param data data to pass to target in a delegatecall
*/
function delegateAndRevert(address target, bytes calldata data) external;
}
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.5;
interface INonceManager {
/**
* Return the next nonce for this sender.
* Within a given key, the nonce values are sequenced (starting with zero, and incremented by one on each userop)
* But UserOp with different keys can come with arbitrary order.
*
* @param sender the account address
* @param key the high 192 bit of the nonce
* @return nonce a full nonce to pass for next UserOp with this sender.
*/
function getNonce(address sender, uint192 key)
external view returns (uint256 nonce);
/**
* Manually increment the nonce of the sender.
* This method is exposed just for completeness..
* Account does NOT need to call it, neither during validation, nor elsewhere,
* as the EntryPoint will update the nonce regardless.
* Possible use-case is call it with various keys to "initialize" their nonces to one, so that future
* UserOperations will not pay extra for the first transaction with a given key.
*/
function incrementNonce(uint192 key) external;
}
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.5;
import "./PackedUserOperation.sol";
/**
* The interface exposed by a paymaster contract, who agrees to pay the gas for user's operations.
* A paymaster must hold a stake to cover the required entrypoint stake and also the gas for the transaction.
*/
interface IPaymaster {
enum PostOpMode {
// User op succeeded.
opSucceeded,
// User op reverted. Still has to pay for gas.
opReverted,
// Only used internally in the EntryPoint (cleanup after postOp reverts). Never calling paymaster with this value
postOpReverted
}
/**
* Payment validation: check if paymaster agrees to pay.
* Must verify sender is the entryPoint.
* Revert to reject this request.
* Note that bundlers will reject this method if it changes the state, unless the paymaster is trusted (whitelisted).
* The paymaster pre-pays using its deposit, and receive back a refund after the postOp method returns.
* @param userOp - The user operation.
* @param userOpHash - Hash of the user's request data.
* @param maxCost - The maximum cost of this transaction (based on maximum gas and gas price from userOp).
* @return context - Value to send to a postOp. Zero length to signify postOp is not required.
* @return validationData - Signature and time-range of this operation, encoded the same as the return
* value of validateUserOperation.
* <20-byte> sigAuthorizer - 0 for valid signature, 1 to mark signature failure,
* other values are invalid for paymaster.
* <6-byte> validUntil - last timestamp this operation is valid. 0 for "indefinite"
* <6-byte> validAfter - first timestamp this operation is valid
* Note that the validation code cannot use block.timestamp (or block.number) directly.
*/
function validatePaymasterUserOp(
PackedUserOperation calldata userOp,
bytes32 userOpHash,
uint256 maxCost
) external returns (bytes memory context, uint256 validationData);
/**
* Post-operation handler.
* Must verify sender is the entryPoint.
* @param mode - Enum with the following options:
* opSucceeded - User operation succeeded.
* opReverted - User op reverted. The paymaster still has to pay for gas.
* postOpReverted - never passed in a call to postOp().
* @param context - The context value returned by validatePaymasterUserOp
* @param actualGasCost - Actual gas used so far (without this postOp call).
* @param actualUserOpFeePerGas - the gas price this UserOp pays. This value is based on the UserOp's maxFeePerGas
* and maxPriorityFee (and basefee)
* It is not the same as tx.gasprice, which is what the bundler pays.
*/
function postOp(
PostOpMode mode,
bytes calldata context,
uint256 actualGasCost,
uint256 actualUserOpFeePerGas
) external;
}
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.7.5;
/**
* Manage deposits and stakes.
* Deposit is just a balance used to pay for UserOperations (either by a paymaster or an account).
* Stake is value locked for at least "unstakeDelay" by the staked entity.
*/
interface IStakeManager {
event Deposited(address indexed account, uint256 totalDeposit);
event Withdrawn(
address indexed account,
address withdrawAddress,
uint256 amount
);
// Emitted when stake or unstake delay are modified.
event StakeLocked(
address indexed account,
uint256 totalStaked,
uint256 unstakeDelaySec
);
// Emitted once a stake is scheduled for withdrawal.
event StakeUnlocked(address indexed account, uint256 withdrawTime);
event StakeWithdrawn(
address indexed account,
address withdrawAddress,
uint256 amount
);
/**
* @param deposit - The entity's deposit.
* @param staked - True if this entity is staked.
* @param stake - Actual amount of ether staked for this entity.
* @param unstakeDelaySec - Minimum delay to withdraw the stake.
* @param withdrawTime - First block timestamp where 'withdrawStake' will be callable, or zero if already locked.
* @dev Sizes were chosen so that deposit fits into one cell (used during handleOp)
* and the rest fit into a 2nd cell (used during stake/unstake)
* - 112 bit allows for 10^15 eth
* - 48 bit for full timestamp
* - 32 bit allows 150 years for unstake delay
*/
struct DepositInfo {
uint256 deposit;
bool staked;
uint112 stake;
uint32 unstakeDelaySec;
uint48 withdrawTime;
}
// API struct used by getStakeInfo and simulateValidation.
struct StakeInfo {
uint256 stake;
uint256 unstakeDelaySec;
}
/**
* Get deposit info.
* @param account - The account to query.
* @return info - Full deposit information of given account.
*/
function getDepositInfo(
address account
) external view returns (DepositInfo memory info);
/**
* Get account balance.
* @param account - The account to query.
* @return - The deposit (for gas payment) of the account.
*/
function balanceOf(address account) external view returns (uint256);
/**
* Add to the deposit of the given account.
* @param account - The account to add to.
*/
function depositTo(address account) external payable;
/**
* Add to the account's stake - amount and delay
* any pending unstake is first cancelled.
* @param _unstakeDelaySec - The new lock duration before the deposit can be withdrawn.
*/
function addStake(uint32 _unstakeDelaySec) external payable;
/**
* Attempt to unlock the stake.
* The value can be withdrawn (using withdrawStake) after the unstake delay.
*/
function unlockStake() external;
/**
* Withdraw from the (unlocked) stake.
* Must first call unlockStake and wait for the unstakeDelay to pass.
* @param withdrawAddress - The address to send withdrawn value.
*/
function withdrawStake(address payable withdrawAddress) external;
/**
* Withdraw from the deposit.
* @param withdrawAddress - The address to send withdrawn value.
* @param withdrawAmount - The amount to withdraw.
*/
function withdrawTo(
address payable withdrawAddress,
uint256 withdrawAmount
) external;
}
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.5;
/**
* User Operation struct
* @param sender - The sender account of this request.
* @param nonce - Unique value the sender uses to verify it is not a replay.
* @param initCode - If set, the account contract will be created by this constructor/
* @param callData - The method call to execute on this account.
* @param accountGasLimits - Packed gas limits for validateUserOp and gas limit passed to the callData method call.
* @param preVerificationGas - Gas not calculated by the handleOps method, but added to the gas paid.
* Covers batch overhead.
* @param gasFees - packed gas fields maxPriorityFeePerGas and maxFeePerGas - Same as EIP-1559 gas parameters.
* @param paymasterAndData - If set, this field holds the paymaster address, verification gas limit, postOp gas limit and paymaster-specific extra data
* The paymaster will pay for the transaction instead of the sender.
* @param signature - Sender-verified signature over the entire request, the EntryPoint address and the chain ID.
*/
struct PackedUserOperation {
address sender;
uint256 nonce;
bytes initCode;
bytes callData;
bytes32 accountGasLimits;
uint256 preVerificationGas;
bytes32 gasFees;
bytes paymasterAndData;
bytes signature;
}
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.23;
// solhint-disable no-inline-assembly
/**
* Utility functions helpful when making different kinds of contract calls in Solidity.
*/
library Exec {
function call(
address to,
uint256 value,
bytes memory data,
uint256 txGas
) internal returns (bool success) {
assembly ("memory-safe") {
success := call(txGas, to, value, add(data, 0x20), mload(data), 0, 0)
}
}
function staticcall(
address to,
bytes memory data,
uint256 txGas
) internal view returns (bool success) {
assembly ("memory-safe") {
success := staticcall(txGas, to, add(data, 0x20), mload(data), 0, 0)
}
}
function delegateCall(
address to,
bytes memory data,
uint256 txGas
) internal returns (bool success) {
assembly ("memory-safe") {
success := delegatecall(txGas, to, add(data, 0x20), mload(data), 0, 0)
}
}
// get returned data from last call or calldelegate
function getReturnData(uint256 maxLen) internal pure returns (bytes memory returnData) {
assembly ("memory-safe") {
let len := returndatasize()
if gt(len, maxLen) {
len := maxLen
}
let ptr := mload(0x40)
mstore(0x40, add(ptr, add(len, 0x20)))
mstore(ptr, len)
returndatacopy(add(ptr, 0x20), 0, len)
returnData := ptr
}
}
// revert with explicit byte array (probably reverted info from call)
function revertWithData(bytes memory returnData) internal pure {
assembly ("memory-safe") {
revert(add(returnData, 32), mload(returnData))
}
}
function callAndRevert(address to, bytes memory data, uint256 maxLen) internal {
bool success = call(to,0,data,gasleft());
if (!success) {
revertWithData(getReturnData(maxLen));
}
}
}
File 2 of 3: Ondo
// SPDX-License-Identifier: AGPL-3.0
pragma solidity 0.8.3;
/*
* @dev Provides information about the current execution context, including the
* sender of the transaction and its data. While these are generally available
* via msg.sender and msg.data, they should not be accessed in such a direct
* manner, since when dealing with meta-transactions the account sending and
* paying for execution may not be the actual sender (as far as an application
* is concerned).
*
* This contract is only required for intermediate, library-like contracts.
*/
abstract contract Context {
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}
function _msgData() internal view virtual returns (bytes calldata) {
this; // silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691
return msg.data;
}
}
/**
* @dev Interface of the ERC165 standard, as defined in the
* https://eips.ethereum.org/EIPS/eip-165[EIP].
*
* Implementers can declare support of contract interfaces, which can then be
* queried by others ({ERC165Checker}).
*
* For an implementation, see {ERC165}.
*/
interface IERC165 {
/**
* @dev Returns true if this contract implements the interface defined by
* `interfaceId`. See the corresponding
* https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
* to learn more about how these ids are created.
*
* This function call must use less than 30 000 gas.
*/
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}
/**
* @dev Implementation of the {IERC165} interface.
*
* Contracts that want to implement ERC165 should inherit from this contract and override {supportsInterface} to check
* for the additional interface id that will be supported. For example:
*
* ```solidity
* function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
* return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId);
* }
* ```
*
* Alternatively, {ERC165Storage} provides an easier to use but more expensive implementation.
*/
abstract contract ERC165 is IERC165 {
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId)
public
view
virtual
override
returns (bool)
{
return interfaceId == type(IERC165).interfaceId;
}
}
/**
* @dev External interface of AccessControl declared to support ERC165 detection.
*/
interface IAccessControl {
function hasRole(bytes32 role, address account) external view returns (bool);
function getRoleAdmin(bytes32 role) external view returns (bytes32);
function grantRole(bytes32 role, address account) external;
function revokeRole(bytes32 role, address account) external;
function renounceRole(bytes32 role, address account) external;
}
/**
* @dev Contract module that allows children to implement role-based access
* control mechanisms. This is a lightweight version that doesn't allow enumerating role
* members except through off-chain means by accessing the contract event logs. Some
* applications may benefit from on-chain enumerability, for those cases see
* {AccessControlEnumerable}.
*
* Roles are referred to by their `bytes32` identifier. These should be exposed
* in the external API and be unique. The best way to achieve this is by
* using `public constant` hash digests:
*
* ```
* bytes32 public constant MY_ROLE = keccak256("MY_ROLE");
* ```
*
* Roles can be used to represent a set of permissions. To restrict access to a
* function call, use {hasRole}:
*
* ```
* function foo() public {
* require(hasRole(MY_ROLE, msg.sender));
* ...
* }
* ```
*
* Roles can be granted and revoked dynamically via the {grantRole} and
* {revokeRole} functions. Each role has an associated admin role, and only
* accounts that have a role's admin role can call {grantRole} and {revokeRole}.
*
* By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means
* that only accounts with this role will be able to grant or revoke other
* roles. More complex role relationships can be created by using
* {_setRoleAdmin}.
*
* WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to
* grant and revoke this role. Extra precautions should be taken to secure
* accounts that have been granted it.
*/
abstract contract AccessControl is Context, IAccessControl, ERC165 {
struct RoleData {
mapping(address => bool) members;
bytes32 adminRole;
}
mapping(bytes32 => RoleData) private _roles;
bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;
/**
* @dev Emitted when `newAdminRole` is set as ``role``'s admin role, replacing `previousAdminRole`
*
* `DEFAULT_ADMIN_ROLE` is the starting admin for all roles, despite
* {RoleAdminChanged} not being emitted signaling this.
*
* _Available since v3.1._
*/
event RoleAdminChanged(
bytes32 indexed role,
bytes32 indexed previousAdminRole,
bytes32 indexed newAdminRole
);
/**
* @dev Emitted when `account` is granted `role`.
*
* `sender` is the account that originated the contract call, an admin role
* bearer except when using {_setupRole}.
*/
event RoleGranted(
bytes32 indexed role,
address indexed account,
address indexed sender
);
/**
* @dev Emitted when `account` is revoked `role`.
*
* `sender` is the account that originated the contract call:
* - if using `revokeRole`, it is the admin role bearer
* - if using `renounceRole`, it is the role bearer (i.e. `account`)
*/
event RoleRevoked(
bytes32 indexed role,
address indexed account,
address indexed sender
);
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId)
public
view
virtual
override
returns (bool)
{
return
interfaceId == type(IAccessControl).interfaceId ||
super.supportsInterface(interfaceId);
}
/**
* @dev Returns `true` if `account` has been granted `role`.
*/
function hasRole(bytes32 role, address account)
public
view
override
returns (bool)
{
return _roles[role].members[account];
}
/**
* @dev Returns the admin role that controls `role`. See {grantRole} and
* {revokeRole}.
*
* To change a role's admin, use {_setRoleAdmin}.
*/
function getRoleAdmin(bytes32 role) public view override returns (bytes32) {
return _roles[role].adminRole;
}
/**
* @dev Grants `role` to `account`.
*
* If `account` had not been already granted `role`, emits a {RoleGranted}
* event.
*
* Requirements:
*
* - the caller must have ``role``'s admin role.
*/
function grantRole(bytes32 role, address account) public virtual override {
require(
hasRole(getRoleAdmin(role), _msgSender()),
"AccessControl: sender must be an admin to grant"
);
_grantRole(role, account);
}
/**
* @dev Revokes `role` from `account`.
*
* If `account` had been granted `role`, emits a {RoleRevoked} event.
*
* Requirements:
*
* - the caller must have ``role``'s admin role.
*/
function revokeRole(bytes32 role, address account) public virtual override {
require(
hasRole(getRoleAdmin(role), _msgSender()),
"AccessControl: sender must be an admin to revoke"
);
_revokeRole(role, account);
}
/**
* @dev Revokes `role` from the calling account.
*
* Roles are often managed via {grantRole} and {revokeRole}: this function's
* purpose is to provide a mechanism for accounts to lose their privileges
* if they are compromised (such as when a trusted device is misplaced).
*
* If the calling account had been granted `role`, emits a {RoleRevoked}
* event.
*
* Requirements:
*
* - the caller must be `account`.
*/
function renounceRole(bytes32 role, address account) public virtual override {
require(
account == _msgSender(),
"AccessControl: can only renounce roles for self"
);
_revokeRole(role, account);
}
/**
* @dev Grants `role` to `account`.
*
* If `account` had not been already granted `role`, emits a {RoleGranted}
* event. Note that unlike {grantRole}, this function doesn't perform any
* checks on the calling account.
*
* [WARNING]
* ====
* This function should only be called from the constructor when setting
* up the initial roles for the system.
*
* Using this function in any other way is effectively circumventing the admin
* system imposed by {AccessControl}.
* ====
*/
function _setupRole(bytes32 role, address account) internal virtual {
_grantRole(role, account);
}
/**
* @dev Sets `adminRole` as ``role``'s admin role.
*
* Emits a {RoleAdminChanged} event.
*/
function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual {
emit RoleAdminChanged(role, getRoleAdmin(role), adminRole);
_roles[role].adminRole = adminRole;
}
function _grantRole(bytes32 role, address account) private {
if (!hasRole(role, account)) {
_roles[role].members[account] = true;
emit RoleGranted(role, account, _msgSender());
}
}
function _revokeRole(bytes32 role, address account) private {
if (hasRole(role, account)) {
_roles[role].members[account] = false;
emit RoleRevoked(role, account, _msgSender());
}
}
}
interface IOndo {
enum InvestorType {CoinlistTranche1, CoinlistTranche2, SeedTranche}
// ----------- State changing api -----------
/// @notice Called by timelock contract to initialize locked balance of coinlist/seed investor
function updateTrancheBalance(
address beneficiary,
uint256 rawAmount,
InvestorType tranche
) external;
// ----------- Getters -----------
/// @notice Gets the TOTAL amount of Ondo available for an address
function getFreedBalance(address account) external view returns (uint96);
/// @notice Gets the initial locked balance and unlocked Ondo for an address
function getVestedBalance(address account)
external
view
returns (uint96, uint96);
}
abstract contract LinearTimelock {
struct InvestorParam {
IOndo.InvestorType investorType;
uint96 initialBalance;
}
/// @notice the timestamp at which releasing is allowed
uint256 public cliffTimestamp;
/// @notice the linear vesting period for the first tranche
uint256 public immutable tranche1VestingPeriod;
/// @notice the linear vesting period for the second tranche
uint256 public immutable tranche2VestingPeriod;
/// @notice the linear vesting period for the Seed/Series A Tranche
uint256 public immutable seedVestingPeriod;
/// @dev mapping of balances for each investor
mapping(address => InvestorParam) internal investorBalances;
/// @notice role that allows updating of tranche balances - granted to Merkle airdrop contract
bytes32 public constant TIMELOCK_UPDATE_ROLE =
keccak256("TIMELOCK_UPDATE_ROLE");
constructor(
uint256 _cliffTimestamp,
uint256 _tranche1VestingPeriod,
uint256 _tranche2VestingPeriod,
uint256 _seedVestingPeriod
) {
cliffTimestamp = _cliffTimestamp;
tranche1VestingPeriod = _tranche1VestingPeriod;
tranche2VestingPeriod = _tranche2VestingPeriod;
seedVestingPeriod = _seedVestingPeriod;
}
function passedCliff() public view returns (bool) {
return block.timestamp > cliffTimestamp;
}
/// @dev the seedVestingPeriod is the longest vesting period
function passedAllVestingPeriods() public view returns (bool) {
return block.timestamp > cliffTimestamp + seedVestingPeriod;
}
/**
@notice View function to get the user's initial balance and current amount of freed balance
*/
function getVestedBalance(address account)
external
view
returns (uint256, uint256)
{
if (investorBalances[account].initialBalance == 0) {
return (0, 0);
}
InvestorParam memory investorParam = investorBalances[account];
uint96 amountAvailable;
if (passedAllVestingPeriods()) {
amountAvailable = investorParam.initialBalance;
} else if (passedCliff()) {
(uint256 vestingPeriod, uint256 elapsed) =
_getTrancheInfo(investorParam.investorType);
amountAvailable = _proportionAvailable(
elapsed,
vestingPeriod,
investorParam
);
} else {
amountAvailable = 0;
}
return (investorParam.initialBalance, amountAvailable);
}
function _getTrancheInfo(IOndo.InvestorType investorType)
internal
view
returns (uint256 vestingPeriod, uint256 elapsed)
{
elapsed = block.timestamp - cliffTimestamp;
if (investorType == IOndo.InvestorType.CoinlistTranche1) {
elapsed = elapsed > tranche1VestingPeriod
? tranche1VestingPeriod
: elapsed;
vestingPeriod = tranche1VestingPeriod;
} else if (investorType == IOndo.InvestorType.CoinlistTranche2) {
elapsed = elapsed > tranche2VestingPeriod
? tranche2VestingPeriod
: elapsed;
vestingPeriod = tranche2VestingPeriod;
} else if (investorType == IOndo.InvestorType.SeedTranche) {
elapsed = elapsed > seedVestingPeriod ? seedVestingPeriod : elapsed;
vestingPeriod = seedVestingPeriod;
}
}
function _proportionAvailable(
uint256 elapsed,
uint256 vestingPeriod,
InvestorParam memory investorParam
) internal pure returns (uint96) {
if (investorParam.investorType == IOndo.InvestorType.SeedTranche) {
// Seed/Series A Tranche Balance = proportionAvail*2/3 + x/3, where x = Balance. This allows 1/3 of the series A balance to be unlocked at cliff
uint96 vestedAmount =
safe96(
(((investorParam.initialBalance * elapsed) / vestingPeriod) * 2) / 3,
"Ondo::_proportionAvailable: amount exceeds 96 bits"
);
return
add96(
vestedAmount,
investorParam.initialBalance / 3,
"Ondo::_proportionAvailable: overflow"
);
} else {
return
safe96(
(investorParam.initialBalance * elapsed) / vestingPeriod,
"Ondo::_proportionAvailable: amount exceeds 96 bits"
);
}
}
function safe32(uint256 n, string memory errorMessage)
internal
pure
returns (uint32)
{
require(n < 2**32, errorMessage);
return uint32(n);
}
function safe96(uint256 n, string memory errorMessage)
internal
pure
returns (uint96)
{
require(n < 2**96, errorMessage);
return uint96(n);
}
function add96(
uint96 a,
uint96 b,
string memory errorMessage
) internal pure returns (uint96) {
uint96 c = a + b;
require(c >= a, errorMessage);
return c;
}
function sub96(
uint96 a,
uint96 b,
string memory errorMessage
) internal pure returns (uint96) {
require(b <= a, errorMessage);
return a - b;
}
}
contract Ondo is AccessControl, LinearTimelock {
/// @notice EIP-20 token name for this token
string public constant name = "Ondo";
/// @notice EIP-20 token symbol for this token
string public constant symbol = "ONDO";
/// @notice EIP-20 token decimals for this token
uint8 public constant decimals = 18;
// whether token transfers are allowed
bool public transferAllowed; // false by default
/// @notice Total number of tokens in circulation
uint256 public totalSupply = 10_000_000_000e18; // 10 billion Ondo
// Allowance amounts on behalf of others
mapping(address => mapping(address => uint96)) internal allowances;
// Official record of token balances for each account
mapping(address => uint96) internal balances;
/// @notice A record of each accounts delegate
mapping(address => address) public delegates;
/// @notice A checkpoint for marking number of votes from a given block
struct Checkpoint {
uint32 fromBlock;
uint96 votes;
}
/// @notice A record of votes checkpoints for each account, by index
mapping(address => mapping(uint32 => Checkpoint)) public checkpoints;
/// @notice The number of checkpoints for each account
mapping(address => uint32) public numCheckpoints;
/// @notice The EIP-712 typehash for the contract's domain
bytes32 public constant DOMAIN_TYPEHASH =
keccak256(
"EIP712Domain(string name,uint256 chainId,address verifyingContract)"
);
/// @notice The EIP-712 typehash for the delegation struct used by the contract
bytes32 public constant DELEGATION_TYPEHASH =
keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)");
/// @notice The identifier of the role which allows special transfer privileges.
bytes32 public constant TRANSFER_ROLE = keccak256("TRANSFER_ROLE");
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
/// @notice A record of states for signing / validating signatures
mapping(address => uint256) public nonces;
/// @notice An event thats emitted when an account changes its delegate
event DelegateChanged(
address indexed delegator,
address indexed fromDelegate,
address indexed toDelegate
);
/// @notice An event thats emitted when a delegate account's vote balance changes
event DelegateVotesChanged(
address indexed delegate,
uint256 previousBalance,
uint256 newBalance
);
/// @notice The standard EIP-20 transfer event
event Transfer(address indexed from, address indexed to, uint256 amount);
/// @notice The standard EIP-20 approval event
event Approval(
address indexed owner,
address indexed spender,
uint256 amount
);
event CliffTimestampUpdate(uint256 newTimestamp);
/**
* @dev Emitted when the transfer is enabled triggered by `account`.
*/
event TransferEnabled(address account);
/// @notice a modifier which checks if transfers are allowed
modifier whenTransferAllowed() {
require(
transferAllowed || hasRole(TRANSFER_ROLE, msg.sender),
"OndoToken: Transfers not allowed or not right privillege"
);
_;
}
/**
* @notice Construct a new Ondo token
* @param _governance The initial account to grant owner permission and all the tokens
*/
constructor(
address _governance,
uint256 _cliffTimestamp,
uint256 _tranche1VestingPeriod,
uint256 _tranche2VestingPeriod,
uint256 _seedVestingPeriod
)
LinearTimelock(
_cliffTimestamp,
_tranche1VestingPeriod,
_tranche2VestingPeriod,
_seedVestingPeriod
)
{
balances[_governance] = uint96(totalSupply);
_setupRole(DEFAULT_ADMIN_ROLE, _governance);
_setupRole(TRANSFER_ROLE, _governance);
_setupRole(MINTER_ROLE, _governance);
emit Transfer(address(0), _governance, totalSupply);
}
/**
* @notice Get the number of tokens `spender` is approved to spend on behalf of `account`
* @param account The address of the account holding the funds
* @param spender The address of the account spending the funds
* @return The number of tokens approved
*/
function allowance(address account, address spender)
external
view
returns (uint256)
{
return allowances[account][spender];
}
/**
* @notice Approve `spender` to transfer up to `amount` from `src`
* @dev This will overwrite the approval amount for `spender`
* and is subject to issues noted [here](https://eips.ethereum.org/EIPS/eip-20#approve)
* @param spender The address of the account which may transfer tokens
* @param rawAmount The number of tokens that are approved (2^256-1 means infinite)
* @return Whether or not the approval succeeded
*/
function approve(address spender, uint256 rawAmount) external returns (bool) {
uint96 amount;
if (rawAmount == type(uint256).max) {
amount = type(uint96).max;
} else {
amount = safe96(rawAmount, "Ondo::approve: amount exceeds 96 bits");
}
allowances[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
/**
* @notice Get the number of tokens held by the `account`
* @param account The address of the account to get the balance of
* @return The number of tokens held
*/
function balanceOf(address account) external view returns (uint256) {
return balances[account];
}
/**
* @notice Get the total number of UNLOCKED tokens held by the `account`
* @param account The address of the account to get the unlocked balance of
* @return The number of unlocked tokens held.
*/
function getFreedBalance(address account) external view returns (uint256) {
if (investorBalances[account].initialBalance > 0) {
return _getFreedBalance(account);
} else {
return balances[account];
}
}
/**
* @notice Transfer `amount` tokens from `msg.sender` to `dst`
* @param dst The address of the destination account
* @param rawAmount The number of tokens to transfer
* @return Whether or not the transfer succeeded
*/
function transfer(address dst, uint256 rawAmount) external returns (bool) {
uint96 amount = safe96(rawAmount, "Ondo::transfer: amount exceeds 96 bits");
_transferTokens(msg.sender, dst, amount);
return true;
}
/**
* @notice Transfer `amount` tokens from `src` to `dst`
* @param src The address of the source account
* @param dst The address of the destination account
* @param rawAmount The number of tokens to transfer
* @return Whether or not the transfer succeeded
*/
function transferFrom(
address src,
address dst,
uint256 rawAmount
) external returns (bool) {
address spender = msg.sender;
uint96 spenderAllowance = allowances[src][spender];
uint96 amount = safe96(rawAmount, "Ondo::approve: amount exceeds 96 bits");
if (spender != src && spenderAllowance != type(uint96).max) {
uint96 newAllowance =
sub96(
spenderAllowance,
amount,
"Ondo::transferFrom: transfer amount exceeds spender allowance"
);
allowances[src][spender] = newAllowance;
emit Approval(src, spender, newAllowance);
}
_transferTokens(src, dst, amount);
return true;
}
/**
* @notice Delegate votes from `msg.sender` to `delegatee`
* @param delegatee The address to delegate votes to
*/
function delegate(address delegatee) public {
return _delegate(msg.sender, delegatee);
}
/**
* @notice Delegates votes from signatory to `delegatee`
* @param delegatee The address to delegate votes to
* @param nonce The contract state required to match the signature
* @param expiry The time at which to expire the signature
* @param v The recovery byte of the signature
* @param r Half of the ECDSA signature pair
* @param s Half of the ECDSA signature pair
*/
function delegateBySig(
address delegatee,
uint256 nonce,
uint256 expiry,
uint8 v,
bytes32 r,
bytes32 s
) public {
bytes32 domainSeparator =
keccak256(
abi.encode(
DOMAIN_TYPEHASH,
keccak256(bytes(name)),
getChainId(),
address(this)
)
);
bytes32 structHash =
keccak256(abi.encode(DELEGATION_TYPEHASH, delegatee, nonce, expiry));
bytes32 digest =
keccak256(abi.encodePacked("\\x19\\x01", domainSeparator, structHash));
address signatory = ecrecover(digest, v, r, s);
require(signatory != address(0), "Ondo::delegateBySig: invalid signature");
require(nonce == nonces[signatory]++, "Ondo::delegateBySig: invalid nonce");
require(
block.timestamp <= expiry,
"Ondo::delegateBySig: signature expired"
);
return _delegate(signatory, delegatee);
}
/**
* @notice Gets the current votes balance for `account`
* @param account The address to get votes balance
* @return The number of current votes for `account`
*/
function getCurrentVotes(address account) external view returns (uint96) {
uint32 nCheckpoints = numCheckpoints[account];
return nCheckpoints > 0 ? checkpoints[account][nCheckpoints - 1].votes : 0;
}
/**
* @notice Determine the prior number of votes for an account as of a block number
* @dev Block number must be a finalized block or else this function will revert to prevent misinformation.
* @param account The address of the account to check
* @param blockNumber The block number to get the vote balance at
* @return The number of votes the account had as of the given block
*/
function getPriorVotes(address account, uint256 blockNumber)
public
view
returns (uint96)
{
require(
blockNumber < block.number,
"Ondo::getPriorVotes: not yet determined"
);
uint32 nCheckpoints = numCheckpoints[account];
if (nCheckpoints == 0) {
return 0;
}
// First check most recent balance
if (checkpoints[account][nCheckpoints - 1].fromBlock <= blockNumber) {
return checkpoints[account][nCheckpoints - 1].votes;
}
// Next check implicit zero balance
if (checkpoints[account][0].fromBlock > blockNumber) {
return 0;
}
uint32 lower = 0;
uint32 upper = nCheckpoints - 1;
while (upper > lower) {
uint32 center = upper - (upper - lower) / 2; // ceil, avoiding overflow
Checkpoint memory cp = checkpoints[account][center];
if (cp.fromBlock == blockNumber) {
return cp.votes;
} else if (cp.fromBlock < blockNumber) {
lower = center;
} else {
upper = center - 1;
}
}
return checkpoints[account][lower].votes;
}
/**
* @notice Create `rawAmount` new tokens and assign them to `account`.
* @param account The address to give newly minted tokens to
* @param rawAmount Number of new tokens to mint.
* @dev Even though total token supply is uint96, we use uint256 for the amount for consistency with other external interfaces.
*/
function mint(address account, uint256 rawAmount) external {
require(hasRole(MINTER_ROLE, msg.sender), "Ondo::mint: not authorized");
require(account != address(0), "cannot mint to the zero address");
uint96 amount = safe96(rawAmount, "Ondo::mint: amount exceeds 96 bits");
uint96 supply =
safe96(totalSupply, "Ondo::mint: totalSupply exceeds 96 bits");
totalSupply = add96(supply, amount, "Ondo::mint: token supply overflow");
balances[account] = add96(
balances[account],
amount,
"Ondo::mint: balance overflow"
);
emit Transfer(address(0), account, amount);
}
function _delegate(address delegator, address delegatee) internal {
address currentDelegate = delegates[delegator];
uint96 delegatorBalance = balances[delegator];
delegates[delegator] = delegatee;
emit DelegateChanged(delegator, currentDelegate, delegatee);
_moveDelegates(currentDelegate, delegatee, delegatorBalance);
}
function _transferTokens(
address src,
address dst,
uint96 amount
) internal whenTransferAllowed {
require(
src != address(0),
"Ondo::_transferTokens: cannot transfer from the zero address"
);
require(
dst != address(0),
"Ondo::_transferTokens: cannot transfer to the zero address"
);
if (investorBalances[src].initialBalance > 0) {
require(
amount <= _getFreedBalance(src),
"Ondo::_transferTokens: not enough unlocked balance"
);
}
balances[src] = sub96(
balances[src],
amount,
"Ondo::_transferTokens: transfer amount exceeds balance"
);
balances[dst] = add96(
balances[dst],
amount,
"Ondo::_transferTokens: transfer amount overflows"
);
emit Transfer(src, dst, amount);
_moveDelegates(delegates[src], delegates[dst], amount);
}
function _moveDelegates(
address srcRep,
address dstRep,
uint96 amount
) internal {
if (srcRep != dstRep && amount > 0) {
if (srcRep != address(0)) {
uint32 srcRepNum = numCheckpoints[srcRep];
uint96 srcRepOld =
srcRepNum > 0 ? checkpoints[srcRep][srcRepNum - 1].votes : 0;
uint96 srcRepNew =
sub96(srcRepOld, amount, "Ondo::_moveVotes: vote amount underflows");
_writeCheckpoint(srcRep, srcRepNum, srcRepOld, srcRepNew);
}
if (dstRep != address(0)) {
uint32 dstRepNum = numCheckpoints[dstRep];
uint96 dstRepOld =
dstRepNum > 0 ? checkpoints[dstRep][dstRepNum - 1].votes : 0;
uint96 dstRepNew =
add96(dstRepOld, amount, "Ondo::_moveVotes: vote amount overflows");
_writeCheckpoint(dstRep, dstRepNum, dstRepOld, dstRepNew);
}
}
}
function _writeCheckpoint(
address delegatee,
uint32 nCheckpoints,
uint96 oldVotes,
uint96 newVotes
) internal {
uint32 blockNumber =
safe32(
block.number,
"Ondo::_writeCheckpoint: block number exceeds 32 bits"
);
if (
nCheckpoints > 0 &&
checkpoints[delegatee][nCheckpoints - 1].fromBlock == blockNumber
) {
checkpoints[delegatee][nCheckpoints - 1].votes = newVotes;
} else {
checkpoints[delegatee][nCheckpoints] = Checkpoint(blockNumber, newVotes);
numCheckpoints[delegatee] = nCheckpoints + 1;
}
emit DelegateVotesChanged(delegatee, oldVotes, newVotes);
}
function getChainId() internal view returns (uint256) {
uint256 chainId;
assembly {
chainId := chainid()
}
return chainId;
}
/**
* @notice Turn on _transferAllowed variable. Transfers are enabled
*/
function enableTransfer() external {
require(
hasRole(DEFAULT_ADMIN_ROLE, msg.sender),
"Ondo::enableTransfer: not authorized"
);
transferAllowed = true;
emit TransferEnabled(msg.sender);
}
/**
* @notice Called by merkle airdrop contract to initialize locked balances
*/
function updateTrancheBalance(
address beneficiary,
uint256 rawAmount,
IOndo.InvestorType investorType
) external {
require(hasRole(TIMELOCK_UPDATE_ROLE, msg.sender));
require(rawAmount > 0, "Ondo::updateTrancheBalance: amount must be > 0");
require(
investorBalances[beneficiary].initialBalance == 0,
"Ondo::updateTrancheBalance: already has timelocked Ondo"
); //Prevents users from being in more than 1 tranche
uint96 amount =
safe96(rawAmount, "Ondo::updateTrancheBalance: amount exceeds 96 bits");
investorBalances[beneficiary] = InvestorParam(investorType, amount);
}
/**
* @notice Internal function the amount of unlocked Ondo for an account that participated in Coinlist/Seed Investments
*/
function _getFreedBalance(address account) internal view returns (uint96) {
if (passedAllVestingPeriods()) {
//all vesting periods are over, just return the total balance
return balances[account];
} else {
InvestorParam memory investorParam = investorBalances[account];
if (passedCliff()) {
//we are in between the cliff timestamp and last vesting period
(uint256 vestingPeriod, uint256 elapsed) =
_getTrancheInfo(investorParam.investorType);
uint96 lockedBalance =
sub96(
investorParam.initialBalance,
_proportionAvailable(elapsed, vestingPeriod, investorParam),
"Ondo::getFreedBalance: locked balance underflow"
);
return
sub96(
balances[account],
lockedBalance,
"Ondo::getFreedBalance: total freed balance underflow"
);
} else {
//we have not hit the cliff yet, all investor balance is locked
return
sub96(
balances[account],
investorParam.initialBalance,
"Ondo::getFreedBalance: balance underflow"
);
}
}
}
function updateCliffTimestamp(uint256 newTimestamp) external {
require(
hasRole(DEFAULT_ADMIN_ROLE, msg.sender),
"Ondo::updateCliffTimestamp: not authorized"
);
cliffTimestamp = newTimestamp;
emit CliffTimestampUpdate(newTimestamp);
}
}
File 3 of 3: BizGuard
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol)
pragma solidity ^0.8.20;
import {Context} from "../utils/Context.sol";
/**
* @dev Contract module which provides a basic access control mechanism, where
* there is an account (an owner) that can be granted exclusive access to
* specific functions.
*
* The initial owner is set to the address provided by the deployer. This can
* later be changed with {transferOwnership}.
*
* This module is used through inheritance. It will make available the modifier
* `onlyOwner`, which can be applied to your functions to restrict their use to
* the owner.
*/
abstract contract Ownable is Context {
address private _owner;
/**
* @dev The caller account is not authorized to perform an operation.
*/
error OwnableUnauthorizedAccount(address account);
/**
* @dev The owner is not a valid owner account. (eg. `address(0)`)
*/
error OwnableInvalidOwner(address owner);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev Initializes the contract setting the address provided by the deployer as the initial owner.
*/
constructor(address initialOwner) {
if (initialOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(initialOwner);
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
_checkOwner();
_;
}
/**
* @dev Returns the address of the current owner.
*/
function owner() public view virtual returns (address) {
return _owner;
}
/**
* @dev Throws if the sender is not the owner.
*/
function _checkOwner() internal view virtual {
if (owner() != _msgSender()) {
revert OwnableUnauthorizedAccount(_msgSender());
}
}
/**
* @dev Leaves the contract without owner. It will not be possible to call
* `onlyOwner` functions. Can only be called by the current owner.
*
* NOTE: Renouncing ownership will leave the contract without an owner,
* thereby disabling any functionality that is only available to the owner.
*/
function renounceOwnership() public virtual onlyOwner {
_transferOwnership(address(0));
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Can only be called by the current owner.
*/
function transferOwnership(address newOwner) public virtual onlyOwner {
if (newOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(newOwner);
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Internal function without access restriction.
*/
function _transferOwnership(address newOwner) internal virtual {
address oldOwner = _owner;
_owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (access/Ownable2Step.sol)
pragma solidity ^0.8.20;
import {Ownable} from "./Ownable.sol";
/**
* @dev Contract module which provides access control mechanism, where
* there is an account (an owner) that can be granted exclusive access to
* specific functions.
*
* This extension of the {Ownable} contract includes a two-step mechanism to transfer
* ownership, where the new owner must call {acceptOwnership} in order to replace the
* old one. This can help prevent common mistakes, such as transfers of ownership to
* incorrect accounts, or to contracts that are unable to interact with the
* permission system.
*
* The initial owner is specified at deployment time in the constructor for `Ownable`. This
* can later be changed with {transferOwnership} and {acceptOwnership}.
*
* This module is used through inheritance. It will make available all functions
* from parent (Ownable).
*/
abstract contract Ownable2Step is Ownable {
address private _pendingOwner;
event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner);
/**
* @dev Returns the address of the pending owner.
*/
function pendingOwner() public view virtual returns (address) {
return _pendingOwner;
}
/**
* @dev Starts the ownership transfer of the contract to a new account. Replaces the pending transfer if there is one.
* Can only be called by the current owner.
*
* Setting `newOwner` to the zero address is allowed; this can be used to cancel an initiated ownership transfer.
*/
function transferOwnership(address newOwner) public virtual override onlyOwner {
_pendingOwner = newOwner;
emit OwnershipTransferStarted(owner(), newOwner);
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`) and deletes any pending owner.
* Internal function without access restriction.
*/
function _transferOwnership(address newOwner) internal virtual override {
delete _pendingOwner;
super._transferOwnership(newOwner);
}
/**
* @dev The new owner accepts the ownership transfer.
*/
function acceptOwnership() public virtual {
address sender = _msgSender();
if (pendingOwner() != sender) {
revert OwnableUnauthorizedAccount(sender);
}
_transferOwnership(sender);
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)
pragma solidity ^0.8.20;
/**
* @dev Provides information about the current execution context, including the
* sender of the transaction and its data. While these are generally available
* via msg.sender and msg.data, they should not be accessed in such a direct
* manner, since when dealing with meta-transactions the account sending and
* paying for execution may not be the actual sender (as far as an application
* is concerned).
*
* This contract is only required for intermediate, library-like contracts.
*/
abstract contract Context {
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}
function _msgData() internal view virtual returns (bytes calldata) {
return msg.data;
}
function _contextSuffixLength() internal view virtual returns (uint256) {
return 0;
}
}
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.28;
import {Ownable2Step, Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol";
/**
* @title BizGuard - Guard contract for Biz.
* @dev Most important concepts:
* - BizGuard maintains the nonce of all Biz for signature validation.
* - BizGuard is an ownable smart contract with 4337 flag to enable/disable ERC-4337 of Biz.
* @author David Kim - @PowerStream3604
*/
contract BizGuard is Ownable2Step {
event FlagSet(bool flag);
mapping(address => uint256) public accountNonce;
bool erc4337Flag;
/**
* @notice constructor
* @dev Sets the initial owner of the account.
* @param owner address.
*/
constructor(address owner) Ownable(owner) {}
/**
* @notice Increment nonce of the msg.sender.
* @dev Nonce can only increment and not decrement.
* @return nonce uint256 nonce of the msg.sender.
*/
function incrementNonce() external returns (uint256 nonce) {
nonce = accountNonce[msg.sender];
unchecked {
++accountNonce[msg.sender];
}
}
/**
* @notice Return if the 4337 flag is enabled.
* @dev This view function is called by validateUserOp() in all Biz wallet.
* @return isEnabled bool flag whether 4337 is enabled.
*/
function is4337Enabled() external view returns (bool isEnabled) {
isEnabled = erc4337Flag;
}
/**
* @notice Set function to set 4337 Flag.
* @dev This function can only be called by the owner.
* @param flag bool value of the flag.
*/
function set4337Flag(bool flag) external onlyOwner {
erc4337Flag = flag;
emit FlagSet(flag);
}
}