Transactions
Token Transfers
Tokens
Internal Transactions
Coin Balance History
Logs
Code
Read Contract
Read Proxy
Write Contract
Write Proxy
Minimal Proxy Contract for 0x5fe3543f96c7aa1a85d82206ccedc80dc2722d00.
EIP-1167 - minimal bytecode implementation that delegates all calls to a known address
EIP-1167 - minimal bytecode implementation that delegates all calls to a known address
- Contract name:
- TimeCapsule
- Optimization enabled
- true
- Compiler version
- v0.8.16+commit.07a7930e
- Optimization runs
- 200
- Verified at
- 2023-07-17T12:15:44.109806Z
Contract source code
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.16;
// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/IERC20.sol)
/**
* @dev Interface of the ERC20 standard as defined in the EIP.
*/
interface IERC20 {
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
/**
* @dev Returns the amount of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the amount of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves `amount` tokens from the caller's account to `to`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address to, uint256 amount) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 amount) external returns (bool);
/**
* @dev Moves `amount` tokens from `from` to `to` using the
* allowance mechanism. `amount` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(address from, address to, uint256 amount) external returns (bool);
}
/**
* @dev copied and condensed from @openzeppelin/contracts/proxy/utils/Initializable.sol
**/
abstract contract Initializable {
/**
* @dev Indicates that the contract has been initialized.
* @custom:oz-retyped-from bool
*/
uint8 private _initialized;
/**
* @dev Indicates that the contract is in the process of being initialized.
*/
bool private _initializing;
/**
* @dev Triggered when the contract has been initialized or reinitialized.
*/
event Initialized(uint8 version);
/**
* @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope,
* `onlyInitializing` functions can be used to initialize parent contracts.
*
* Similar to `reinitializer(1)`, except that functions marked with `initializer` can be nested in the context of a
* constructor.
*
* Emits an {Initialized} event.
*/
modifier initializer() {
bool isTopLevelCall = !_initializing;
require(
/* changed from: (isTopLevelCall && _initialized < 1) || (!Address.isContract(address(this)) && _initialized == 1),
to: */(isTopLevelCall && _initialized < 1) || ((address(this).code.length == 0) && _initialized == 1),
"Initializable: contract is already initialized"
);
_initialized = 1;
if (isTopLevelCall) {
_initializing = true;
}
_;
if (isTopLevelCall) {
_initializing = false;
emit Initialized(1);
}
}
/**
* @dev Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call.
* Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized
* to any version. It is recommended to use this to lock implementation contracts that are designed to be called
* through proxies.
*
* Emits an {Initialized} event the first time it is successfully executed.
*/
function _disableInitializers() internal virtual {
require(!_initializing, "Initializable: contract is initializing");
if (_initialized < type(uint8).max) {
_initialized = type(uint8).max;
emit Initialized(type(uint8).max);
}
}
/**
* @dev Returns `true` if the contract is currently initializing. See {onlyInitializing}.
* @dev added by TPM
*/
function isInitialized() public view returns (bool) {
return _initialized == 1;
}
}
library Strings {
function toString(uint256 value) internal pure returns (string memory) {
// from @openzeppelin String.sol
unchecked {
////
// uint256 length = Math.log10(value) + 1; =>
// from @openzeppelin Math.sol
uint256 length = 0;
if (value >= 10**64) { value /= 10**64; length += 64; }
if (value >= 10**32) { value /= 10**32; length += 32; }
if (value >= 10**16) { value /= 10**16; length += 16; }
if (value >= 10**8) { value /= 10**8; length += 8; }
if (value >= 10**4) { value /= 10**4; length += 4; }
if (value >= 10**2) { value /= 10**2; length += 2; }
if (value >= 10**1) { length += 1; }
length++;
////
string memory buffer = new string(length);
uint256 ptr;
assembly {
ptr := add(buffer, add(32, length))
}
while (true) {
ptr--;
/// @solidity memory-safe-assembly
assembly {
mstore8(ptr, byte(mod(value, 10), "0123456789abcdef"))
}
value /= 10;
if (value == 0) break;
}
return buffer;
}
}
}
/**
* @dev Interface of the ERC20 standard as defined in the EIP.
*/
interface ITimeCapsuleFactory {
struct OwnerCapsuleRecord {
address capsuleAddress;
bytes32 recoveryAddressHash;
bool validated;
}
function predictedCapsuleAddress(address ownerAddress) external view returns (address predictedAddress);
function createTimeCapsule() external;
function createTimeCapsule(bytes32 _recoveryAddressHash) external;
function capsuleAddressOf(address _owner) external view returns (address capsuleAddress);
function validateRecoveryAddressHash(
address _owner,
bytes32 _recoveryAddressHash,
bytes memory _signature
) external;
function checkRecoveryAddress(address _owner, bytes32 _addressHash) external view returns (bool confirmed);
function isRecoveryHashValidated(address _owner) external view returns (bool);
function getRecoveryRecord(address _owner) external view returns (OwnerCapsuleRecord memory);
function recoverOwnership( address _oldOwner, address _newOwner ) external;
}
/// @dev SYNONYMS: vault, capsule, timecapsule
/// @dev _feeSplitter (feeSplitter.sol) is our own, trusted contract (context: reentrance attacks)
interface IFeeSplitter {
function splitERC20(address tokenAddress) external returns (bool);
}
/**
* @dev A place for TimeCapsule Constants to live
*/
abstract contract TimeCapsuleContext is Initializable {
uint256 constant internal FEE_DIVISOR = 1000; // 0.1%
uint64 constant internal ONE_HOUR = 60 * 60;
uint64 constant internal ONE_DAY = 24 * 60 * 60;
uint64 constant internal SEVEN_DAYS = 7 * ONE_DAY;
uint64 constant internal ONE_YEAR = 365 * ONE_DAY;
uint64 constant internal MAX_LOCK_SECONDS = 20089 * ONE_DAY; // ~55 years (365.25 days each)
uint64 constant internal TIMELOCK_SECONDS = SEVEN_DAYS;
uint64 constant internal BEHAVIORLOCK_SECONDS = ONE_YEAR;
address constant internal NATIVE_COIN = address(0);
address constant internal INVALID_SIGNER_ADDRESS = address(0);
string constant internal VAULT_RECOVERY_AUTHORIZATION="VAULT RECOVERY AUTHORIZATION";
string constant internal CONFIRMING_RECOVERY_ADDRESS="CONFIRMING RECOVERY ADDRESS";
enum VaultStatus {
NOMINAL, // all is well
PANIC, // 'panic switch' activated (lockout for recovery)
RECOVERED // (read-only)
}
enum LockState {
INERT,
WITHDRAWAL_PENDING
}
enum LockType {
DRIP,
HARD,
BEHAVIORAL
}
struct Lock {
LockState state; // uint8
bytes32 tag; // <= 31 character descriptor string
uint64 lockTime; // UTC
uint64 unlockTime; // UTC
uint64 releaseTime; // UTC timestamp when time-locked funds release for sending
LockType lockType; // uint8
bytes6 unlockHash; // first 6 bytes of keccak256 hash
// 256 bit boundary
uint256 lockedAmount; // in ERC20 token's base units
uint256 withdrawnToDate; // in ERC20 token's base units
uint256 timelockedAmount; // amount of any pending, time-locked withdrawal
}
}
/**
* @title TimeCapsule
*/
contract TimeCapsule is TimeCapsuleContext{
address public owner;
ITimeCapsuleFactory private _capsuleFactory;
mapping(address => Lock[]) private _locks; // address is 'rc20 token contract address or address(0)
mapping(address => uint256) private _totalLockedAmount;
uint64 private _deadmanTime;
VaultStatus private _vaultStatus;
IFeeSplitter private _feeSplitter;
constructor() {
// Contructor not used by factory. We use it here only to prevent master copy hijacks
// the factory will call this.initialize() for one-off setup tasks.
_disableInitializers();
}
/**
* @dev Because this contract is a proxied clone (oppenzep'Clone.sol)
* functions herein get called **twice**. The first call has `msg.sender`
* set as the address of the caller, as usual. The second _proxy_ call
* has `msg.sender` set to this contract's own address. Hence, we roll
* our own 'Ownable' (minus transferability) and with a different name
* than 'onlyOwner' to avoid confusion.
*/
modifier _onlyCapsuleOwner() {
if (address(this) != msg.sender) {
require(owner == msg.sender, "Not owner");
_;
}
}
/**
* @dev This modifier blocks calls to functions when the vault is in the 'panic' state.
*/
modifier _notPanicState() {
if (address(this) != msg.sender) {
require(_vaultStatus != VaultStatus.PANIC, "Forbidden");
_;
}
}
/**
* Called once by factory directly after cloning to initialize vault.
* @param _newOwner the vaults's owner address
* @param _factoryAddress used to manage recovery from panic state aka "undo a hack"
* @param _feeSplitterContract the [contract] address to send fees
*/
function initialize(
address _newOwner,
address _factoryAddress,
IFeeSplitter _feeSplitterContract
)
initializer
public
{
owner = _newOwner;
_capsuleFactory = ITimeCapsuleFactory(_factoryAddress);
_deadmanTime = (uint64)(block.timestamp);
_vaultStatus = VaultStatus.NOMINAL;
_feeSplitter = _feeSplitterContract;
}
/**
* When native coin is sent directly to this vault (as if it were a standard wallet)
* the funds are auto-locked for seven days (and the fee is paid accordingly.) This is
* intended as a safety feature and not to be advertized as good practice.
*/
receive() external payable { }
event Locked(
bytes32 tag,
address tokenAddress,
uint256 lockIndex,
uint256 amount,
uint256 fee,
uint256 lockTime,
uint256 unlockTime,
LockType lockType
);
event WithdrawalInitiated(
address tokenAddress,
uint256 lockIndex,
uint256 amount
);
event WithdrawalCancelled(
address tokenAddress,
uint256 lockIndex
);
event Withdrawal(
address tokenAddress,
uint256 lockIndex,
uint256 amount
);
event LockReleased(
address tokenAddress,
uint256 lockIndex
);
event RecoveryInitiated();
event Recovered();
error HardLocked(
uint64 unlockTime
);
error InsufficientFunds(
uint256 grossBalance,
uint256 alreadyLocked,
uint256 availableToLock
);
/**
* Transfers nativer coin from the vault to a recipient.
* @notice Do not use simple trasfer(<maxgas: 2300!>) — Gnosis safe (for example) infamously incompatible
* @param _to recipient address
* @param _value base unit amount to transfer
*/
function _transferNative(
address _to,
uint256 _value
)
internal
returns (bool success)
{
(success, ) = _to.call{ value: _value }("");
}
/**
* Transfers 'RC20 tokens from the vault to a recipient.
* @notice internal function
* @param _tokenAddress 'RC20 token contract address
* @param _to recipient address
* @param _value base unit amount to transfer
* @return bool
*/
function _transferToken(
address _tokenAddress,
address _to,
uint256 _value
)
internal
returns (bool)
{
return IERC20(_tokenAddress).transfer(
_to,
_value
);
}
/**
* Returns the current "state" of the vault. See `enum VaultStatus {...}`
* @return status enum VaultStatus.<NOMINAL|PANIC|RECOVERED>
*/
function vaultStatus()
public
view
returns (VaultStatus status)
{
status = _vaultStatus;
}
/**
* Validates recovery address. Validation is required as a separate step to account for possibily
* of mistake or bad actor using the same recovery address as an attempt to 'block' geniune use.
* @param _recoveryAddressHash keccak256 (sha3) hash of recovery address (so as not to 'dox' the address)
* @param _signature personal_sign of the string: "CONFIRMING RECOVERY ADDRESS"
*/
function validateRecoveryAddressHash(
bytes32 _recoveryAddressHash,
bytes memory _signature
)
public
_onlyCapsuleOwner
_notPanicState
{
_capsuleFactory.validateRecoveryAddressHash(
owner,
_recoveryAddressHash,
_signature
);
}
/**
* Used to test if a given recovery address hash is valid for (matches) the hash stored for this vault.
* @param _recoveryAddressHash keccak256 hash of the recovery address / public key
* @return confirmed bool
*/
function checkRecoveryAddress(bytes32 _recoveryAddressHash)
public
view
returns (bool confirmed)
{
confirmed = _capsuleFactory.checkRecoveryAddress(owner, _recoveryAddressHash);
}
/**
* Returns true if tha vault's recovery address hash has been validated — see validateRecoveryAddressHash(...).
* @return validated bool
*/
function isRecoveryHashValidated()
public view
returns (bool validated)
{
return _capsuleFactory.isRecoveryHashValidated(owner);
}
/**
* Returns the number of active locks for a given token contract address (or address(0) for
* native coin.) Used in conjunction with lockData() to retrieve list of locks for a spcific token.
* @param _tokenAddress 'RC2- contract address
* @return count number of active locks for the specified token
*/
function lockCount(address _tokenAddress)
public
view
returns (uint256 count)
{
count = _locks[_tokenAddress].length;
}
/**
* Returns Lock record for an existing lock.
* @param _tokenAddress 'RC20 token contract address
* @param _index lock index
* @return lock Lock record
*/
function lockData(
address _tokenAddress,
uint256 _index
)
public
view
returns (Lock memory lock)
{
Lock[] memory locks = _locks[_tokenAddress];
require(locks.length > 0,
"No locks for given token address"
);
require(locks.length > _index,
"Invalid lock index"
);
lock = locks[_index];
}
/**
* Returns available/lockable balance (owned by this vault and not tied up in locks)
* @param _tokenAddress token contract address or address(0) for native coin
*/
function availableBalance(
address _tokenAddress // use address(0) for native coin
)
public
view
returns (uint256 available)
{
available = 0;
uint256 _grossBalance;
if (_tokenAddress == NATIVE_COIN) {
_grossBalance = address(this).balance;
} else {
require(
_tokenAddress.code.length > 0,
"not a token"
);
_grossBalance = IERC20(_tokenAddress).balanceOf(address(this));
}
if (_grossBalance > _totalLockedAmount[_tokenAddress]) {
available = _grossBalance - _totalLockedAmount[_tokenAddress];
}
}
/**
* @notice internal function
* @param _tag bytes32String: up to 31 characters of user input as a label for the lock
* @param _tokenAddress token contract address or address(0) for native coin
* @param _lockAmount unit value of amount to be locked
* @param _lockTime UTC unix timestamp (seconds)
* @param _unlockTime UTC unix timestamp (seconds)
* @param _lockType enum LockType.<DRIP|HARD|BEHAVIORAL>
* @return lockIndex array index of created lock
*/
function _storeNewLock(
bytes32 _tag,
address _tokenAddress,
uint256 _lockAmount,
uint64 _lockTime,
uint64 _unlockTime,
LockType _lockType
)
internal
returns (uint256 lockIndex)
{
Lock[] storage locksRef = _locks[_tokenAddress];
Lock memory lock = Lock({
state: LockState.INERT,
tag: _tag,
lockTime: _lockTime,
unlockTime: _unlockTime,
lockType: _lockType,
unlockHash: 0,
lockedAmount: _lockAmount,
withdrawnToDate: 0,
timelockedAmount: 0,
releaseTime: 0
});
locksRef.push(lock);
lockIndex = locksRef.length - 1;
}
/**
* @notice internal function
* @param _tag bytes32String: up to 31 characters of user input as a label for the lock (may be "")
* @param _tokenAddress token contract address or address(0) for native coin
* @param _lockAmount unit value of amount to be locked
* @param _lockTime UTC unix timestamp (seconds)
* @param _unlockTime UTC unix timestamp (seconds)
* @param _lockType LockType.<DRIP|HARD|BEHAVIORAL>
* @return lockIndex array index of created lock
*/
function _createLock(
bytes32 _tag,
address _tokenAddress,
uint256 _lockAmount,
uint256 _fee,
uint64 _lockTime, // UTC unix timestamp (seconds)
uint64 _unlockTime, // UTC unix timestamp (seconds)
LockType _lockType
)
internal
returns (uint256 lockIndex)
{
require(_lockTime >= (uint64)(block.timestamp) - (6 * ONE_HOUR),
"Lock time must not be more than six hours in the past"
);
require(_unlockTime > _lockTime,
"Unlock time must be greater than lock time"
);
require((_unlockTime - _lockTime) < MAX_LOCK_SECONDS, // built-in "safe math" solidity@>0.8.0
"Unlock time must be within 55 years of lock time"
);
unchecked { // division underflow acceptable (decimal truncation)
uint256 _feeCheck = _lockAmount / FEE_DIVISOR;
require(_feeCheck == _fee,
"Fee calc audit failed"
);
}
if (_tokenAddress == NATIVE_COIN) { // native coin
uint256 _availableToLock = availableBalance(_tokenAddress); // any msg.value is already included in balance
if (_availableToLock < (_lockAmount + _fee)) {
revert InsufficientFunds(
address(this).balance - msg.value, // grossBalance (before msg.value, being 'refunded' by this revert)
_totalLockedAmount[_tokenAddress], // alreadyLocked
_availableToLock // availableToLock (includes any incoming msg.value (now being reverted))
);
}
if (_fee > 0) {
(bool feeTransferred, ) = address(_feeSplitter).call{value: _fee}("");
require(
feeTransferred,
"Fee transfer rejected by external contract"
);
}
} else { // 'RC20
IERC20 tokenContract = IERC20(_tokenAddress);
uint256 _availableToLock = availableBalance(_tokenAddress);
if (_availableToLock < (_lockAmount + _fee)) {
uint256 _tokenBalance = IERC20(tokenContract).balanceOf(address(this));
revert InsufficientFunds(
_tokenBalance, // grossBalance
_totalLockedAmount[_tokenAddress], // alreadyLocked
_availableToLock // availableToLock)
);
}
if (_fee > 0) {
bool feeTransferred = IERC20(tokenContract).transfer(
address(_feeSplitter),
_fee
);
require(
feeTransferred,
"Fee transfer rejected by external contract"
);
require(
// @dev splitERC20 splits and sends on the _feeSplitter's entire current balance
_feeSplitter.splitERC20(address(tokenContract)),
"Fee split callout failed"
);
}
}
lockIndex = _storeNewLock(
_tag,
_tokenAddress,
_lockAmount,
_lockTime,
_unlockTime,
_lockType
);
_totalLockedAmount[_tokenAddress] += _lockAmount;
// update 'watchdog' deadman timer
if (_deadmanTime < _unlockTime) _deadmanTime = _unlockTime;
emit Locked(
_tag,
_tokenAddress,
lockIndex,
_lockAmount,
_fee,
_lockTime,
_unlockTime,
_lockType
);
}
/**
* Creates a new standard (LockType.DRIP) time lock.
* @param _tag bytes32String: up to 31 characters of user input as a label for the lock (may be "")
* @param _tokenAddress token contract address or address(0) for native coin
* @param _lockAmount unit value of amount to be locked
* @param _lockTime UTC unix timestamp (seconds)
* @param _unlockTime UTC unix timestamp (seconds)
* @return lockIndex array index of created lock
*/
function createLock(
bytes32 _tag,
address _tokenAddress,
uint256 _lockAmount,
uint256 _fee,
uint64 _lockTime,
uint64 _unlockTime
)
public
payable
_onlyCapsuleOwner
_notPanicState
returns (uint256 lockIndex)
{
lockIndex = _createLock(
_tag,
_tokenAddress,
_lockAmount,
_fee,
_lockTime,
_unlockTime,
LockType.DRIP
);
}
/**
* Creates a new hard lock (LockType.HARD) — a lock that does not drip and must complete full term before withdrawal.
* @param _tag bytes32String: up to 31 characters of user input as a label for the lock (may be "")
* @param _tokenAddress token contract address or address(0) for native coin
* @param _lockAmount unit value of amount to be locked
* @param _lockTime UTC unix timestamp (seconds)
* @param _unlockTime UTC unix timestamp (seconds)
* @return lockIndex array index of created lock
*/
function createHardLock(
bytes32 _tag,
address _tokenAddress,
uint256 _lockAmount,
uint256 _fee,
uint64 _lockTime,
uint64 _unlockTime
)
public
payable
_onlyCapsuleOwner
_notPanicState
returns (uint256 lockIndex)
{
lockIndex = _createLock(
_tag,
_tokenAddress,
_lockAmount,
_fee,
_lockTime,
_unlockTime,
LockType.HARD
);
}
/**
* Creates a behavioral lock (LockType.BAHVIORAL) which lasts up to a maximum of ONE YEAR from _lockStart
* or immediately released when the correct unlock code is passed to releaseBehaviorLock().
* @notice UI implementations should limit the unlock code to a simple six digit number.
* @param _tag bytes32String: up to 31 characters of user input as a label for the lock (may be "")
* @param _tokenAddress token contract address or address(0) for native coin
* @param _lockAmount unit value of amount to be locked
* @param _lockTime UTC unix timestamp (seconds)
* @param _unlockHash first six bytes of a keccak256 (sha3) of a 'secret' unlock text (ideally UI limited to simple 6 digits)
* @return lockIndex array index of created lock
*/
/// @dev we consider it unsafe to trust a miner's clock for lock/unlock times
function createBehaviorLock(
bytes32 _tag,
address _tokenAddress,
uint256 _lockAmount,
uint256 _fee,
uint64 _lockTime,
bytes6 _unlockHash
)
public
payable
_onlyCapsuleOwner
_notPanicState
returns (uint256 lockIndex)
{
lockIndex = _createLock(
_tag,
_tokenAddress,
_lockAmount,
_fee,
_lockTime,
_lockTime + BEHAVIORLOCK_SECONDS,
LockType.BEHAVIORAL
);
_locks[_tokenAddress][lockIndex].unlockHash = _unlockHash;
}
/**
* Intended for a bahioral lock 'friend' to check that the code they have is valid for a given lock.
* @notice Behavioral locks are only minimally secure *intentionally*. The user who created
* the lock may 'cheat' or 'fail to achieve their goal'. Thus for example, potential use
* of this function to brute force the inlock code, while discouraged, is an acceptable
* trade-off. After all, it is the vault owner's money and only their own self worth at stake.
* @param _tokenAddress token contract address or address(0) for native coin
* @param _lockIndex lock index
* @param _unlockCode clear text unlock code
* @return correct bool
*/
function checkBehaviorLockCode(
address _tokenAddress,
uint256 _lockIndex,
string memory _unlockCode
)
public
view
returns (bool correct)
{
bytes32 hashedUnlockCode = keccak256(abi.encodePacked(_unlockCode));
correct = bytes6(hashedUnlockCode) == _locks[_tokenAddress][_lockIndex].unlockHash;
}
/**
* Releases funds from a behavior lock if; a) given correct code OR b) a year has passed since the lock was cretaed.
* @notice Funds are return to the vault's lockable balance. Withdrawal from the vault requires a new lock be created
* @param _tokenAddress token contract address or address(0) for native coin
* @param _lockIndex lock index (for this token)
* @param _unlockCode clear text 'secret' unlock code (cAse senstitve)
*/
function claimBehaviorLock(
address _tokenAddress,
uint256 _lockIndex,
string memory _unlockCode
)
public
payable
_onlyCapsuleOwner
_notPanicState
{
if (_locks[_tokenAddress][_lockIndex].lockTime > uint64(block.timestamp) - BEHAVIORLOCK_SECONDS) {
require(
checkBehaviorLockCode(_tokenAddress, _lockIndex, _unlockCode) == true,
"Invalid code"
);
}
_totalLockedAmount[_tokenAddress] -= _locks[_tokenAddress][_lockIndex].lockedAmount;
_locks[_tokenAddress][_lockIndex].unlockTime = uint64(block.timestamp);
releaseLock(_tokenAddress, _lockIndex);
}
/**
* Returns value of token or coin realeasable NOW — aka the current DRIP amount.
* @notice The actual value released by initiateWithdrawal() may differ — slightly higher.
* However, making this a public function allows users to get an idea of current drip
* amounts.
* @param _tokenAddress token contract address or address(0) for native coin
* @param _index lock index
* @return withdrawalAllowance currently available withdrawable balance (drip amount)
*/
function calculateAllowance(
address _tokenAddress,
uint256 _index
)
public
view
returns (uint256 withdrawalAllowance)
{
Lock memory lock = lockData(_tokenAddress, _index);
if (lock.withdrawnToDate >= lock.lockedAmount) {
withdrawalAllowance = 0;
} else if (block.timestamp < (lock.lockTime - ONE_HOUR) || block.timestamp >= lock.unlockTime) {
if (lock.withdrawnToDate >= lock.lockedAmount) {
withdrawalAllowance = 0;
} else {
withdrawalAllowance = lock.lockedAmount - lock.withdrawnToDate;
}
} else {
uint256 lockTerm = lock.unlockTime - lock.lockTime;
uint256 termServed = ((block.timestamp - lock.lockTime) * 10000 ) / lockTerm ; // 10000 => 100.00% of term
uint256 totalPortionFromStart = lock.lockedAmount * termServed;
unchecked { // division undeflow acceptable (discard remainer)
totalPortionFromStart /= 10000;
}
if (lock.withdrawnToDate >= totalPortionFromStart) withdrawalAllowance = 0;
else withdrawalAllowance = totalPortionFromStart - lock.withdrawnToDate;
// Web UI uses this function to display currently available "drip" amounts. So, we
// account for a pending withdrawal by not including that amount in the returned allowance.
if (lock.timelockedAmount >= withdrawalAllowance) withdrawalAllowance = 0;
else withdrawalAllowance -= lock.timelockedAmount;
}
}
/**
* Releases any remaining balance in a lock without need of withdrawal.
* Reverts if unlock time not yet reached.
* @param _tokenAddress token contract address or address(0) for native coin
* @param _lockIndex lock index (for this token)
* @custom:emits LockReleased(tokenAddress, lockIndex)
*/
function releaseLock(
address _tokenAddress,
uint256 _lockIndex
)
internal
{
Lock[] storage locksRef = _locks[_tokenAddress];
Lock storage lock = locksRef[_lockIndex];
require (
block.timestamp >= lock.unlockTime,
"Before unlock time"
);
// gas efficient removal of array element
uint256 lastIndex = locksRef.length - 1;
if (_lockIndex != lastIndex) locksRef[_lockIndex] = locksRef[lastIndex];
locksRef.pop();
emit LockReleased(
_tokenAddress,
_lockIndex
);
}
/**
* Initiates a seven day withdrawal timelock, after which sendWithdrawal() can be
* called to retrieve the funds.
* @notice Only ONE withdrawl may be active at a time for each token lock. (See `cancelWithdrawal()`, bellow.)
* @param _tokenAddress token contract address or address(0) for native coin
* @param _lockIndex lock index (for this token)
* @custom:emits WithdrawalInitiated(tokenAddress, lockIndex, withdrawalAllowance)
*/
function initiateWithdrawal(
address _tokenAddress,
uint256 _lockIndex
)
public
_onlyCapsuleOwner
_notPanicState
returns (uint256 amount)
{
Lock[] storage locksRef = _locks[_tokenAddress];
require(locksRef.length > 0,
"Lock not found"
);
require(locksRef.length > _lockIndex,
"Invalid lock index"
);
require(locksRef[_lockIndex].state != LockState.WITHDRAWAL_PENDING,
"Withdrawal already pending"
);
Lock storage lock = locksRef[_lockIndex];
require(
lock.lockType != LockType.BEHAVIORAL,
"Behavioral lock"
);
if (lock.lockType == LockType.HARD && uint64(block.timestamp) < lock.unlockTime) {
revert HardLocked({
unlockTime: lock.unlockTime
});
}
uint256 withdrawalAllowance = calculateAllowance(
_tokenAddress,
_lockIndex
);
require(withdrawalAllowance > 0,
"None available"
);
lock.state = LockState.WITHDRAWAL_PENDING;
lock.timelockedAmount = withdrawalAllowance;
lock.releaseTime = uint64(block.timestamp) + TIMELOCK_SECONDS;
emit WithdrawalInitiated(
_tokenAddress,
_lockIndex,
withdrawalAllowance
);
amount = withdrawalAllowance;
}
/**
* Cancels a pending withdrawal
* @param _tokenAddress token contract address or address(0) for native coin
* @param _lockIndex lock index (for this token)
* @custom:emits WithdrawalCancelled(tokenAddress, lockIndex)
*/
function cancelWithdrawal(
address _tokenAddress,
uint256 _lockIndex
)
public
_onlyCapsuleOwner
{
Lock[] storage locks = _locks[_tokenAddress];
require(locks.length > 0,
"No matching lock found"
);
require(locks.length > _lockIndex,
"Invalid lock index"
);
Lock storage lock = locks[_lockIndex];
require(lock.state == LockState.WITHDRAWAL_PENDING,
"No withdrawal pending"
);
lock.timelockedAmount = 0;
lock.state = LockState.INERT;
emit WithdrawalCancelled(
_tokenAddress,
_lockIndex
);
}
/**
* Sends a pending seven day timelocked withdrawal to the vault's owner (reverts if not yet time)
* @param _tokenAddress token contract address or address(0) for native coin
* @param _lockIndex lock index (for this token)
* @custom:emits Withdrawal(tokenAddress, lockIndex, withdrawalAmount)
*/
function sendWithdrawal(
address _tokenAddress,
uint256 _lockIndex
)
public
_onlyCapsuleOwner
_notPanicState
{
Lock[] storage locksRef = _locks[_tokenAddress];
Lock storage lock = locksRef[_lockIndex];
require(
lock.state == LockState.WITHDRAWAL_PENDING,
"No withdrawal pending"
);
require(
lock.timelockedAmount > 0,
"No withdrawal pending"
);
require(
block.timestamp >= lock.releaseTime,
"Timelocked"
);
/**
* @dev !IMPORTANT!: Clear the balance owed BEFORE calling sender's
* potential contract address to avoid ye olde DAO dance (reentrance hack)
*/
uint256 withdrawalAmount = lock.timelockedAmount;
lock.timelockedAmount = 0;
lock.withdrawnToDate += withdrawalAmount;
bool withdrawalTransferred = (_tokenAddress == NATIVE_COIN)
? _transferNative( owner, withdrawalAmount )
: _transferToken( _tokenAddress, owner, withdrawalAmount )
;
require (
withdrawalTransferred,
"Withdrawal transfer failed"
);
_totalLockedAmount[_tokenAddress] -= withdrawalAmount;
// check for last (emptying) withdrawal
if (lock.withdrawnToDate >= lock.lockedAmount) {
releaseLock(_tokenAddress, _lockIndex);
} else {
lock.releaseTime = 0; // gas discount
lock.state = LockState.INERT;
}
emit Withdrawal(
_tokenAddress,
_lockIndex,
withdrawalAmount
);
}
/**
* Splits a 65 byte (130 nibble) 'raw' signature into R, S, V components
* @param _signature 65 byte (130 nibble) 'raw' signature
* @return r signature R component
* @return s signature S component
* @return v signature V component
*/
function _splitSignature(bytes memory _signature) private pure returns (bytes32 r, bytes32 s, uint8 v) {
assembly {
r := mload(add(_signature, 32))
s := mload(add(_signature, 64))
v := byte(0, mload(add(_signature, 96)))
}
}
/**
* Recovers the address of the signer of a message hash
* @param _messageHash keccak256 (sha3) hash of the message signed
* @param _signature 65 byte (130 nibble) signature
*/
function __recoverSignerAddress(
bytes32 _messageHash,
bytes memory _signature
)
private
pure
returns (address signerAddress)
{
(bytes32 r,bytes32 s, uint8 v) = _splitSignature(_signature);
signerAddress = ecrecover(_messageHash, v, r, s);
}
/**
* Recovers the address of the signer of a arbitrary length message
* @param _message the signed message
* @param _signature signature
*/
function _recoverSignerAddress(
string memory _message,
bytes memory _signature
)
private
pure
returns (address signerAddress)
{
if (_signature.length != 65) return INVALID_SIGNER_ADDRESS;
bytes32 _messageHash = keccak256(
abi.encodePacked(
"\x19Ethereum Signed Message:\n",
Strings.toString(bytes(_message).length),
bytes(_message)
)
);
signerAddress = __recoverSignerAddress(_messageHash, _signature);
}
/**
* @notice internal function
*/
function _panic() internal {
require(
_vaultStatus == VaultStatus.NOMINAL,
"Panic is once only. Good luck!"
);
require(
_capsuleFactory.isRecoveryHashValidated(owner) == true,
"No recovery address"
);
_vaultStatus = VaultStatus.PANIC;
emit RecoveryInitiated();
}
/**
* Places the vault into PANIC state. NON-REVERSIBLE — vault must be recovered to.
* NON-REVERSIBLE — vault must be "recovered" to regain functional access — see recoverVault()
* @notice Two versions — see also panic(bytes memory _signature)
*/
function panic() public _onlyCapsuleOwner {
_panic();
}
/**
* Allows anyone with an off-chain signed message from the original owner to place
* the vault into PANIC state. NON-REVERSIBLE — vault must be "recovered" to regain
* functional access — see recoverVault()
* @notice Two versions — see also panic()
*/
function panic(
bytes memory _signature
)
public
{
address signerAddress = _recoverSignerAddress(VAULT_RECOVERY_AUTHORIZATION, _signature);
require(signerAddress == owner, "Invalid signature");
_panic();
}
/**
* Recovers a vault from PANIC state — see panic() & panic(bytes memory _signature. (Also known as "undo a hack".)
* Recovery can only be executed by the recovery address, which will become the vault's effective new owner.
* The PANIC / RECOVERY process can only be executed ONCE. Don't get hacked again!
* @notice internal function
* @param _originalOwner address of original owner
*/
function _recoverVault(
address _originalOwner
)
internal
{
address _newOwner = msg.sender; // set local owner variable to new owner
// update the factory contract's recovery and owner address mappings
// reverts if vault's recoveryAddressHash does not match keccak256(_newOwner)
_capsuleFactory.recoverOwnership(
_originalOwner,
_newOwner
);
owner = _newOwner;
_vaultStatus = VaultStatus.RECOVERED;
emit Recovered();
}
/**
* Recovers an EXPIRED vault — one that has not had any lock/unlock activity for
* a full year after the latest (most future) lock end time ever created.
* @notice Only the recovery account can do this.
* @param _originalOwner original owner's address
*/
function recoverExpiredVault(
address _originalOwner
)
public
{
uint64 deadmanRecoveryTime = _deadmanTime + ONE_YEAR; // latest ever unlockTime plus one year
uint64 blockTime = (uint64)(block.timestamp);
require(blockTime >= deadmanRecoveryTime, "Vault not expired");
_recoverVault(_originalOwner);
}
/**
* Recovers a vault from PANIC state, passing ownership to the caller.
* The vault must be in PANIC state and caller must be the recovery address
* itself (according to stored recoveryAddressHash.)
**/
function recoverVault(
address _originalOwner
)
public
{
require(_vaultStatus == VaultStatus.PANIC, "Forbidden");
_recoverVault(_originalOwner);
}
}
Contract ABI
[{"type":"constructor","inputs":[]},{"type":"function","stateMutability":"view","outputs":[{"type":"uint256","name":"available","internalType":"uint256"}],"name":"availableBalance","inputs":[{"type":"address","name":"_tokenAddress","internalType":"address"}]},{"type":"function","stateMutability":"view","outputs":[{"type":"uint256","name":"withdrawalAllowance","internalType":"uint256"}],"name":"calculateAllowance","inputs":[{"type":"address","name":"_tokenAddress","internalType":"address"},{"type":"uint256","name":"_index","internalType":"uint256"}]},{"type":"function","stateMutability":"nonpayable","outputs":[],"name":"cancelWithdrawal","inputs":[{"type":"address","name":"_tokenAddress","internalType":"address"},{"type":"uint256","name":"_lockIndex","internalType":"uint256"}]},{"type":"function","stateMutability":"view","outputs":[{"type":"bool","name":"correct","internalType":"bool"}],"name":"checkBehaviorLockCode","inputs":[{"type":"address","name":"_tokenAddress","internalType":"address"},{"type":"uint256","name":"_lockIndex","internalType":"uint256"},{"type":"string","name":"_unlockCode","internalType":"string"}]},{"type":"function","stateMutability":"view","outputs":[{"type":"bool","name":"confirmed","internalType":"bool"}],"name":"checkRecoveryAddress","inputs":[{"type":"bytes32","name":"_recoveryAddressHash","internalType":"bytes32"}]},{"type":"function","stateMutability":"payable","outputs":[],"name":"claimBehaviorLock","inputs":[{"type":"address","name":"_tokenAddress","internalType":"address"},{"type":"uint256","name":"_lockIndex","internalType":"uint256"},{"type":"string","name":"_unlockCode","internalType":"string"}]},{"type":"function","stateMutability":"payable","outputs":[{"type":"uint256","name":"lockIndex","internalType":"uint256"}],"name":"createBehaviorLock","inputs":[{"type":"bytes32","name":"_tag","internalType":"bytes32"},{"type":"address","name":"_tokenAddress","internalType":"address"},{"type":"uint256","name":"_lockAmount","internalType":"uint256"},{"type":"uint256","name":"_fee","internalType":"uint256"},{"type":"uint64","name":"_lockTime","internalType":"uint64"},{"type":"bytes6","name":"_unlockHash","internalType":"bytes6"}]},{"type":"function","stateMutability":"payable","outputs":[{"type":"uint256","name":"lockIndex","internalType":"uint256"}],"name":"createHardLock","inputs":[{"type":"bytes32","name":"_tag","internalType":"bytes32"},{"type":"address","name":"_tokenAddress","internalType":"address"},{"type":"uint256","name":"_lockAmount","internalType":"uint256"},{"type":"uint256","name":"_fee","internalType":"uint256"},{"type":"uint64","name":"_lockTime","internalType":"uint64"},{"type":"uint64","name":"_unlockTime","internalType":"uint64"}]},{"type":"function","stateMutability":"payable","outputs":[{"type":"uint256","name":"lockIndex","internalType":"uint256"}],"name":"createLock","inputs":[{"type":"bytes32","name":"_tag","internalType":"bytes32"},{"type":"address","name":"_tokenAddress","internalType":"address"},{"type":"uint256","name":"_lockAmount","internalType":"uint256"},{"type":"uint256","name":"_fee","internalType":"uint256"},{"type":"uint64","name":"_lockTime","internalType":"uint64"},{"type":"uint64","name":"_unlockTime","internalType":"uint64"}]},{"type":"function","stateMutability":"nonpayable","outputs":[],"name":"initialize","inputs":[{"type":"address","name":"_newOwner","internalType":"address"},{"type":"address","name":"_factoryAddress","internalType":"address"},{"type":"address","name":"_feeSplitterContract","internalType":"contract IFeeSplitter"}]},{"type":"function","stateMutability":"nonpayable","outputs":[{"type":"uint256","name":"amount","internalType":"uint256"}],"name":"initiateWithdrawal","inputs":[{"type":"address","name":"_tokenAddress","internalType":"address"},{"type":"uint256","name":"_lockIndex","internalType":"uint256"}]},{"type":"function","stateMutability":"view","outputs":[{"type":"bool","name":"","internalType":"bool"}],"name":"isInitialized","inputs":[]},{"type":"function","stateMutability":"view","outputs":[{"type":"bool","name":"validated","internalType":"bool"}],"name":"isRecoveryHashValidated","inputs":[]},{"type":"function","stateMutability":"view","outputs":[{"type":"uint256","name":"count","internalType":"uint256"}],"name":"lockCount","inputs":[{"type":"address","name":"_tokenAddress","internalType":"address"}]},{"type":"function","stateMutability":"view","outputs":[{"type":"tuple","name":"lock","internalType":"struct TimeCapsuleContext.Lock","components":[{"type":"uint8"},{"type":"bytes32"},{"type":"uint64"},{"type":"uint64"},{"type":"uint64"},{"type":"uint8"},{"type":"bytes6"},{"type":"uint256"},{"type":"uint256"},{"type":"uint256"}]}],"name":"lockData","inputs":[{"type":"address","name":"_tokenAddress","internalType":"address"},{"type":"uint256","name":"_index","internalType":"uint256"}]},{"type":"function","stateMutability":"view","outputs":[{"type":"address","name":"","internalType":"address"}],"name":"owner","inputs":[]},{"type":"function","stateMutability":"nonpayable","outputs":[],"name":"panic","inputs":[]},{"type":"function","stateMutability":"nonpayable","outputs":[],"name":"panic","inputs":[{"type":"bytes","name":"_signature","internalType":"bytes"}]},{"type":"function","stateMutability":"nonpayable","outputs":[],"name":"recoverExpiredVault","inputs":[{"type":"address","name":"_originalOwner","internalType":"address"}]},{"type":"function","stateMutability":"nonpayable","outputs":[],"name":"recoverVault","inputs":[{"type":"address","name":"_originalOwner","internalType":"address"}]},{"type":"function","stateMutability":"nonpayable","outputs":[],"name":"sendWithdrawal","inputs":[{"type":"address","name":"_tokenAddress","internalType":"address"},{"type":"uint256","name":"_lockIndex","internalType":"uint256"}]},{"type":"function","stateMutability":"nonpayable","outputs":[],"name":"validateRecoveryAddressHash","inputs":[{"type":"bytes32","name":"_recoveryAddressHash","internalType":"bytes32"},{"type":"bytes","name":"_signature","internalType":"bytes"}]},{"type":"function","stateMutability":"view","outputs":[{"type":"uint8","name":"status","internalType":"enum TimeCapsuleContext.VaultStatus"}],"name":"vaultStatus","inputs":[]},{"type":"event","name":"Initialized","inputs":[{"type":"uint8","name":"version","indexed":false}],"anonymous":false},{"type":"event","name":"LockReleased","inputs":[{"type":"address","name":"tokenAddress","indexed":false},{"type":"uint256","name":"lockIndex","indexed":false}],"anonymous":false},{"type":"event","name":"Locked","inputs":[{"type":"bytes32","name":"tag","indexed":false},{"type":"address","name":"tokenAddress","indexed":false},{"type":"uint256","name":"lockIndex","indexed":false},{"type":"uint256","name":"amount","indexed":false},{"type":"uint256","name":"fee","indexed":false},{"type":"uint256","name":"lockTime","indexed":false},{"type":"uint256","name":"unlockTime","indexed":false},{"type":"uint8","name":"lockType","indexed":false}],"anonymous":false},{"type":"event","name":"Recovered","inputs":[],"anonymous":false},{"type":"event","name":"RecoveryInitiated","inputs":[],"anonymous":false},{"type":"event","name":"Withdrawal","inputs":[{"type":"address","name":"tokenAddress","indexed":false},{"type":"uint256","name":"lockIndex","indexed":false},{"type":"uint256","name":"amount","indexed":false}],"anonymous":false},{"type":"event","name":"WithdrawalCancelled","inputs":[{"type":"address","name":"tokenAddress","indexed":false},{"type":"uint256","name":"lockIndex","indexed":false}],"anonymous":false},{"type":"event","name":"WithdrawalInitiated","inputs":[{"type":"address","name":"tokenAddress","indexed":false},{"type":"uint256","name":"lockIndex","indexed":false},{"type":"uint256","name":"amount","indexed":false}],"anonymous":false},{"type":"error","name":"HardLocked","inputs":[{"type":"uint64","name":"unlockTime","internalType":"uint64"}]},{"type":"error","name":"InsufficientFunds","inputs":[{"type":"uint256","name":"grossBalance","internalType":"uint256"},{"type":"uint256","name":"alreadyLocked","internalType":"uint256"},{"type":"uint256","name":"availableToLock","internalType":"uint256"}]},{"type":"receive"}]
Contract Creation Code
0x3d602d80600a3d3981f3363d3d373d3d3d363d735fe3543f96c7aa1a85d82206ccedc80dc2722d005af43d82803e903d91602b57fd5bf3
Deployed ByteCode
0x363d3d373d3d3d363d735fe3543f96c7aa1a85d82206ccedc80dc2722d005af43d82803e903d91602b57fd5bf3