Transaction Hash:
Block:
18325328 at Oct-11-2023 06:09:59 AM +UTC
Transaction Fee:
0.00033551526794292 ETH
$0.77
Gas Used:
61,080 Gas / 5.493046299 Gwei
Emitted Events:
314 |
TrainingGround.Claim( metaToken=1766847064778384329583297500742918515827483896875618958121606201292623479, programme=16, level=3 )
|
315 |
TrainingGround.StartTraining( metaToken=1766847064778384329583297500742918515827483896875618958121606201292623479, programme=5, level=0 )
|
Account State Difference:
Address | Before | After | State Difference | ||
---|---|---|---|---|---|
0x6015C527...b64bf4b7F |
1.654213764079436809 Eth
Nonce: 16
|
1.653878248811493889 Eth
Nonce: 17
| 0.00033551526794292 | ||
0x95222290...5CC4BAfe5
Miner
| (beaverbuild) | 6.356324137898963528 Eth | 6.356330245898963528 Eth | 0.000006108 | |
0xd9e6db06...72f4cAFcC |
Execution Trace
TrainingGround.switchProgramme( tokenAddress=0x5d2BF3b4264EFADE95fC89348f1367fCa0552861, tokenId=3703, programme=5 )
-
JumpPort.isDeposited( tokenAddress=0x5d2BF3b4264EFADE95fC89348f1367fCa0552861, tokenId=3703 ) => ( True )
-
JumpPort.ownerOf( tokenAddress=0x5d2BF3b4264EFADE95fC89348f1367fCa0552861, tokenId=3703 ) => ( owner=0x6015C5273A9730A4c4823230AFB2a3ab64bf4b7F )
switchProgramme[TrainingGround (ln:243)]
getMetaToken[TrainingGround (ln:250)]
claimLevel[TrainingGround (ln:255)]
getClaimedLevel[TrainingGround (ln:281)]
getCurrentLevel[TrainingGround (ln:282)]
getTrainingStatus[TrainingGround (ln:163)]
Claim[TrainingGround (ln:286)]
_startProgramme[TrainingGround (ln:257)]
getClaimedLevel[TrainingGround (ln:205)]
StartTraining[TrainingGround (ln:216)]
File 1 of 2: TrainingGround
File 2 of 2: JumpPort
// SPDX-License-Identifier: AGPL-3.0 pragma solidity ^0.8.9; import {Portal} from "./Portal.sol"; interface ILegionFighter { function isCore(uint256 tokenId) external view returns (bool, uint256); function Equipped(uint256 tokenId) external view returns (uint8); } contract TrainingGround is Portal { bool public trainingActive = false; struct Permission { uint8 allowLevel; uint248 index; } mapping(address => Permission) allowedCollections; // collection address => Permission struct mapping(uint256 => address) allowedCollectionsLookup; // allowedCollections index => collection address (reverse lookup) uint256 allowedCollectionsCount = 0; struct State { uint8 status; // 0 or 1 - Boolean for whether currently training uint8 programme; // Programme Id uint208 progress; // Progress (104 x 2 bits) uint32 startBlock; // Block started training in current programme } mapping(uint256 => uint32[3]) public durationsByProgramme; // programme id => durations[] mapping(uint256 => uint256) public participantsByProgramme; // programme id => count of participants uint256 public programmesCount = 0; mapping(uint256 => State) stateByMetaToken; // metaToken => State struct event NewProgramme(uint8 indexed programme, uint32[3] durations); event StartTraining(uint256 indexed metaToken, uint8 indexed programme, uint8 level); event Claim(uint256 indexed metaToken, uint8 indexed programme, uint8 level); constructor(address jumpPortAddress) Portal(jumpPortAddress) {} /* Helper / View functions */ /** * @dev Get a metaToken (a unique id representing a specific tokenId in a specific collection) * @param tokenAddress the collection contract address * @param tokenId the fighter tokenId */ function getMetaToken(address tokenAddress, uint256 tokenId) public view returns (uint256 metaToken) { uint256 tokenAddressIndex = uint256(allowedCollections[tokenAddress].index); metaToken = (tokenAddressIndex << 240) | tokenId; } /** * @dev Get the contract address and tokenId of a given metaToken * @param metaToken a unique id representing a specific tokenId in a specific collection */ function getTokenDetails(uint256 metaToken) public view returns (address tokenAddress, uint256 tokenId) { uint256 tokenAddressIndex = metaToken >> 240; tokenAddress = allowedCollectionsLookup[tokenAddressIndex]; tokenId = metaToken & 0x0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; } /** * @dev Check whether a given collection address is allowed into Training Ground */ function isAllowed(address tokenAddress) external view returns (bool allowed) { return allowedCollections[tokenAddress].allowLevel > 0; } /** * @dev Check whether a given fighter is currently training (by metaToken) */ function isTraining(uint256 metaToken) public view returns (bool training) { return stateByMetaToken[metaToken].status > 0; } /** * @dev Check whether a given fighter is currently training (by contract address and tokenId) */ function isTraining(address tokenAddress, uint256 tokenId) public view returns (bool training) { return isTraining(getMetaToken(tokenAddress, tokenId)); } /** * @dev Get the full progress (represented by a uint208) of a given fighter (by metaToken) */ function getFullProgress(uint256 metaToken) public view returns (uint208 progress) { return stateByMetaToken[metaToken].progress; } /** * @dev Get the full progress (represented by a uint208) of a given fighter (by contract address and tokenId) */ function getFullProgress(address tokenAddress, uint256 tokenId) public view returns (uint208 progress) { return getFullProgress(getMetaToken(tokenAddress, tokenId)); } /** * @dev Get a count of each level achieved for a given fighter (by metaToken) * Returns level counts as named Taunts, Fighting styles and Combos */ function getSkillCounts(uint256 metaToken) public view returns ( uint256 taunts, uint256 styles, uint256 combos ) { unchecked { State storage state = stateByMetaToken[metaToken]; for (uint8 i = 1; i <= programmesCount; i++) { uint256 level = uint8(state.progress >> (208 - (i * 2))) & 3; if (level >= 1) { taunts++; } if (level >= 2) { styles++; } if (level == 3) { combos++; } } } } /** * @dev Get a count of each level achieved for a given fighter (by contract address and tokenId) * Returns level counts as named Taunts, Fighting styles and Combos */ function getSkillCounts(address tokenAddress, uint256 tokenId) public view returns ( uint256 taunts, uint256 styles, uint256 combos ) { return getSkillCounts(getMetaToken(tokenAddress, tokenId)); } /** * @dev Get the status of a fighter during training in their current programme (by metaToken) * Returns the current programme id, the block they started and the duration */ function getTrainingStatus(uint256 metaToken) public view returns ( uint8 programme, uint32 startBlock, uint32 duration ) { require(stateByMetaToken[metaToken].status > 0, "Token is not training"); State storage state = stateByMetaToken[metaToken]; programme = state.programme; startBlock = state.startBlock; duration = uint32(block.number - uint256(state.startBlock)); } /** * @dev Get the status of a fighter during training in their current programme (by contract address and tokenId) * Returns the current programme id, the block they started and the duration */ function getTrainingStatus(address tokenAddress, uint256 tokenId) public view returns ( uint8 programme, uint32 startBlock, uint32 duration ) { return getTrainingStatus(getMetaToken(tokenAddress, tokenId)); } /** * @dev Get the current level achieved by a fighter in their current programme (by metaToken) */ function getCurrentLevel(uint256 metaToken) public view returns (uint8 level) { unchecked { (uint8 programme, , uint32 blocksDuration) = getTrainingStatus(metaToken); uint32[3] storage durations = durationsByProgramme[programme]; if (durations[2] <= blocksDuration) { return 3; } else if (durations[1] <= blocksDuration) { return 2; } else if (durations[0] <= blocksDuration) { return 1; } else { return 0; } } } /** * @dev Get the current level achieved by a fighter in their current programme (by contract address and tokenId) */ function getCurrentLevel(address tokenAddress, uint256 tokenId) public view returns (uint8 level) { level = getCurrentLevel(getMetaToken(tokenAddress, tokenId)); } /** * @dev Get the level claimed by a fighter in a given programme (by metaToken) */ function getClaimedLevel(uint256 metaToken, uint8 programme) public view returns (uint8 level) { State storage state = stateByMetaToken[metaToken]; level = uint8(state.progress >> (208 - (programme * 2))) & 3; } /** * @dev Get the level claimed by a fighter in a given programme (by contract address and tokenId) */ function getClaimedLevel( address tokenAddress, uint256 tokenId, uint8 programme ) public view returns (uint8 level) { return getClaimedLevel(getMetaToken(tokenAddress, tokenId), programme); } /* Programme actions */ /** * @dev Internal helper function used for starting a fighter on a new programme */ function _startProgramme(uint256 metaToken, uint8 programme) internal { State storage state = stateByMetaToken[metaToken]; uint8 startLevel = getClaimedLevel(metaToken, programme); require(startLevel < 3, "Programme already completed"); state.programme = programme; unchecked { state.startBlock = uint32(block.number); if (startLevel > 0) { uint32[3] storage durations = durationsByProgramme[programme]; state.startBlock = uint32(block.number) - durations[startLevel - 1]; } participantsByProgramme[programme]++; } emit StartTraining(metaToken, programme, startLevel); } /** * @dev Join a fighter onto a training programme * @param tokenAddress the collection contract address * @param tokenId the fighter tokenId * @param programme the id of the programme to join */ function joinProgramme( address tokenAddress, uint256 tokenId, uint8 programme ) public isActive onlyOperator(tokenAddress, tokenId) tokenAllowed(tokenAddress, tokenId) { require(programme > 0 && programme <= programmesCount, "Programme does not exist"); uint256 metaToken = getMetaToken(tokenAddress, tokenId); State storage state = stateByMetaToken[metaToken]; require(state.status == 0, "Already training"); _startProgramme(metaToken, programme); state.status = 1; JumpPort.lockToken(tokenAddress, tokenId); } /** * @dev Switch a fighter onto a different training programme * @param tokenAddress the collection contract address * @param tokenId the fighter tokenId * @param programme the id of the programme to join */ function switchProgramme( address tokenAddress, uint256 tokenId, uint8 programme ) public isActive onlyOperator(tokenAddress, tokenId) { require(allowedCollections[tokenAddress].allowLevel > 0, "Token not allowed"); require(programme > 0 && programme <= programmesCount, "Programme does not exist"); uint256 metaToken = getMetaToken(tokenAddress, tokenId); State storage state = stateByMetaToken[metaToken]; uint8 currentProgramme = state.programme; require(state.status == 1, "Token is not training"); require(currentProgramme != programme, "Token is already in the programme"); claimLevel(metaToken); participantsByProgramme[currentProgramme]--; _startProgramme(metaToken, programme); } /** * @dev Remove a fighter from their current programme * @param tokenAddress the collection contract address * @param tokenId the fighter tokenId */ function leaveCurrentProgramme(address tokenAddress, uint256 tokenId) public onlyOperator(tokenAddress, tokenId) { uint256 metaToken = getMetaToken(tokenAddress, tokenId); State storage state = stateByMetaToken[metaToken]; require(state.status == 1, "Token is not training"); claimLevel(metaToken); participantsByProgramme[state.programme]--; state.status = 0; state.programme = 0; JumpPort.unlockToken(tokenAddress, tokenId); } /** * @dev Claim a level achieved for a given fighter in their current programme (by metaToken) * This function is public so can be called anytime by anyone if they wish to pay the gas */ function claimLevel(uint256 metaToken) public { State storage state = stateByMetaToken[metaToken]; uint8 currentProgramme = state.programme; uint8 claimedLevel = getClaimedLevel(metaToken, currentProgramme); uint8 currentLevel = getCurrentLevel(metaToken); if (currentLevel > claimedLevel) { uint208 mask = uint208(claimedLevel ^ currentLevel) << (208 - currentProgramme * 2); state.progress = (state.progress ^ mask); emit Claim(metaToken, currentProgramme, currentLevel); } } /** * @dev Claim a level achieved for a given fighter in their current programme (by contract address and tokenId) * This function is public so can be called anytime by anyone if they wish to pay the gas */ function claimLevel(address tokenAddress, uint256 tokenId) public { claimLevel(getMetaToken(tokenAddress, tokenId)); } /* Modifiers */ /** * @dev Prevent execution if training is not currently active */ modifier isActive() { require(trainingActive == true, "Training not active"); _; } /** * @dev Prevent execution if the specified token is not in the JumpPort or msg.sender is not owner or approved */ modifier onlyOperator(address tokenAddress, uint256 tokenId) { require(JumpPort.isDeposited(tokenAddress, tokenId) == true, "Token not in JumpPort"); address tokenOwner = JumpPort.ownerOf(tokenAddress, tokenId); require(tokenOwner == msg.sender || JumpPort.getApproved(tokenAddress, tokenId) == msg.sender || JumpPort.isApprovedForAll(tokenOwner, msg.sender) == true, "Not an operator of that token"); _; } /** * @dev Prevent execution if the Legion like fighter is a core or is not equipped */ modifier tokenAllowed(address tokenAddress, uint256 tokenId) { require(allowedCollections[tokenAddress].allowLevel > 0, "Token not allowed"); ILegionFighter LF = ILegionFighter(tokenAddress); (bool core, ) = LF.isCore(tokenId); require(core == false, "Not a Legion Fighter"); require(LF.Equipped(tokenId) > 0, "Fighter not equipped"); _; } /* Administration */ /** * @dev Toggle training active state * @param active desired state of training active (true/false) */ function setTraining(bool active) external onlyRole(ADMIN_ROLE) { trainingActive = active; } /** * @dev Add a new token collection to the allowed list * @param tokenAddress the collection contract address */ function addAllowedCollection(address tokenAddress) external onlyRole(ADMIN_ROLE) { require(allowedCollections[tokenAddress].index == 0, "Collection permissions already exist"); allowedCollectionsCount++; allowedCollections[tokenAddress] = Permission(1, uint248(allowedCollectionsCount)); allowedCollectionsLookup[allowedCollectionsCount] = tokenAddress; } /** * @dev Update a token collections permission level * @param tokenAddress the collection contract address * @param allowLevel an integer (0-255) for allowed permission level. Anything greater than 0 is allowed. */ function updateAllowedCollection(address tokenAddress, uint8 allowLevel) external onlyRole(ADMIN_ROLE) { require(allowedCollections[tokenAddress].index > 0, "Collection permissions do not exist"); allowedCollections[tokenAddress].allowLevel = allowLevel; } /** * @dev Add a new programme with associated level durations * @param durations an array of block heights for each level duration */ function addProgramme(uint32[3] calldata durations) public onlyRole(ADMIN_ROLE) { require(durations[1] > durations[0] && durations[2] > durations[1], "Durations not in ascending order"); require(programmesCount < 104, "Max programmes exist"); programmesCount++; durationsByProgramme[programmesCount] = durations; emit NewProgramme(uint8(programmesCount), durations); } /** * @dev Add a batch of new programmes with associated level durations * @param programmes an array of programme duration arrays (block heights for each level duration) */ function addProgrammes(uint32[3][] calldata programmes) external onlyRole(ADMIN_ROLE) { for (uint8 i = 0; i < programmes.length; i++) { addProgramme(programmes[i]); } } /** * @dev Update an existing programmes level durations * @param programme the id of the programme * @param durations an array of block heights for each level duration */ function updateProgramme(uint8 programme, uint32[3] calldata durations) external onlyRole(ADMIN_ROLE) { require(programme > 0 && programme <= programmesCount, "Programme does not exist"); require(durations[1] > durations[0] && durations[2] > durations[1], "Durations not in ascending order"); durationsByProgramme[programme] = durations; } /** * @dev Eject a fighter from their current programme * @param tokenAddress the collection contract address * @param tokenId the fighter tokenId */ function ejectFighter(address tokenAddress, uint256 tokenId) external onlyRole(ADMIN_ROLE) { uint256 metaToken = getMetaToken(tokenAddress, tokenId); State storage state = stateByMetaToken[metaToken]; require(state.status == 1, "Token is not training"); participantsByProgramme[state.programme]--; state.status = 0; state.programme = 0; JumpPort.unlockToken(tokenAddress, tokenId); } } // SPDX-License-Identifier: AGPL-3.0 pragma solidity ^0.8.9; import {OwnableBase} from "./OwnableBase.sol"; interface IJumpPort { function ownerOf(address tokenAddress, uint256 tokenId) external view returns (address owner); function isDeposited(address tokenAddress, uint256 tokenId) external view returns (bool); function getApproved(address tokenAddress, uint256 tokenId) external view returns (address copilot); function isApprovedForAll(address owner, address operator) external view returns (bool); function lockToken(address tokenAddress, uint256 tokenId) external; function unlockToken(address tokenAddress, uint256 tokenId) external; function unlockAllTokens(bool isOverridden) external; function blockExecution(bool isBlocked) external; } abstract contract Portal is OwnableBase { IJumpPort public JumpPort; bytes32 public constant UNLOCK_ROLE = keccak256("UNLOCK_ROLE"); constructor(address jumpPortAddress) { JumpPort = IJumpPort(jumpPortAddress); } /** * @dev Allow current administrators to be able to grant/revoke unlock role to other addresses. */ function setUnlockRole(address account, bool canUnlock) public onlyRole(ADMIN_ROLE) { roles[UNLOCK_ROLE][account] = canUnlock; emit RoleChange(UNLOCK_ROLE, account, canUnlock, msg.sender); } /** * @dev Mark locks held by this portal as void or not. * Allows for portals to have a degree of self-governance; if the administrator(s) of a portal * realize something is wrong and wish to allow all tokens locked by that portal as void, they're * able to indicate that to the JumpPort, without needing to invlove JumpPort governance. */ function unlockAllTokens(bool isOverridden) public onlyRole(ADMIN_ROLE) { JumpPort.unlockAllTokens(isOverridden); } /** * @dev Prevent this Portal from calling `executeAction` on the JumpPort. * Intended to be called in the situation of a large failure of an individual Portal's operation, * as a way for the Portal itself to indicate it has failed, and arbitrary contract calls should not * be allowed to originate from it. * * This function only allows Portals to enable/disable their own execution right. */ function blockExecution(bool isBlocked) public onlyRole(ADMIN_ROLE) { JumpPort.blockExecution(isBlocked); } } // SPDX-License-Identifier: AGPL-3.0 pragma solidity ^0.8.9; interface IReverseResolver { function claim(address owner) external returns (bytes32); } interface IERC20 { function balanceOf(address account) external view returns (uint256); function transfer(address recipient, uint256 amount) external returns (bool); } interface IERC721 { function safeTransferFrom( address from, address to, uint256 tokenId ) external; } interface IDocumentationRepository { function doc(address contractAddress) external view returns ( string memory name, string memory description, string memory details ); } error MissingRole(bytes32 role, address operator); abstract contract OwnableBase { bytes32 public constant ADMIN_ROLE = 0x00; mapping(bytes32 => mapping(address => bool)) internal roles; // role => operator => hasRole mapping(bytes32 => uint256) internal validSignatures; // message hash => expiration block height IDocumentationRepository public DocumentationRepository; event RoleChange(bytes32 indexed role, address indexed account, bool indexed isGranted, address sender); constructor() { roles[ADMIN_ROLE][msg.sender] = true; } function doc() public view returns ( string memory name, string memory description, string memory details ) { return DocumentationRepository.doc(address(this)); } /** * @dev See {ERC1271-isValidSignature}. */ function isValidSignature(bytes32 hash, bytes memory) external view returns (bytes4 magicValue) { if (validSignatures[hash] >= block.number) { return 0x1626ba7e; // bytes4(keccak256("isValidSignature(bytes32,bytes)") } else { return 0xffffffff; } } /** * @dev Inspect whether a specific address has a specific role. */ function hasRole(bytes32 role, address account) public view returns (bool) { return roles[role][account]; } /* Modifiers */ modifier onlyRole(bytes32 role) { if (roles[role][msg.sender] != true) revert MissingRole(role, msg.sender); _; } /* Administration */ /** * @dev Allow current administrators to be able to grant/revoke admin role to other addresses. */ function setAdmin(address account, bool isAdmin) public onlyRole(ADMIN_ROLE) { roles[ADMIN_ROLE][account] = isAdmin; emit RoleChange(ADMIN_ROLE, account, isAdmin, msg.sender); } /** * @dev Claim ENS reverse-resolver rights for this contract. * https://docs.ens.domains/contract-api-reference/reverseregistrar#claim-address */ function setReverseResolver(address registrar) public onlyRole(ADMIN_ROLE) { IReverseResolver(registrar).claim(msg.sender); } /** * @dev Update address for on-chain documentation lookup. */ function setDocumentationRepository(address documentationAddress) public onlyRole(ADMIN_ROLE) { DocumentationRepository = IDocumentationRepository(documentationAddress); } /** * @dev Set a message as valid, to be queried by ERC1271 clients. */ function markMessageSigned(bytes32 hash, uint256 expirationLength) public onlyRole(ADMIN_ROLE) { validSignatures[hash] = block.number + expirationLength; } /** * @dev Rescue ERC20 assets sent directly to this contract. */ function withdrawForeignERC20(address tokenContract) public onlyRole(ADMIN_ROLE) { IERC20 token = IERC20(tokenContract); token.transfer(msg.sender, token.balanceOf(address(this))); } /** * @dev Rescue ERC721 assets sent directly to this contract. */ function withdrawForeignERC721(address tokenContract, uint256 tokenId) public virtual onlyRole(ADMIN_ROLE) { IERC721(tokenContract).safeTransferFrom(address(this), msg.sender, tokenId); } function withdrawEth() public onlyRole(ADMIN_ROLE) { payable(msg.sender).transfer(address(this).balance); } }
File 2 of 2: JumpPort
// SPDX-License-Identifier: AGPL-3.0 pragma solidity ^0.8.9; import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; import "./OwnableBase.sol"; interface IERC721Transfers { function safeTransferFrom ( address from, address to, uint256 tokenId ) external; function safeTransferFrom ( address from, address to, uint256 tokenId, bytes calldata data ) external; function transferFrom ( address from, address to, uint256 tokenId ) external; } contract JumpPort is IERC721Receiver, OwnableBase { bool public depositPaused; bytes32 public constant PORTAL_ROLE = keccak256("PORTAL_ROLE"); struct LockRecord { address parentLock; bool isLocked; } /** * @dev Record an Owner's collection balance, and the block it was last updated. * * Stored as less than uint256 values to fit into one storage slot. * * This structure will work until approximately the year 2510840694154672305534315769283066566440942177785607 * (when the block height becomes too large for a uint192), and for oners who don't have more than * 18,446,744,073,709,551,615 items from a single collection deposited. */ struct BalanceRecord { uint64 balance; uint192 blockHeight; } mapping(address => bool) public lockOverride; mapping(address => bool) public executionBlocked; mapping(address => mapping(uint256 => mapping(address => LockRecord))) internal portalLocks; // collection address => token ID => portal address => LockRecord mapping(address => mapping(uint256 => address)) private currentLock; // collection address => token ID => portal address mapping(address => mapping(uint256 => address)) private Owners; // collection address => token ID => owner address mapping(address => mapping(address => BalanceRecord)) private OwnerBalances; // collection address => owner Address => count mapping(address => mapping(uint256 => uint256)) private DepositBlock; // collection address => token ID => block height mapping(address => mapping(uint256 => uint256)) private PingRequestBlock; // collection address => token ID => block height mapping(address => mapping(uint256 => address)) private Copilots; // collection address => token ID => copilot address mapping(address => mapping(address => bool)) private CopilotApprovals; // owner address => copilot address => is approved event Deposit(address indexed owner, address indexed tokenAddress, uint256 indexed tokenId); event Withdraw(address indexed owner, address indexed tokenAddress, uint256 indexed tokenId, uint256 duration); event Approval(address indexed owner, address indexed approved, address indexed tokenAddress, uint256 tokenId); event ApprovalForAll(address indexed owner, address indexed operator, bool approved); event Lock(address indexed portalAddress, address indexed owner, address indexed tokenAddress, uint256 tokenId); event Unlock(address indexed portalAddress, address indexed owner, address indexed tokenAddress, uint256 tokenId); event ActionExecuted(address indexed tokenAddress, uint256 indexed tokenId, address target, bytes data); constructor (address documentationAddress) OwnableBase(documentationAddress) {} /* Deposit Tokens */ /** * @dev Receive a token directly; transferred with the `safeTransferFrom` method of another ERC721 token. * @param operator the _msgSender of the transaction * @param from the address of the former owner of the incoming token * @param tokenId the ID of the incoming token * @param data additional metdata */ function onERC721Received ( address operator, address from, uint256 tokenId, bytes calldata data ) public override whenDepositNotPaused returns (bytes4) { Owners[msg.sender][tokenId] = from; unchecked { OwnerBalances[msg.sender][from].balance++; OwnerBalances[msg.sender][from].blockHeight = uint192(block.number); } DepositBlock[msg.sender][tokenId] = block.number; PingRequestBlock[msg.sender][tokenId] = 0; emit Deposit(from, msg.sender, tokenId); return IERC721Receiver.onERC721Received.selector; } /** * @dev Deposit an individual token from a specific collection. * To be successful, the JumpPort contract must be "Approved" to move this token on behalf * of the current owner, in the token's contract. */ function deposit (address tokenAddress, uint256 tokenId) public whenDepositNotPaused { IERC721Transfers(tokenAddress).transferFrom(msg.sender, address(this), tokenId); Owners[tokenAddress][tokenId] = msg.sender; unchecked { OwnerBalances[tokenAddress][msg.sender].balance++; OwnerBalances[tokenAddress][msg.sender].blockHeight = uint192(block.number); } DepositBlock[tokenAddress][tokenId] = block.number; PingRequestBlock[tokenAddress][tokenId] = 0; emit Deposit(msg.sender, tokenAddress, tokenId); } /** * @dev Deposit multiple tokens from a single collection. * To be successful, the JumpPort contract must be "Approved" to move these tokens on behalf * of the current owner, in the token's contract. */ function deposit (address tokenAddress, uint256[] calldata tokenIds) public { unchecked { for (uint256 i; i < tokenIds.length; i++) { deposit(tokenAddress, tokenIds[i]); } } } /** * @dev Deposit multiple tokens from multiple different collections. * To be successful, the JumpPort contract must be "Approved" to move these tokens on behalf * of the current owner, in the token's contract. */ function deposit (address[] calldata tokenAddresses, uint256[] calldata tokenIds) public { require(tokenAddresses.length == tokenIds.length, "Mismatched inputs"); unchecked { for (uint256 i; i < tokenIds.length; i++) { deposit(tokenAddresses[i], tokenIds[i]); } } } /* Withdraw Tokens */ /** * @dev Internal helper function that clears out the tracked metadata for the token. * Does not do any permission checks, and does not do the actual transferring of the token. */ function _withdraw (address tokenAddress, uint256 tokenId) internal { address currentOwner = Owners[tokenAddress][tokenId]; emit Withdraw( currentOwner, tokenAddress, tokenId, block.number - DepositBlock[tokenAddress][tokenId] ); unchecked { OwnerBalances[tokenAddress][currentOwner].balance--; OwnerBalances[tokenAddress][currentOwner].blockHeight = uint192(block.number); } Owners[tokenAddress][tokenId] = address(0); DepositBlock[tokenAddress][tokenId] = 0; Copilots[tokenAddress][tokenId] = address(0); } /** * @dev Withdraw a token, to the owner's address, using `safeTransferFrom`, with no additional data. */ function safeWithdraw (address tokenAddress, uint256 tokenId) public isPilot(tokenAddress, tokenId) withdrawAllowed(tokenAddress, tokenId) { address ownerAddress = Owners[tokenAddress][tokenId]; _withdraw(tokenAddress, tokenId); IERC721Transfers(tokenAddress).safeTransferFrom(address(this), ownerAddress, tokenId); } /** * @dev Withdraw a token, to the owner's address, using `safeTransferFrom`, with additional calldata. */ function safeWithdraw (address tokenAddress, uint256 tokenId, bytes calldata data) public isPilot(tokenAddress, tokenId) withdrawAllowed(tokenAddress, tokenId) { address ownerAddress = Owners[tokenAddress][tokenId]; _withdraw(tokenAddress, tokenId); IERC721Transfers(tokenAddress).safeTransferFrom(address(this), ownerAddress, tokenId, data); } /** * @dev Bulk withdraw multiple tokens, using `safeTransferFrom`, with no additional data. */ function safeWithdraw (address[] calldata tokenAddresses, uint256[] calldata tokenIds) public { require(tokenAddresses.length == tokenIds.length, "Inputs mismatched"); for(uint256 i = 0; i < tokenAddresses.length; i++) { safeWithdraw(tokenAddresses[i], tokenIds[i]); } } /** * @dev Bulk withdraw multiple tokens, using `safeTransferFrom`, with additional calldata. */ function safeWithdraw (address[] calldata tokenAddresses, uint256[] calldata tokenIds, bytes[] calldata data) public { require(tokenAddresses.length == tokenIds.length, "Inputs mismatched"); for(uint256 i = 0; i < tokenAddresses.length; i++) { safeWithdraw(tokenAddresses[i], tokenIds[i], data[i]); } } /** * @dev Withdraw a token, to the owner's address, using `transferFrom`. * USING `transferFrom` RATHER THAN `safeTransferFrom` COULD RESULT IN LOST TOKENS. USE `safeWithdraw` FUNCTIONS * WHERE POSSIBLE, OR DOUBLE-CHECK RECEIVING ADDRESSES CAN HOLD TOKENS IF USING THIS FUNCTION. */ function withdraw (address tokenAddress, uint256 tokenId) public isPilot(tokenAddress, tokenId) withdrawAllowed(tokenAddress, tokenId) { address ownerAddress = Owners[tokenAddress][tokenId]; _withdraw(tokenAddress, tokenId); IERC721Transfers(tokenAddress).transferFrom(address(this), ownerAddress, tokenId); } /** * @dev Bulk withdraw multiple tokens, to specific addresses, using `transferFrom`. * USING `transferFrom` RATHER THAN `safeTransferFrom` COULD RESULT IN LOST TOKENS. USE `safeWithdraw` FUNCTIONS * WHERE POSSIBLE, OR DOUBLE-CHECK RECEIVING ADDRESSES CAN HOLD TOKENS IF USING THIS FUNCTION. */ function withdraw (address[] calldata tokenAddresses, uint256[] calldata tokenIds) public { require(tokenAddresses.length == tokenIds.length, "Inputs mismatched"); for(uint256 i = 0; i < tokenAddresses.length; i++) { withdraw(tokenAddresses[i], tokenIds[i]); } } /** * @dev Designate another address that can act on behalf of this token. * This allows the Copliot address to withdraw the token from the JumpPort, and interact with any Portal * on behalf of this token. */ function setCopilot (address copilot, address tokenAddress, uint256 tokenId) public { require(Owners[tokenAddress][tokenId] == msg.sender, "Not the owner of that token"); require(msg.sender != copilot, "approve to caller"); Copilots[tokenAddress][tokenId] = copilot; emit Approval(msg.sender, copilot, tokenAddress, tokenId); } /** * @dev Designate another address that can act on behalf of all tokens owned by the sender's address. * This allows the Copliot address to withdraw the token from the JumpPort, and interact with any Portal * on behalf of any token owned by the sender. */ function setCopilotForAll (address copilot, bool approved) public { require(msg.sender != copilot, "approve to caller"); CopilotApprovals[msg.sender][copilot] = approved; emit ApprovalForAll(msg.sender, copilot, approved); } /* Receive actions from Portals */ /** * @dev Lock a token into the JumpPort. * Causes a token to not be able to be withdrawn by its Owner, until the same Portal contract calls the `unlockToken` function for it, * or the locks for that portal are all marked as invalid (either by JumpPort administrators, or the Portal itself). */ function lockToken (address tokenAddress, uint256 tokenId) public tokenDeposited(tokenAddress, tokenId) onlyRole(PORTAL_ROLE) { if (portalLocks[tokenAddress][tokenId][msg.sender].isLocked) return; // Already locked; nothing to do // Check if this lock is already in the chain of "active" locks address checkPortal = currentLock[tokenAddress][tokenId]; while (checkPortal != address(0)) { if (checkPortal == msg.sender) { // This portal is already in the chain of active locks portalLocks[tokenAddress][tokenId][msg.sender].isLocked = true; emit Lock(msg.sender, Owners[tokenAddress][tokenId], tokenAddress, tokenId); return; } checkPortal = portalLocks[tokenAddress][tokenId][checkPortal].parentLock; } // Looped through all active locks and didn't find this Portal. So, add it as the new head portalLocks[tokenAddress][tokenId][msg.sender] = LockRecord(currentLock[tokenAddress][tokenId], true); currentLock[tokenAddress][tokenId] = msg.sender; emit Lock(msg.sender, Owners[tokenAddress][tokenId], tokenAddress, tokenId); } /** * @dev Unlocks a token held in the JumpPort. * Does not withdraw the token from the JumpPort, but makes it available for withdraw whenever the Owner wishes to. */ function unlockToken (address tokenAddress, uint256 tokenId) public tokenDeposited(tokenAddress, tokenId) onlyRole(PORTAL_ROLE) { portalLocks[tokenAddress][tokenId][msg.sender].isLocked = false; emit Unlock(msg.sender, Owners[tokenAddress][tokenId], tokenAddress, tokenId); if (!isLocked(tokenAddress, tokenId)) { currentLock[tokenAddress][tokenId] = address(0); } } /** * @dev Take an action as the JumpPort (the owner of the tokens within it), as directed by a Portal. * This is a powerful function and Portals that wish to use it NEEDS TO MAKE SURE its execution is guarded by * checks to ensure the address passed as `operator` to this function is the one authorizing the action * (in most cases, it should be the `msg.sender` communicating to the Portal), and that the `payload` * being passed in operates on the `tokenId` indicated, and no other tokens. * * Here on the JumpPort side, it verifies that the `operator` passed in is the current owner or a Copilot of the * token being operated upon, but has to trust the Portal that the passed-in `tokenId` matches what token * will get acted upon in the `payload`. */ function executeAction (address operator, address tokenAddress, uint256 tokenId, address targetAddress, bytes calldata payload) public payable tokenDeposited(tokenAddress, tokenId) onlyRole(PORTAL_ROLE) returns(bytes memory result) { require(executionBlocked[msg.sender] == false, "Execution blocked for that Portal"); // Check if operator is allowed to act for this token address owner = Owners[tokenAddress][tokenId]; require( operator == owner || operator == Copilots[tokenAddress][tokenId] || CopilotApprovals[owner][operator] == true, "Not an operator of that token" ); // Make the external call (bool success, bytes memory returnData) = targetAddress.call{ value: msg.value }(payload); if (success == false) { if (returnData.length == 0) { revert("Executing action on other contract failed"); } else { assembly { revert(add(32, returnData), mload(returnData)) } } } else { emit ActionExecuted(tokenAddress, tokenId, targetAddress, payload); return returnData; } } /** * @dev Unlocks all locks held by a Portal. * Intended to be called in the situation of a large failure of an individual Portal's operation, * as a way for the Portal itself to indicate it has failed, and all tokens that were previously * locked by it should be allowed to exit. * * This function only allows Portals to enable/disable the locks they created. The function `setAdminLockOverride` * is similar, but allows JumpPort administrators to set/clear the lock ability for any Portal contract. */ function unlockAllTokens (bool isOverridden) public onlyRole(PORTAL_ROLE) { lockOverride[msg.sender] = isOverridden; } /** * @dev Prevent a Portal from executing calls to other contracts. * Intended to be called in the situation of a large failure of an individual Portal's operation, * as a way for the Portal itself to indicate it has failed, and arbitrary contract calls should not * be allowed to originate from it. * * This function only allows Portals to enable/disable their own execution right. The function `setAdminExecutionBlocked` * is similar, but allows JumpPort administrators to set/clear the execution block for any Portal contract. */ function blockExecution (bool isBlocked) public onlyRole(PORTAL_ROLE) { executionBlocked[msg.sender] = isBlocked; } /* View functions */ /** * @dev Is the specified token currently deposited in the JumpPort? */ function isDeposited (address tokenAddress, uint256 tokenId) public view returns (bool) { return Owners[tokenAddress][tokenId] != address(0); } /** * @dev When was the specified token deposited in the JumpPort? */ function depositedSince (address tokenAddress, uint256 tokenId) public view tokenDeposited(tokenAddress, tokenId) returns (uint256 blockNumber) { blockNumber = DepositBlock[tokenAddress][tokenId]; } /** * @dev Is the specified token currently locked in the JumpPort? * If any Portal contract has a valid lock (the Portal has indicated the token should be locked, * and the Portal's locking rights haven't been overridden) on the token, this function will return true. */ function isLocked (address tokenAddress, uint256 tokenId) public view tokenDeposited(tokenAddress, tokenId) returns (bool) { address checkPortal = currentLock[tokenAddress][tokenId]; while (checkPortal != address(0)) { if (portalLocks[tokenAddress][tokenId][checkPortal].isLocked && lockOverride[checkPortal] == false) return true; checkPortal = portalLocks[tokenAddress][tokenId][checkPortal].parentLock; } return false; } /** * @dev Get a list of all Portal contract addresses that hold a valid lock (the Portal has indicated the token should be locked, * and the Portal's locking rights haven't been overridden) on the token. */ function lockedBy (address tokenAddress, uint256 tokenId) public view returns (address[] memory) { address[] memory lockedRaw = new address[](500); uint256 index = 0; address checkPortal = currentLock[tokenAddress][tokenId]; while (checkPortal != address(0)) { if (portalLocks[tokenAddress][tokenId][checkPortal].isLocked && lockOverride[checkPortal] == false) { lockedRaw[index] = checkPortal; index++; } checkPortal = portalLocks[tokenAddress][tokenId][checkPortal].parentLock; } address[] memory lockedFinal = new address[](index); unchecked { for (uint256 i = 0; i < index; i++) { lockedFinal[i] = lockedRaw[i]; } } return lockedFinal; } /** * @dev Who is the owner of the specified token that is deposited in the JumpPort? * A core tenent of the JumpPort is that this value will not change while the token is deposited; * a token cannot change owners while in the JumpPort, though they can add/remove Copilots. */ function ownerOf (address tokenAddress, uint256 tokenId) public view tokenDeposited(tokenAddress, tokenId) returns (address owner) { owner = Owners[tokenAddress][tokenId]; } /** * @dev Who are the owners of a specified range of tokens in a collection? * Bulk query function, to be able to enumerate a whole token collection more easily on the client end */ function ownersOf (address tokenAddress, uint256 tokenSearchStart, uint256 tokenSearchEnd) public view returns (address[] memory tokenOwners) { unchecked { require(tokenSearchEnd >= tokenSearchStart, "Search parameters out of order"); tokenOwners = new address[](tokenSearchEnd - tokenSearchStart + 1); for (uint256 i = tokenSearchStart; i <= tokenSearchEnd; i++) { tokenOwners[i - tokenSearchStart] = Owners[tokenAddress][i]; } } } /** * @dev For a specified owner address, what tokens in the specified range do they own? * Bulk query function, to be able to enumerate a specific address' collection more easily on the client end */ function ownedTokens (address tokenAddress, address owner, uint256 tokenSearchStart, uint256 tokenSearchEnd) public view returns (uint256[] memory tokenIds) { unchecked { require(tokenSearchEnd >= tokenSearchStart, "Search parameters out of order"); require(owner != address(0), "Balance query for the zero address"); uint256[] memory ownedRaw = new uint256[](tokenSearchEnd - tokenSearchStart); uint256 index = 0; for (uint256 i = tokenSearchStart; i <= tokenSearchEnd; i++) { if (Owners[tokenAddress][i] == owner) { ownedRaw[index] = i; index++; } } uint256[] memory ownedFinal = new uint256[](index); for (uint256 i = 0; i < index; i++) { ownedFinal[i] = ownedRaw[i]; } return ownedFinal; } } /** * @dev For a specific token collection, how many tokens in that collection does a specific owning address own in the JumpPort? */ function balanceOf (address tokenAddress, address owner) public view returns (BalanceRecord memory) { require(owner != address(0), "Balance query for the zero address"); return OwnerBalances[tokenAddress][owner]; } /** * @dev For a specific set of token collections, how many tokens total does a specific owning address own in the JumpPort? * Bulk query function, to be able to enumerate a specific address' collection more easily on the client end */ function balanceOf (address[] calldata tokenAddresses, address owner) public view returns (uint256) { require(owner != address(0), "Balance query for the zero address"); uint256 totalBalance = 0; unchecked { for (uint256 i = 0; i < tokenAddresses.length; i++) { totalBalance += OwnerBalances[tokenAddresses[i]][owner].balance; } } return totalBalance; } /** * @dev For a specific token, which other address is approved to act as the owner of that token for actions pertaining to the JumpPort? */ function getApproved (address tokenAddress, uint256 tokenId) public view tokenDeposited(tokenAddress, tokenId) returns (address copilot) { copilot = Copilots[tokenAddress][tokenId]; } /** * @dev For a specific owner's address, is the specified operator address allowed to act as the owner for actions pertaining to the JumpPort? */ function isApprovedForAll (address owner, address operator) public view returns (bool) { return CopilotApprovals[owner][operator]; } /* Modifiers */ /** * @dev Prevent execution if the specified token is not currently deposited in the JumpPort. */ modifier tokenDeposited (address tokenAddress, uint256 tokenId) { require(Owners[tokenAddress][tokenId] != address(0), "Not currently deposited"); _; } /** * @dev Prevent execution if deposits to the JumpPort overall are paused. */ modifier whenDepositNotPaused () { require(depositPaused == false, "Paused"); _; } /** * @dev Prevent execution if the specified token is locked by any Portal currently. */ modifier withdrawAllowed (address tokenAddress, uint256 tokenId) { require(!isLocked(tokenAddress, tokenId), "Token is locked"); _; } /** * @dev Prevent execution if the transaction sender is not the owner nor Copliot for the specified token. */ modifier isPilot (address tokenAddress, uint256 tokenId) { address owner = Owners[tokenAddress][tokenId]; require( msg.sender == owner || msg.sender == Copilots[tokenAddress][tokenId] || CopilotApprovals[owner][msg.sender] == true, "Not an operator of that token" ); _; } /* Administration */ /** * @dev Add or remove the "Portal" role to a specified address. */ function setPortalValidation (address portalAddress, bool isValid) public onlyRole(ADMIN_ROLE) { roles[PORTAL_ROLE][portalAddress] = isValid; emit RoleChange(PORTAL_ROLE, portalAddress, isValid, msg.sender); } /** * @dev Prevent new tokens from being added to the JumpPort. */ function setPaused (bool isDepositPaused) public onlyRole(ADMIN_ROLE) { depositPaused = isDepositPaused; } /** * @dev As an administrator of the JumpPort contract, set a Portal's locks to be valid or not. * * This function allows JumpPort administrators to set/clear the override for any Portal contract. * The `unlockAllTokens` function is similar (allowing Portal addresses to set/clear lock overrides as well) * but only for their own Portal address. */ function setAdminLockOverride (address portal, bool isOverridden) public onlyRole(ADMIN_ROLE) { lockOverride[portal] = isOverridden; } /** * @dev As an administrator of the JumpPort contract, set a Portal to be able to execute other functions or not. * * This function allows JumpPort administrators to set/clear the execution block for any Portal contract. * The `blockExecution` function is similar (allowing Portal addresses to set/clear lock overrides as well) * but only for their own Portal address. */ function setAdminExecutionBlocked (address portal, bool isBlocked) public onlyRole(ADMIN_ROLE) { executionBlocked[portal] = isBlocked; } /** * @dev Contract owner requesting the owner of a token check in. * This starts the process of the owner of the contract being able to remove any token, after a time delay. * If the current owner does not want the token removed, they have 2,400,000 blocks (about one year) * to trigger the `ownerPong` method, which will abort the withdraw */ function adminWithdrawPing (address tokenAddress, uint256 tokenId) public onlyRole(ADMIN_ROLE) { require(Owners[tokenAddress][tokenId] != address(0), "Token not deposited"); PingRequestBlock[tokenAddress][tokenId] = block.number; } /** * @dev As the owner of a token, abort an attempt to force-remove it. * The owner of the contract can remove any token from the JumpPort, if they trigger the `adminWithdrawPing` function * for that token, and the owner does not respond by calling this function within 2,400,000 blocks (about one year) */ function ownerPong (address tokenAddress, uint256 tokenId) public isPilot(tokenAddress, tokenId) { PingRequestBlock[tokenAddress][tokenId] = 0; } /** * @dev As an Administrator, abort an attempt to force-remove a token. * This is a means for the Administration to change its mind about a force-withdraw, or to correct the actions of a rogue Administrator. */ function adminPong (address tokenAddress, uint256 tokenId) public onlyRole(ADMIN_ROLE) { PingRequestBlock[tokenAddress][tokenId] = 0; } /** * @dev Check if a token has a ping from the contract Administration pending, and if so, what block it was requested at * Returns zero if there is no request pending. */ function tokenPingRequestBlock (address tokenAddress, uint256 tokenId) public view returns (uint256 blockNumber) { return PingRequestBlock[tokenAddress][tokenId]; } /** * @dev Check if a set of tokens have a ping from the contract Administration pending, and if so, what block it was requested at * Returns zero for a token if there is no request pending for that token. */ function tokenPingRequestBlocks (address[] calldata tokenAddresses, uint256[] calldata tokenIds) public view returns(uint256[] memory blockNumbers) { require(tokenAddresses.length == tokenIds.length, "Inputs mismatched"); unchecked { blockNumbers = new uint256[](tokenAddresses.length); for (uint256 i = 0; i < tokenAddresses.length; i++) { blockNumbers[i] = PingRequestBlock[tokenAddresses[i]][tokenIds[i]]; } } } /** * @dev Rescue ERC721 assets sent directly to this contract. */ function withdrawForeignERC721 (address tokenContract, uint256 tokenId) public override onlyRole(ADMIN_ROLE) { if (Owners[tokenContract][tokenId] == address(0)) { // This token got here without being properly recorded; allow withdraw immediately DepositBlock[tokenContract][tokenId] = 0; Copilots[tokenContract][tokenId] = address(0); IERC721(tokenContract).safeTransferFrom( address(this), msg.sender, tokenId ); return; } // This token is deposited into the JumpPort in a valid manner. // Only allow contract-owner withdraw if owner does not respond to ping unchecked { require(PingRequestBlock[tokenContract][tokenId] > 0 && PingRequestBlock[tokenContract][tokenId] < block.number - 2_400_000, "Owner ping has not expired"); } currentLock[tokenContract][tokenId] = address(0); // Remove all locks on this token _withdraw(tokenContract, tokenId); IERC721(tokenContract).safeTransferFrom( address(this), msg.sender, tokenId ); } } // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (token/ERC721/IERC721Receiver.sol) pragma solidity ^0.8.0; /** * @title ERC721 token receiver interface * @dev Interface for any contract that wants to support safeTransfers * from ERC721 asset contracts. */ interface IERC721Receiver { /** * @dev Whenever an {IERC721} `tokenId` token is transferred to this contract via {IERC721-safeTransferFrom} * by `operator` from `from`, this function is called. * * It must return its Solidity selector to confirm the token transfer. * If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted. * * The selector can be obtained in Solidity with `IERC721.onERC721Received.selector`. */ function onERC721Received( address operator, address from, uint256 tokenId, bytes calldata data ) external returns (bytes4); } // SPDX-License-Identifier: AGPL-3.0 pragma solidity ^0.8.9; interface IReverseResolver { function claim (address owner) external returns (bytes32); } interface IERC20 { function balanceOf (address account) external view returns (uint256); function transfer (address recipient, uint256 amount) external returns (bool); } interface IERC721 { function safeTransferFrom (address from, address to, uint256 tokenId ) external; } interface IDocumentationRepository { function doc (address contractAddress) external view returns (string memory name, string memory description, string memory details); } error MissingRole(bytes32 role, address operator); abstract contract OwnableBase { bytes32 public constant ADMIN_ROLE = 0x00; mapping(bytes32 => mapping(address => bool)) internal roles; // role => operator => hasRole mapping(bytes32 => uint256) internal validSignatures; // message hash => expiration block height IDocumentationRepository public DocumentationRepository; event RoleChange (bytes32 indexed role, address indexed account, bool indexed isGranted, address sender); constructor (address documentationAddress) { roles[ADMIN_ROLE][msg.sender] = true; DocumentationRepository = IDocumentationRepository(documentationAddress); } function doc () public view returns (string memory name, string memory description, string memory details) { return DocumentationRepository.doc(address(this)); } /** * @dev See {ERC1271-isValidSignature}. */ function isValidSignature(bytes32 hash, bytes memory) external view returns (bytes4 magicValue) { if (validSignatures[hash] >= block.number) { return 0x1626ba7e; // bytes4(keccak256("isValidSignature(bytes32,bytes)") } else { return 0xffffffff; } } /** * @dev Inspect whether a specific address has a specific role. */ function hasRole (bytes32 role, address account) public view returns (bool) { return roles[role][account]; } /* Modifiers */ modifier onlyRole (bytes32 role) { if (roles[role][msg.sender] != true) revert MissingRole(role, msg.sender); _; } /* Administration */ /** * @dev Allow current administrators to be able to grant/revoke admin role to other addresses. */ function setAdmin (address account, bool isAdmin) public onlyRole(ADMIN_ROLE) { roles[ADMIN_ROLE][account] = isAdmin; emit RoleChange(ADMIN_ROLE, account, isAdmin, msg.sender); } /** * @dev Claim ENS reverse-resolver rights for this contract. * https://docs.ens.domains/contract-api-reference/reverseregistrar#claim-address */ function setReverseResolver (address registrar) public onlyRole(ADMIN_ROLE) { IReverseResolver(registrar).claim(msg.sender); } /** * @dev Update address for on-chain documentation lookup. */ function setDocumentationRepository (address documentationAddress) public onlyRole(ADMIN_ROLE) { DocumentationRepository = IDocumentationRepository(documentationAddress); } /** * @dev Set a message as valid, to be queried by ERC1271 clients. */ function markMessageSigned (bytes32 hash, uint256 expirationLength) public onlyRole(ADMIN_ROLE) { validSignatures[hash] = block.number + expirationLength; } /** * @dev Rescue ERC20 assets sent directly to this contract. */ function withdrawForeignERC20 (address tokenContract) public onlyRole(ADMIN_ROLE) { IERC20 token = IERC20(tokenContract); token.transfer(msg.sender, token.balanceOf(address(this))); } /** * @dev Rescue ERC721 assets sent directly to this contract. */ function withdrawForeignERC721 (address tokenContract, uint256 tokenId) public virtual onlyRole(ADMIN_ROLE) { IERC721(tokenContract).safeTransferFrom( address(this), msg.sender, tokenId ); } function withdrawEth () public onlyRole(ADMIN_ROLE) { payable(msg.sender).transfer(address(this).balance); } }