diff options
8 files changed, 1580 insertions, 81 deletions
diff --git a/contracts/test-utils/src/types.ts b/contracts/test-utils/src/types.ts index d738fcd4e..04f95e1a8 100644 --- a/contracts/test-utils/src/types.ts +++ b/contracts/test-utils/src/types.ts @@ -99,6 +99,7 @@ export enum ContractName { ERC721Proxy = 'ERC721Proxy', DummyERC721Receiver = 'DummyERC721Receiver', DummyERC721Token = 'DummyERC721Token', + DummyYesComplianceToken = 'DummyYesComplianceToken', TestLibBytes = 'TestLibBytes', TestWallet = 'TestWallet', Authorizable = 'Authorizable', diff --git a/packages/contracts/contracts/tokens/YesComplianceToken/IYesComplianceToken.sol b/packages/contracts/contracts/tokens/YesComplianceToken/IYesComplianceToken.sol new file mode 100644 index 000000000..1573c6bac --- /dev/null +++ b/packages/contracts/contracts/tokens/YesComplianceToken/IYesComplianceToken.sol @@ -0,0 +1,118 @@ +pragma solidity ^0.4.24; + + +/** + * @notice an ERC721 "yes" compliance token supporting a collection of country-specific attributions which answer specific + * compliance-related queries with YES. (attestations) + * + * primarily ERC721 is useful for the self-management of claiming addresses. a single token is more useful + * than a non-ERC721 interface because of interop with other 721-supporting systems/ui; it allows users to + * manage their financial stamp with flexibility using a well-established simple concept of non-fungible tokens. + * this interface is for anyone needing to carry around and otherwise manage their proof of compliance. + * + * the financial systems these users authenticate against have a different set of API requirements. they need + * more contextualization ability than a balance check to support distinctions of attestations, as well as geographic + * distinction. these integrations are made simpler as the language of the query more closely match the language of compliance. + * + * this interface describes, beyond 721, these simple compliance-specific interfaces (and their management tools) + * + * notes: + * - no address can be associated with more than one identity (though addresses may have more than token). issuance + * in this circumstance will fail + * - one person or business = one entity + * - one entity may have many tokens across many addresses; they can mint and burn tokens tied to their identity at will + * - two token types: control & non-control. both carry compliance proof + * - control tokens let their holders mint and burn (within the same entity) + * - non-control tokens are solely for compliance queries + * - a lock on the entity is used instead of token revocation to remove the cash burden assumed by a customer to + * redistribute a fleet of coins + * - all country codes should be via ISO-3166-1 + * + * any (non-view) methods not explicitly marked idempotent are not idempotent. + */ +contract YesComplianceTokenV1 /*is ERC721Token*/ /*, ERC165 :should: */ { + + uint256 public constant OWNER_ENTITY_ID = 1; + + uint8 public constant YESMARK_OWNER = 128; + uint8 public constant YESMARK_VALIDATOR = 129; + + /* + todo events: entity updated, destroyed, ???? + Finalized + Attested + + */ + + /** + * @notice query api: returns true if the specified address has the given country/yes attestation. this + * is the primary method partners will use to query the active qualifications of any particular + * address. + */ + function isYes(uint256 _validatorEntityId, address _address, uint16 _countryCode, uint8 _yes) external view returns(bool) ; + + /** @notice same as isYes except as an imperative */ + function requireYes(uint256 _validatorEntityId, address _address, uint16 _countryCode, uint8 _yes) external view ; + + /** + * @notice retrieve all YES marks for an address in a particular country + * @param _validatorEntityId the validator ID to consider. or, use 0 for any of them + * @param _address the validator ID to consider, or 0 for any of them + * @param _countryCode the ISO-3166-1 country code + * @return (non-duplicate) array of YES marks present + */ + function getYes(uint256 _validatorEntityId, address _address, uint16 _countryCode) external view returns(uint8[] /* memory */); + + // function getCountries(uint256 _validatorEntityId, address _address) external view returns(uint16[] /* memory */); + + /** + * @notice create new tokens. fail if _to already + * belongs to a different entity and caller is not validator + * @param _control true if the new token is a control token (can mint, burn). aka NOT limited. + * @param _entityId the entity to mint for, supply 0 to use the entity tied to the caller + * @return the newly created token ID + */ + function mint(address _to, uint256 _entityId, bool _control) external returns (uint256); + + /** @notice shortcut to mint() + setYes() in one call, for a single country */ + function mint(address _to, uint256 _entityId, bool _control, uint16 _countryCode, uint8[] _yes) external returns (uint256); + + /** @notice destroys a specific token */ + function burn(uint256 _tokenId) external; + + /** @notice destroys the entire entity and all tokens */ + function burnEntity(uint256 _entityId) external; + + /** + * @notice adds a specific attestations (yes) to an entity. idempotent: will return normally even if the mark + * was already set by this validator + */ + function setYes(uint256 _entityId, uint16 _countryCode, uint8 _yes) external; + + /** + * @notice removes a attestation(s) from a specific validator for an entity. idempotent + */ + function clearYes(uint256 _entityId, uint16 _countryCode, uint8 _yes) external; + + /** @notice removes all attestations in a given country for a particular entity. idempotent */ + function clearYes(uint256 _entityId, uint16 _countryCode) external; + + /** @notice removes all attestations for a particular entity. idempotent */ + function clearYes(uint256 _entityId) external; + + /** @notice assigns a lock to an entity, rendering all isYes queries false. idempotent */ + function setLocked(uint256 _entityId, bool _lock) external; + + /** @notice checks whether or not a particular entity is locked */ + function isLocked(uint256 _entityId) external view returns(bool); + + /** @notice returns true if the specified token has been finalized (cannot be moved) */ + function isFinalized(uint256 _tokenId) external view returns(bool); + + /** @notice finalizes a token by ID preventing it from getting moved. idempotent */ + function finalize(uint256 _tokenId) external; + + /** @return the entity ID associated with an address (or fail if there is not one) */ + function getEntityId(address _address) external view returns(uint256); + +}
\ No newline at end of file diff --git a/packages/contracts/contracts/tokens/YesComplianceToken/WyreERC721Token/ERC721.sol b/packages/contracts/contracts/tokens/YesComplianceToken/WyreERC721Token/ERC721.sol new file mode 100644 index 000000000..5b4907f13 --- /dev/null +++ b/packages/contracts/contracts/tokens/YesComplianceToken/WyreERC721Token/ERC721.sol @@ -0,0 +1,40 @@ +pragma solidity ^0.4.21; + +import "./ERC721Basic.sol"; + + +/** + * @title ERC-721 Non-Fungible Token Standard, optional enumeration extension + * @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md + */ +contract ERC721Enumerable is ERC721Basic { + function totalSupply() public view returns (uint256); + function tokenOfOwnerByIndex( + address _owner, + uint256 _index + ) + public + view + returns (uint256 _tokenId); + + function tokenByIndex(uint256 _index) public view returns (uint256); +} + + +/** + * @title ERC-721 Non-Fungible Token Standard, optional metadata extension + * @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md + */ +contract ERC721Metadata is ERC721Basic { + function name() external view returns (string _name); + function symbol() external view returns (string _symbol); + function tokenURI(uint256 _tokenId) public view returns (string); +} + + +/** + * @title ERC-721 Non-Fungible Token Standard, full implementation interface + * @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md + */ +contract ERC721 is ERC721Basic, ERC721Enumerable, ERC721Metadata { +}
\ No newline at end of file diff --git a/packages/contracts/contracts/tokens/YesComplianceToken/WyreERC721Token/ERC721Basic.sol b/packages/contracts/contracts/tokens/YesComplianceToken/WyreERC721Token/ERC721Basic.sol new file mode 100644 index 000000000..20f3c5812 --- /dev/null +++ b/packages/contracts/contracts/tokens/YesComplianceToken/WyreERC721Token/ERC721Basic.sol @@ -0,0 +1,47 @@ +pragma solidity ^0.4.21; + +/** + * @title ERC721 Non-Fungible Token Standard basic interface + * @dev see https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md + */ +contract ERC721Basic { + event Transfer( + address indexed _from, + address indexed _to, + uint256 indexed _tokenId + ); + event Approval( + address indexed _owner, + address indexed _approved, + uint256 indexed _tokenId + ); + event ApprovalForAll( + address indexed _owner, + address indexed _operator, + bool _approved + ); + + function balanceOf(address _owner) public view returns (uint256 _balance); + function ownerOf(uint256 _tokenId) public view returns (address _owner); + function exists(uint256 _tokenId) public view returns (bool _exists); + + function approve(address _to, uint256 _tokenId) public; + function getApproved(uint256 _tokenId) + public view returns (address _operator); + + function setApprovalForAll(address _operator, bool _approved) public; + function isApprovedForAll(address _owner, address _operator) + public view returns (bool); + + function transferFrom(address _from, address _to, uint256 _tokenId) public; + function safeTransferFrom(address _from, address _to, uint256 _tokenId) + public; + + function safeTransferFrom( + address _from, + address _to, + uint256 _tokenId, + bytes _data + ) + public; +}
\ No newline at end of file diff --git a/packages/contracts/contracts/tokens/YesComplianceToken/WyreERC721Token/ERC721BasicToken.sol b/packages/contracts/contracts/tokens/YesComplianceToken/WyreERC721Token/ERC721BasicToken.sol new file mode 100644 index 000000000..1d3fa37b8 --- /dev/null +++ b/packages/contracts/contracts/tokens/YesComplianceToken/WyreERC721Token/ERC721BasicToken.sol @@ -0,0 +1,353 @@ +pragma solidity ^0.4.21; + +import "./ERC721Basic.sol"; +import "./ERC721Receiver.sol"; +import "../../math/SafeMath.sol"; +import "../../AddressUtils.sol"; +import "../../introspection/ERC165Support.sol"; + + +/** + * @title ERC721 Non-Fungible Token Standard basic implementation + * @dev see https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md + */ +contract ERC721BasicToken is ERC165Support, ERC721Basic { + + bytes4 private constant InterfaceId_ERC721 = 0x80ac58cd; + /* + * 0x80ac58cd === + * bytes4(keccak256('balanceOf(address)')) ^ + * bytes4(keccak256('ownerOf(uint256)')) ^ + * bytes4(keccak256('approve(address,uint256)')) ^ + * bytes4(keccak256('getApproved(uint256)')) ^ + * bytes4(keccak256('setApprovalForAll(address,bool)')) ^ + * bytes4(keccak256('isApprovedForAll(address,address)')) ^ + * bytes4(keccak256('transferFrom(address,address,uint256)')) ^ + * bytes4(keccak256('safeTransferFrom(address,address,uint256)')) ^ + * bytes4(keccak256('safeTransferFrom(address,address,uint256,bytes)')) + */ + + bytes4 private constant InterfaceId_ERC721Exists = 0x4f558e79; + /* + * 0x4f558e79 === + * bytes4(keccak256('exists(uint256)')) + */ + + using SafeMath for uint256; + using AddressUtils for address; + + // Equals to `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))` + // which can be also obtained as `ERC721Receiver(0).onERC721Received.selector` + bytes4 private constant ERC721_RECEIVED = 0x150b7a02; + + // Mapping from token ID to owner + mapping (uint256 => address) internal tokenOwner; + + // Mapping from token ID to approved address + mapping (uint256 => address) internal tokenApprovals; + + // Mapping from owner to number of owned token + mapping (address => uint256) internal ownedTokensCount; + + // Mapping from owner to operator approvals + mapping (address => mapping (address => bool)) internal operatorApprovals; + + /** + * @dev Guarantees msg.sender is owner of the given token + * @param _tokenId uint256 ID of the token to validate its ownership belongs to msg.sender + */ + modifier onlyOwnerOf(uint256 _tokenId) { + require(ownerOf(_tokenId) == msg.sender); + _; + } + + /** + * @dev Checks msg.sender can transfer a token, by being owner, approved, or operator + * @param _tokenId uint256 ID of the token to validate + */ + modifier canTransfer(uint256 _tokenId) { + require(isApprovedOrOwner(msg.sender, _tokenId)); + _; + } + + function _supportsInterface(bytes4 _interfaceId) + internal + view + returns (bool) + { + return super._supportsInterface(_interfaceId) || + _interfaceId == InterfaceId_ERC721 || _interfaceId == InterfaceId_ERC721Exists; + } + + /** + * @dev Gets the balance of the specified address + * @param _owner address to query the balance of + * @return uint256 representing the amount owned by the passed address + */ + function balanceOf(address _owner) public view returns (uint256) { + require(_owner != address(0)); + return ownedTokensCount[_owner]; + } + + /** + * @dev Gets the owner of the specified token ID + * @param _tokenId uint256 ID of the token to query the owner of + * @return owner address currently marked as the owner of the given token ID + */ + function ownerOf(uint256 _tokenId) public view returns (address) { + address owner = tokenOwner[_tokenId]; + require(owner != address(0)); + return owner; + } + + /** + * @dev Returns whether the specified token exists + * @param _tokenId uint256 ID of the token to query the existence of + * @return whether the token exists + */ + function exists(uint256 _tokenId) public view returns (bool) { + address owner = tokenOwner[_tokenId]; + return owner != address(0); + } + + /** + * @dev Approves another address to transfer the given token ID + * The zero address indicates there is no approved address. + * There can only be one approved address per token at a given time. + * Can only be called by the token owner or an approved operator. + * @param _to address to be approved for the given token ID + * @param _tokenId uint256 ID of the token to be approved + */ + function approve(address _to, uint256 _tokenId) public { + address owner = ownerOf(_tokenId); + require(_to != owner); + require(msg.sender == owner || isApprovedForAll(owner, msg.sender)); + + tokenApprovals[_tokenId] = _to; + emit Approval(owner, _to, _tokenId); + } + + /** + * @dev Gets the approved address for a token ID, or zero if no address set + * @param _tokenId uint256 ID of the token to query the approval of + * @return address currently approved for the given token ID + */ + function getApproved(uint256 _tokenId) public view returns (address) { + return tokenApprovals[_tokenId]; + } + + /** + * @dev Sets or unsets the approval of a given operator + * An operator is allowed to transfer all tokens of the sender on their behalf + * @param _to operator address to set the approval + * @param _approved representing the status of the approval to be set + */ + function setApprovalForAll(address _to, bool _approved) public { + require(_to != msg.sender); + operatorApprovals[msg.sender][_to] = _approved; + emit ApprovalForAll(msg.sender, _to, _approved); + } + + /** + * @dev Tells whether an operator is approved by a given owner + * @param _owner owner address which you want to query the approval of + * @param _operator operator address which you want to query the approval of + * @return bool whether the given operator is approved by the given owner + */ + function isApprovedForAll( + address _owner, + address _operator + ) + public + view + returns (bool) + { + return operatorApprovals[_owner][_operator]; + } + + /** + * @dev Transfers the ownership of a given token ID to another address + * Usage of this method is discouraged, use `safeTransferFrom` whenever possible + * Requires the msg sender to be the owner, approved, or operator + * @param _from current owner of the token + * @param _to address to receive the ownership of the given token ID + * @param _tokenId uint256 ID of the token to be transferred + */ + function transferFrom( + address _from, + address _to, + uint256 _tokenId + ) + public + canTransfer(_tokenId) + { + require(_from != address(0)); + require(_to != address(0)); + + clearApproval(_from, _tokenId); + removeTokenFrom(_from, _tokenId); + addTokenTo(_to, _tokenId); + + emit Transfer(_from, _to, _tokenId); + } + + /** + * @dev Safely transfers the ownership of a given token ID to another address + * If the target address is a contract, it must implement `onERC721Received`, + * which is called upon a safe transfer, and return the magic value + * `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`; otherwise, + * the transfer is reverted. + * + * Requires the msg sender to be the owner, approved, or operator + * @param _from current owner of the token + * @param _to address to receive the ownership of the given token ID + * @param _tokenId uint256 ID of the token to be transferred + */ + function safeTransferFrom( + address _from, + address _to, + uint256 _tokenId + ) + public + canTransfer(_tokenId) + { + // solium-disable-next-line arg-overflow + safeTransferFrom(_from, _to, _tokenId, ""); + } + + /** + * @dev Safely transfers the ownership of a given token ID to another address + * If the target address is a contract, it must implement `onERC721Received`, + * which is called upon a safe transfer, and return the magic value + * `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`; otherwise, + * the transfer is reverted. + * Requires the msg sender to be the owner, approved, or operator + * @param _from current owner of the token + * @param _to address to receive the ownership of the given token ID + * @param _tokenId uint256 ID of the token to be transferred + * @param _data bytes data to send along with a safe transfer check + */ + function safeTransferFrom( + address _from, + address _to, + uint256 _tokenId, + bytes _data + ) + public + canTransfer(_tokenId) + { + transferFrom(_from, _to, _tokenId); + // solium-disable-next-line arg-overflow + require(checkAndCallSafeTransfer(_from, _to, _tokenId, _data)); + } + + /** + * @dev Returns whether the given spender can transfer a given token ID + * @param _spender address of the spender to query + * @param _tokenId uint256 ID of the token to be transferred + * @return bool whether the msg.sender is approved for the given token ID, + * is an operator of the owner, or is the owner of the token + */ + function isApprovedOrOwner( + address _spender, + uint256 _tokenId + ) + internal + view + returns (bool) + { + address owner = ownerOf(_tokenId); + // Disable solium check because of + // https://github.com/duaraghav8/Solium/issues/175 + // solium-disable-next-line operator-whitespace + return ( + _spender == owner || + getApproved(_tokenId) == _spender || + isApprovedForAll(owner, _spender) + ); + } + + /** + * @dev Internal function to mint a new token + * Reverts if the given token ID already exists + * @param _to The address that will own the minted token + * @param _tokenId uint256 ID of the token to be minted by the msg.sender + */ + function _mint(address _to, uint256 _tokenId) internal { + require(_to != address(0)); + addTokenTo(_to, _tokenId); + emit Transfer(address(0), _to, _tokenId); + } + + /** + * @dev Internal function to burn a specific token + * Reverts if the token does not exist + * @param _tokenId uint256 ID of the token being burned by the msg.sender + */ + function _burn(address _owner, uint256 _tokenId) internal { + clearApproval(_owner, _tokenId); + removeTokenFrom(_owner, _tokenId); + emit Transfer(_owner, address(0), _tokenId); + } + + /** + * @dev Internal function to clear current approval of a given token ID + * Reverts if the given address is not indeed the owner of the token + * @param _owner owner of the token + * @param _tokenId uint256 ID of the token to be transferred + */ + function clearApproval(address _owner, uint256 _tokenId) internal { + require(ownerOf(_tokenId) == _owner); + if (tokenApprovals[_tokenId] != address(0)) { + tokenApprovals[_tokenId] = address(0); + } + } + + /** + * @dev Internal function to add a token ID to the list of a given address + * @param _to address representing the new owner of the given token ID + * @param _tokenId uint256 ID of the token to be added to the tokens list of the given address + */ + function addTokenTo(address _to, uint256 _tokenId) internal { + require(tokenOwner[_tokenId] == address(0)); + tokenOwner[_tokenId] = _to; + ownedTokensCount[_to] = ownedTokensCount[_to].add(1); + } + + /** + * @dev Internal function to remove a token ID from the list of a given address + * @param _from address representing the previous owner of the given token ID + * @param _tokenId uint256 ID of the token to be removed from the tokens list of the given address + */ + function removeTokenFrom(address _from, uint256 _tokenId) internal { + require(ownerOf(_tokenId) == _from); + ownedTokensCount[_from] = ownedTokensCount[_from].sub(1); + tokenOwner[_tokenId] = address(0); + } + + /** + * @dev Internal function to invoke `onERC721Received` on a target address + * The call is not executed if the target address is not a contract + * @param _from address representing the previous owner of the given token ID + * @param _to target address that will receive the tokens + * @param _tokenId uint256 ID of the token to be transferred + * @param _data bytes optional data to send along with the call + * @return whether the call correctly returned the expected magic value + */ + function checkAndCallSafeTransfer( + address _from, + address _to, + uint256 _tokenId, + bytes _data + ) + internal + returns (bool) + { + if (!_to.isContract()) { + return true; + } + bytes4 retval = ERC721Receiver(_to).onERC721Received( + msg.sender, _from, _tokenId, _data); + return (retval == ERC721_RECEIVED); + } +}
\ No newline at end of file diff --git a/packages/contracts/contracts/tokens/YesComplianceToken/WyreERC721Token/ERC721Token.sol b/packages/contracts/contracts/tokens/YesComplianceToken/WyreERC721Token/ERC721Token.sol new file mode 100644 index 000000000..c7acee6df --- /dev/null +++ b/packages/contracts/contracts/tokens/YesComplianceToken/WyreERC721Token/ERC721Token.sol @@ -0,0 +1,218 @@ +pragma solidity ^0.4.21; + +import "./ERC721.sol"; +import "./ERC721BasicToken.sol"; + + +/** + * @title Full ERC721 Token + * This implementation includes all the required and some optional functionality of the ERC721 standard + * Moreover, it includes approve all functionality using operator terminology + * @dev see https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md + */ +contract ERC721Token is ERC721BasicToken, ERC721 { + + bytes4 private constant InterfaceId_ERC721Enumerable = 0x780e9d63; + /** + * 0x780e9d63 === + * bytes4(keccak256('totalSupply()')) ^ + * bytes4(keccak256('tokenOfOwnerByIndex(address,uint256)')) ^ + * bytes4(keccak256('tokenByIndex(uint256)')) + */ + + bytes4 private constant InterfaceId_ERC721Metadata = 0x5b5e139f; + /** + * 0x5b5e139f === + * bytes4(keccak256('name()')) ^ + * bytes4(keccak256('symbol()')) ^ + * bytes4(keccak256('tokenURI(uint256)')) + */ + + // Token name + string internal name_; + + // Token symbol + string internal symbol_; + + // Mapping from owner to list of owned token IDs + mapping(address => uint256[]) internal ownedTokens; + + // Mapping from token ID to index of the owner tokens list + mapping(uint256 => uint256) internal ownedTokensIndex; + + // Array with all token ids, used for enumeration + uint256[] internal allTokens; + + // Mapping from token id to position in the allTokens array + mapping(uint256 => uint256) internal allTokensIndex; + + // Optional mapping for token URIs + mapping(uint256 => string) internal tokenURIs; + + /** + * @dev Constructor function + */ + function initialize(string _name, string _symbol) public isInitializer("ERC721Token", "1.9.0") { + name_ = _name; + symbol_ = _symbol; + } + + function _supportsInterface(bytes4 _interfaceId) + internal + view + returns (bool) + { + return super._supportsInterface(_interfaceId) || + _interfaceId == InterfaceId_ERC721Enumerable || _interfaceId == InterfaceId_ERC721Metadata; + } + + /** + * @dev Gets the token name + * @return string representing the token name + */ + function name() external view returns (string) { + return name_; + } + + /** + * @dev Gets the token symbol + * @return string representing the token symbol + */ + function symbol() external view returns (string) { + return symbol_; + } + + /** + * @dev Returns an URI for a given token ID + * Throws if the token ID does not exist. May return an empty string. + * @param _tokenId uint256 ID of the token to query + */ + function tokenURI(uint256 _tokenId) public view returns (string) { + require(exists(_tokenId)); + return tokenURIs[_tokenId]; + } + + /** + * @dev Gets the token ID at a given index of the tokens list of the requested owner + * @param _owner address owning the tokens list to be accessed + * @param _index uint256 representing the index to be accessed of the requested tokens list + * @return uint256 token ID at the given index of the tokens list owned by the requested address + */ + function tokenOfOwnerByIndex( + address _owner, + uint256 _index + ) + public + view + returns (uint256) + { + require(_index < balanceOf(_owner)); + return ownedTokens[_owner][_index]; + } + + /** + * @dev Gets the total amount of tokens stored by the contract + * @return uint256 representing the total amount of tokens + */ + function totalSupply() public view returns (uint256) { + return allTokens.length; + } + + /** + * @dev Gets the token ID at a given index of all the tokens in this contract + * Reverts if the index is greater or equal to the total number of tokens + * @param _index uint256 representing the index to be accessed of the tokens list + * @return uint256 token ID at the given index of the tokens list + */ + function tokenByIndex(uint256 _index) public view returns (uint256) { + require(_index < totalSupply()); + return allTokens[_index]; + } + + /** + * @dev Internal function to set the token URI for a given token + * Reverts if the token ID does not exist + * @param _tokenId uint256 ID of the token to set its URI + * @param _uri string URI to assign + */ + function _setTokenURI(uint256 _tokenId, string _uri) internal { + require(exists(_tokenId)); + tokenURIs[_tokenId] = _uri; + } + + /** + * @dev Internal function to add a token ID to the list of a given address + * @param _to address representing the new owner of the given token ID + * @param _tokenId uint256 ID of the token to be added to the tokens list of the given address + */ + function addTokenTo(address _to, uint256 _tokenId) internal { + super.addTokenTo(_to, _tokenId); + uint256 length = ownedTokens[_to].length; + ownedTokens[_to].push(_tokenId); + ownedTokensIndex[_tokenId] = length; + } + + /** + * @dev Internal function to remove a token ID from the list of a given address + * @param _from address representing the previous owner of the given token ID + * @param _tokenId uint256 ID of the token to be removed from the tokens list of the given address + */ + function removeTokenFrom(address _from, uint256 _tokenId) internal { + super.removeTokenFrom(_from, _tokenId); + + uint256 tokenIndex = ownedTokensIndex[_tokenId]; + uint256 lastTokenIndex = ownedTokens[_from].length.sub(1); + uint256 lastToken = ownedTokens[_from][lastTokenIndex]; + + ownedTokens[_from][tokenIndex] = lastToken; + ownedTokens[_from][lastTokenIndex] = 0; + // Note that this will handle single-element arrays. In that case, both tokenIndex and lastTokenIndex are going to + // be zero. Then we can make sure that we will remove _tokenId from the ownedTokens list since we are first swapping + // the lastToken to the first position, and then dropping the element placed in the last position of the list + + ownedTokens[_from].length--; + ownedTokensIndex[_tokenId] = 0; + ownedTokensIndex[lastToken] = tokenIndex; + } + + /** + * @dev Internal function to mint a new token + * Reverts if the given token ID already exists + * @param _to address the beneficiary that will own the minted token + * @param _tokenId uint256 ID of the token to be minted by the msg.sender + */ + function _mint(address _to, uint256 _tokenId) internal { + super._mint(_to, _tokenId); + + allTokensIndex[_tokenId] = allTokens.length; + allTokens.push(_tokenId); + } + + /** + * @dev Internal function to burn a specific token + * Reverts if the token does not exist + * @param _owner owner of the token to burn + * @param _tokenId uint256 ID of the token being burned by the msg.sender + */ + function _burn(address _owner, uint256 _tokenId) internal { + super._burn(_owner, _tokenId); + + // Clear metadata (if any) + if (bytes(tokenURIs[_tokenId]).length != 0) { + delete tokenURIs[_tokenId]; + } + + // Reorg all tokens array + uint256 tokenIndex = allTokensIndex[_tokenId]; + uint256 lastTokenIndex = allTokens.length.sub(1); + uint256 lastToken = allTokens[lastTokenIndex]; + + allTokens[tokenIndex] = lastToken; + allTokens[lastTokenIndex] = 0; + + allTokens.length--; + allTokensIndex[_tokenId] = 0; + allTokensIndex[lastToken] = tokenIndex; + } + +}
\ No newline at end of file diff --git a/packages/contracts/contracts/tokens/YesComplianceToken/YesComplianceToken.sol b/packages/contracts/contracts/tokens/YesComplianceToken/YesComplianceToken.sol index abd04219d..b810d9f31 100644 --- a/packages/contracts/contracts/tokens/YesComplianceToken/YesComplianceToken.sol +++ b/packages/contracts/contracts/tokens/YesComplianceToken/YesComplianceToken.sol @@ -1,119 +1,650 @@ pragma solidity ^0.4.24; -import "../ERC721Token/ERC721Token.sol"; +import "./IYesComplianceToken.sol"; /** - * @notice an ERC721 "yes" compliance token supporting a collection of country-specific attributions which answer specific - * compliance-related queries with YES. (attestations) + * draft implementation of YES compliance token * - * primarily ERC721 is useful for the self-management of claiming addresses. a single token is more useful - * than a non-ERC721 interface because of interop with other 721-supporting systems/ui; it allows users to - * manage their financial stamp with flexibility using a well-established simple concept of non-fungible tokens. - * this interface is for anyone needing to carry around and otherwise manage their proof of compliance. + * NOTE: i have done relatively few gas optimization tweaks (beyond using the sturctures necessary to avoid any + * linear time procedures). + * in some cases i am using a call structure which replicates some checks. this is for code clarity/security - + * i marked a few obvious ones which could be optimized for gas, but :meh: * - * the financial systems these users authenticate against have a different set of API requirements. they need - * more contextualization ability than a balance check to support distinctions of attestations, as well as geographic - * distinction. these integrations are made simpler as the language of the query more closely match the language of compliance. - * - * this interface describes, beyond 721, these simple compliance-specific interfaces (and their management tools) - * - * notes: - * - no address can be associated with more than one identity (though addresses may have more than token). issuance - * in this circumstance will fail - * - one person or business = one entity - * - one entity may have many tokens across many addresses; they can mint and burn tokens tied to their identity at will - * - two token types: control & non-control. both carry compliance proof - * - control tokens let their holders mint and burn (within the same entity) - * - non-control tokens are solely for compliance queries - * - a lock on the entity is used instead of token revocation to remove the cash burden assumed by a customer to - * redistribute a fleet of coins - * - all country codes should be via ISO-3166-1 - * - * any (non-view) methods not explicitly marked idempotent are not idempotent. + * todo static owner should follow owner token? remove static owner? :security: :should: + * @author Tyson Malchow */ -contract YesComplianceTokenV1 is ERC721Token /*, ERC165 :should: */ { +contract YesComplianceToken is YesComplianceTokenV1 { - uint256 public constant OWNER_ENTITY_ID = 1; + uint64 private constant MAX_TOKENS_PER_ENTITY = 10240; // completely arbitrary limit + uint64 private constant MAX_ENTITIES = 2**32-1; // bc using 32 bit index tracking + uint64 private constant MAX_VALIDATORS_PER_MARK = 2**32-1; // bc using 32 bit index tracking + uint64 private constant TOTAL_YES_MARKS = 255; // bc 'uint8 yes' - uint8 public constant YESMARK_OWNER = 128; - uint8 public constant YESMARK_VALIDATOR = 129; + // todo could shorten the entity IDs to anything 160+ to make this cheaper? - /* - todo events: entity updated, destroyed, ???? - Finalized - Attested + /** @notice a single YES attestation */ + struct YesMark { - */ + /** @notice ISO-3166-1 country codes */ + uint16 countryCode; - /** - * @notice query api: returns true if the specified address has the given country/yes attestation. this - * is the primary method partners will use to query the active qualifications of any particular - * address. - */ - function isYes(uint256 _validatorEntityId, address _address, uint16 _countryCode, uint8 _yes) external view returns(bool) ; + /** @notice the possibly-country-speicifc YES being marked. */ + uint8 yes; + + // 8 bits more space in this slot.. could upgrade yes to uint16? + + /** @notice the index of this mark in EntityRecord.yesMarks */ + uint32 yesMarkIdx; + + /** a list of the validator entities which have attested to this mark */ + uint256[] validatorEntityIds; - /** @notice same as isYes except as an imperative */ - function requireYes(uint256 _validatorEntityId, address _address, uint16 _countryCode, uint8 _yes) external view ; + /** @notice index of each validator entity ID in validatorEntityIds */ + mapping(uint256 => uint32) validatorEntityIdIdx; + + // uint8 entityListIdx; + } /** - * @notice retrieve all YES marks for an address in a particular country - * @param _validatorEntityId the validator ID to consider. or, use 0 for any of them - * @param _address the validator ID to consider, or 0 for any of them - * @param _countryCode the ISO-3166-1 country code - * @return (non-duplicate) array of YES marks present + * tracks the state for a single recognized entity */ - function getYes(uint256 _validatorEntityId, address _address, uint16 _countryCode) external view returns(uint8[] /* memory */); + struct EntityRecord { + + /** true marking this entity ID has been encountered */ + bool init; + + /** when true, this entity is effectively useless */ + bool locked; + + // 30 bits more space in this slot + + /** position of the entityId in allEntityIds */ + uint32 entityIdIdx; + + /** used for creating reliable token IDs, monotonically increasing */ + uint64 tokenIdCounter; + + /** indexed YES mark lookups */ + mapping(bytes4 => YesMark) yesMarkByKey; + + /** raw collection of all marks keys */ + bytes4[] yesMarkKeys; - // function getCountries(uint256 _validatorEntityId, address _address) external view returns(uint16[] /* memory */); + /** all tokens associated with this identity */ + uint256[] tokenIds; + + // trellis/tower connection ? + // civic connection ? + // erc725/735 connection ? + } /** - * @notice create new tokens. fail if _to already - * belongs to a different entity and caller is not validator - * @param _control true if the new token is a control token (can mint, burn). aka NOT limited. - * @param _entityId the entity to mint for, supply 0 to use the entity tied to the caller - * @return the newly created token ID + * @notice all fields we want to add per-token. + * + * there may never be more than just control flag, in which case it may make sense to collapse this + * to just a mapping(uint256 => bool) ? */ - function mint(address _to, uint256 _entityId, bool _control) external returns (uint256); + struct TokenRecord { + + /** position of the tokenId in EntityRecord.tokenIds */ + uint32 tokenIdIdx; + + /** true if this token has administrative superpowers (aka is _not_ limited) */ + bool control; + + /** true if this token cannot move */ + bool finalized; + + // 30 bits more in this slot - /** @notice shortcut to mint() + setYes() in one call, for a single country */ - function mint(address _to, uint256 _entityId, bool _control, uint16 _countryCode, uint8[] _yes) external returns (uint256); + // limitations: in/out? + } - /** @notice destroys a specific token */ - function burn(uint256 _tokenId) external; + address public ownerAddress; - /** @notice destroys the entire entity and all tokens */ - function burnEntity(uint256 _entityId) external; + mapping(uint256 => TokenRecord) public tokenRecordById; + mapping(uint256 => EntityRecord) public entityRecordById; + mapping(uint256 => uint256) public entityIdByTokenId; + + /** for entity enumeration. maximum of 2^256-1 total entities (i think we'll be ok) */ + uint256[] entityIds; + + constructor() public { + /* this space intentionally left blank */ + } /** - * @notice adds a specific attestations (yes) to an entity. idempotent: will return normally even if the mark - * was already set by this validator + * constructor alternative: first-time initialization the contract/token (required because of upgradeability) */ - function setYes(uint256 _entityId, uint16 _countryCode, uint8 _yes) external; + function initialize(string _name, string _symbol) { + // require(super._symbol.length == 0 || _symbol == super._symbol); // cannot change symbol after first init bc that could fuck shit up + _upgradeable_initialize(); // basically for security + super.initialize(_name, _symbol); // init token info + + // grant the owner token + mint_I(ownerAddress, OWNER_ENTITY_ID, true); + + // ecosystem owner gets both owner and validator marks (self-attested) + setYes_I(OWNER_ENTITY_ID, OWNER_ENTITY_ID, 0, YESMARK_OWNER); + setYes_I(OWNER_ENTITY_ID, OWNER_ENTITY_ID, 0, YESMARK_VALIDATOR); + } /** - * @notice removes a attestation(s) from a specific validator for an entity. idempotent + * executed in lieu of a constructor in a delegated context */ - function clearYes(uint256 _entityId, uint16 _countryCode, uint8 _yes) external; + function _upgradeable_initialize() public { + super._upgradeable_initialize(); // provides require(msg.sender == _upgradeable_delegate_owner); + + // some things are still tied to the owner (instead of the yesmark_owner :notsureif:) + ownerAddress = msg.sender; + } + + // YesComplianceTokenV1 Interface Methods -------------------------------------------------------------------------- + + function isYes(uint256 _validatorEntityId, address _address, uint16 _countryCode, uint8 _yes) external view returns(bool) { + return isYes_I(_validatorEntityId, _address, _countryCode, _yes); + } + + function requireYes(uint256 _validatorEntityId, address _address, uint16 _countryCode, uint8 _yes) external view { + require(isYes_I(_validatorEntityId, _address, _countryCode, _yes)); + } + + function getYes(uint256 _validatorEntityId, address _address, uint16 _countryCode) external view returns(uint8[] memory) { + if(balanceOf(_address) == 0) + return new uint8[](0); + + uint256 entityId = entityIdByTokenId[tokenOfOwnerByIndex(_address, 0)]; + EntityRecord storage e = entityRecordById[entityId]; + uint256 j = 0; + uint256 i; + + // locked always bails + if(e.locked) + return new uint8[](0); + + uint8[] memory r = new uint8[](e.yesMarkKeys.length); + + for(i = 0; i < e.yesMarkKeys.length; i++) { + YesMark storage m = e.yesMarkByKey[e.yesMarkKeys[i]]; + + // filter country code + if(m.countryCode != _countryCode) + continue; + + // filter explicit validator entity + if(_validatorEntityId > 0 + && m.validatorEntityIdIdx[_validatorEntityId] == 0 + && (m.validatorEntityIds.length == 0 || m.validatorEntityIds[0] == _validatorEntityId)) + continue; + + // matched, chyess + r[j++] = m.yes; + } + + // reduce array length + assembly { mstore(r, j) } + + return r; + } + + function mint(address _to, uint256 _entityId, bool _control) external returns (uint256) /* internally protected */{ + uint256 callerTokenId = tokenOfOwnerByIndex(msg.sender, 0); + uint256 callerEntityId = entityIdByTokenId[callerTokenId]; + + // make sure caller has a control token, at the least + require(tokenRecordById[callerTokenId].control, 'control token required'); + + // determine/validate the entity being minted for + uint256 realEntityId; + if(_entityId == 0 || _entityId == callerEntityId) { + // unspecified entity, or caller entity, can do! + realEntityId = callerEntityId; + + } else { + // otherwise make sure caller is a VALIDATOR, else fail + require(senderIsControlValidator(), 'illegal entity id'); // some duplicate checks/lookups, gas leak + realEntityId = _entityId; + } + + return mint_I(_to, realEntityId, _control); + } + + function mint(address _to, uint256 _entityId, bool _control, uint16 _countryCode, uint8[] _yes) external returns (uint256) /* internally protected */ { + // lazy warning: this is a 90% copy/paste job from the mint directly above this + + uint256 callerTokenId = tokenOfOwnerByIndex(msg.sender, 0); + uint256 callerEntityId = entityIdByTokenId[callerTokenId]; + + // make sure caller has a control token, at the least + require(tokenRecordById[callerTokenId].control, 'control token required'); + + // determine/validate the entity being minted for + uint256 realEntityId; + if(_entityId == 0 || _entityId == callerEntityId) { + // unspecified entity, or caller entity, can do! + realEntityId = callerEntityId; + + } else { + // otherwise make sure caller is a VALIDATOR, else fail + require(senderIsControlValidator()); // some duplicate checks/lookups, gas leak + realEntityId = _entityId; + } + + // mint the coin + uint256 tokenId = mint_I(_to, realEntityId, _control); + + // now set the attestations + require(_yes.length <= TOTAL_YES_MARKS); // safety + for(uint256 i = 0; i<_yes.length; i++) { + setYes_I(_entityId, _countryCode, _yes[i]); + } + + return tokenId; + } + + function getEntityId(address _address) external view returns (uint256) { + return entityIdByTokenId[tokenOfOwnerByIndex(_address, 0)]; + } + + function burn(uint256 _tokenId) external permission_control_tokenId(_tokenId) { + uint256 entityId = entityIdByTokenId[_tokenId]; + + EntityRecord storage e = entity(entityId); + TokenRecord storage t = tokenRecordById[_tokenId]; + + // remove token from entity + e.tokenIds[t.tokenIdIdx] = e.tokenIds[e.tokenIds.length - 1]; + e.tokenIds.length--; + + // update tracked index (of swapped, if present) + if(e.tokenIds.length > t.tokenIdIdx) + tokenRecordById[e.tokenIds[t.tokenIdIdx]].tokenIdIdx = t.tokenIdIdx; + + // remove token record + delete tokenRecordById[_tokenId]; + + // burn the actual token + super._burn(tokenOwner[_tokenId], _tokenId); + } + + function burnEntity(uint256 _entityId) external permission_control_entityId(_entityId) { // self-burn allowed + EntityRecord storage e = entity(_entityId); + + // burn all the tokens + for(uint256 i = 0; i < e.tokenIds.length; i++) { + uint256 tokenId = e.tokenIds[i]; + super._burn(tokenOwner[tokenId], tokenId); + } + + // clear all the marks + clearYes_I(_entityId); + + // clear out entity record + e.init = false; + e.locked = false; + e.entityIdIdx = 0; + e.tokenIdCounter = 0; + + assert(e.yesMarkKeys.length == 0); + assert(e.tokenIds.length == 0); + } + + function setYes(uint256 _entityId, uint16 _countryCode, uint8 _yes) external permission_validator { + setYes_I(_entityId, _countryCode, _yes); + } + + function clearYes(uint256 _entityId, uint16 _countryCode, uint8 _yes) external permission_validator { + require(_yes > 0); + require(_yes != 128); + + // special check against reserved country code 0 + if(_countryCode == 0) + require(senderIsEcosystemControl(), 'not authorized as ecosystem control'); // this is duplicating some things, gas leak + + EntityRecord storage e = entity(_entityId); + + uint256 callerTokenId = tokenOfOwnerByIndex(msg.sender, 0); + uint256 callerEntityId = entityIdByTokenId[callerTokenId]; + bytes4 key = yesKey(_countryCode, _yes); + + YesMark storage mark = e.yesMarkByKey[key]; + if(mark.yes == 0) + return; // not set by anyone, bail happily + + if(mark.validatorEntityIdIdx[callerEntityId] == 0 && + (mark.validatorEntityIds.length == 0 || mark.validatorEntityIds[0] != callerEntityId)) { + // set, but not by this validator, bail happily + return; + } + + clearYes_I(mark, e, callerEntityId); + } + + function clearYes(uint256 _entityId, uint16 _countryCode) external permission_validator { + // special check against 129 validator mark + if(_countryCode == 0) + require(senderIsEcosystemControl(), 'not authorized as ecosystem control (129)'); // this is duplicating some things, gas leak + + EntityRecord storage e = entity(_entityId); + + uint256 callerTokenId = tokenOfOwnerByIndex(msg.sender, 0); + uint256 callerEntityId = entityIdByTokenId[callerTokenId]; + uint256 i; + + for(i =0; i<e.yesMarkKeys.length; i++) { + YesMark storage mark = e.yesMarkByKey[e.yesMarkKeys[i]]; + + if(mark.countryCode != _countryCode) + continue; + + if(mark.validatorEntityIdIdx[callerEntityId] == 0 && + (mark.validatorEntityIds.length == 0 || mark.validatorEntityIds[0] != callerEntityId)) { + // set, but not by this validator, skip + continue; + } + + if(clearYes_I(mark, e, callerEntityId)) { + // mark was fully destroyed (and replaced in e.yesMarkKeys with the last one) + i--; + } + } + } + + function clearYes(uint256 _entityId) external permission_validator { + clearYes_I(_entityId); + } + + function setLocked(uint256 _entityId, bool _lock) external permission_validator { + EntityRecord storage e = entity(_entityId); + + // can't fux with owner lock + require(_entityId != OWNER_ENTITY_ID); + + // if caller isn't ecosystem control, cannot target other validators + if(!senderIsEcosystemControl()) + require(e.yesMarkByKey[yesKey(0, YESMARK_VALIDATOR)].yes == 0); + + // lockzz + e.locked = _lock; + } + + function isLocked(uint256 _entityId) external view returns (bool) { + return entity(_entityId).locked; + } + + function isFinalized(uint256 _tokenId) external view returns (bool) { + return tokenRecordById[_tokenId].finalized; + } + + function finalize(uint256 _tokenId) external permission_access_tokenId(_tokenId) { + TokenRecord storage t = tokenRecordById[_tokenId]; + t.finalized = true; + } + + // Internal Methods ------------------------------------------------------------------------------------------------ + + function clearYes_I(YesMark storage mark, EntityRecord storage e, uint256 validatorEntityId) internal returns(bool) { + uint32 idx = mark.validatorEntityIdIdx[validatorEntityId]; + mark.validatorEntityIds[idx] = mark.validatorEntityIds[mark.validatorEntityIds.length - 1]; + mark.validatorEntityIds.length--; + delete mark.validatorEntityIdIdx[validatorEntityId]; + + // remap + if(mark.validatorEntityIds.length > idx) + mark.validatorEntityIdIdx[mark.validatorEntityIds[idx]] = idx; + + // check if the entire mark needs deleting + if(mark.validatorEntityIds.length == 0) { + // yes, it does. swap/delete + idx = mark.yesMarkIdx; + e.yesMarkKeys[idx] = e.yesMarkKeys[e.yesMarkKeys.length - 1]; + e.yesMarkKeys.length--; + + // remap + if(e.yesMarkKeys.length > idx) + e.yesMarkByKey[e.yesMarkKeys[idx]].yesMarkIdx = idx; + + // delete mark + mark.countryCode = 0; + mark.yes = 0; + mark.yesMarkIdx = 0; + // assert(mark.validatorEntityIds.length == 0); + + return true; + } + + return false; + } + + function clearYes_I(uint256 _entityId) internal { + require(_entityId != OWNER_ENTITY_ID); + + EntityRecord storage e = entity(_entityId); + + // only ecosystem control can touch validators + if(!senderIsEcosystemControl()) + require(e.yesMarkByKey[yesKey(0, YESMARK_VALIDATOR)].yes == 0); + + uint256 callerTokenId = tokenOfOwnerByIndex(msg.sender, 0); + uint256 callerEntityId = entityIdByTokenId[callerTokenId]; + uint256 i; + + for(i =0; i<e.yesMarkKeys.length; i++) { + YesMark storage mark = e.yesMarkByKey[e.yesMarkKeys[i]]; + + if(mark.validatorEntityIdIdx[callerEntityId] == 0 && + (mark.validatorEntityIds.length == 0 || mark.validatorEntityIds[0] != callerEntityId)) { + // set, but not by this validator + continue; + } + + if(clearYes_I(mark, e, callerEntityId)) { + // mark was fully destroyed (and replaced in e.yesMarkKeys with the last one) + i--; + } + } + } + + function isYes_I(uint256 _validatorEntityId, address _address, uint16 _countryCode, uint8 _yes) internal view returns(bool) { + if(balanceOf(_address) == 0) + return false; + + uint256 entityId = entityIdByTokenId[tokenOfOwnerByIndex(_address, 0)]; + EntityRecord storage e = entityRecordById[entityId]; + + // locked always bails + if(e.locked) + return false; + + // gate out definite nos + YesMark storage m = e.yesMarkByKey[yesKey(_countryCode, _yes)]; + if(m.yes == 0) + return false; + + // no specific validators, we good + if(_validatorEntityId == 0) + return true; + + // filter by validator + return m.validatorEntityIdIdx[_validatorEntityId] > 0 + || m.validatorEntityIds.length > 0 && m.validatorEntityIds[0] == _validatorEntityId; + } + + function setYes_I(uint256 _entityId, uint16 _countryCode, uint8 _yes) internal { + require(_yes > 0); + require(_yes != 128); + + // special check against 129 validator mark + if(_yes == 129) + require(senderIsEcosystemControl()); // this is duplicating some checks, gas leak + + uint256 callerTokenId = tokenOfOwnerByIndex(msg.sender, 0); + uint256 callerEntityId = entityIdByTokenId[callerTokenId]; + + setYes_I(callerEntityId, _entityId, _countryCode, _yes); + } + + function setYes_I(uint256 _validatorEntityId, uint256 _entityId, uint16 _countryCode, uint8 _yes) internal { + // assert(_yes > 0); + EntityRecord storage targetEntity = entity(_entityId); + + // locate existing mark + bytes4 key = yesKey(_countryCode, _yes); + YesMark storage mark = targetEntity.yesMarkByKey[key]; + + if(mark.yes == 0) { + require(targetEntity.yesMarkKeys.length < TOTAL_YES_MARKS); + + // new mark on the entity + mark.countryCode = _countryCode; + mark.yes = _yes; + mark.yesMarkIdx = uint32(targetEntity.yesMarkKeys.length); + targetEntity.yesMarkKeys.push(key); + + } else if(mark.validatorEntityIdIdx[_validatorEntityId] > 0 || + (mark.validatorEntityIds.length > 0 && mark.validatorEntityIds[0] == _validatorEntityId)) { + + // existing mark and the caller is already on it + /* + i'm inclined to make it do nothing in this case (instead of failing) since i'm not at this point positive how best + to distinguish error types to a caller, which would be required for a caller to know wtf to do in this case + (otherwise they need to query blockchain again) + (but that costs gas... :notsureif:) + */ + return; + } + + require(mark.validatorEntityIds.length < MAX_VALIDATORS_PER_MARK); + + // add this validator to the mark + mark.validatorEntityIdIdx[_validatorEntityId] = uint32(mark.validatorEntityIds.length); + mark.validatorEntityIds.push(_validatorEntityId); + } + + /** non-permissed internal minting impl */ + function mint_I(address _to, uint256 _entityId, bool _control) internal returns (uint256) { + EntityRecord storage e = entity(_entityId); + require(e.tokenIds.length < MAX_TOKENS_PER_ENTITY, 'token limit reached'); + require(e.tokenIdCounter < 2**64-1); // kind of ridiculous but whatever, safety first! + uint256 tokenId = uint256(keccak256(abi.encodePacked(_entityId, e.tokenIdCounter++))); + super._mint(_to, tokenId); + tokenRecordById[tokenId].tokenIdIdx = uint32(e.tokenIds.length); + tokenRecordById[tokenId].control = _control; + e.tokenIds.push(tokenId); + entityIdByTokenId[tokenId] = _entityId; + return tokenId; + } + + /** entity resolution (creation when needed) */ + function entity(uint256 _entityId) internal returns (EntityRecord storage) { + require(_entityId > 0); + EntityRecord storage e = entityRecordById[_entityId]; + if(e.init) return e; + require(entityIds.length < MAX_ENTITIES); + e.init = true; + e.entityIdIdx = uint32(entityIds.length); + entityIds.push(_entityId); + return e; + } + + /** override default addTokenTo for additional transaction limitations */ + function addTokenTo(address _to, uint256 _tokenId) internal { + uint256 entityId = entityIdByTokenId[_tokenId]; + + // ensure one owner cannot be associated with multiple entities + // NOTE: this breaks hotwallet integrations, at this point necessarily so + if(balanceOf(_to) > 0) { + uint256 prevEntityId = entityIdByTokenId[tokenOfOwnerByIndex(_to, 0)]; + require(prevEntityId == entityId, 'conflicting entities'); + } + + require(!tokenRecordById[_tokenId].finalized, 'token is finalized'); + + super.addTokenTo(_to, _tokenId); + } + + /** the sender is the same entity as the one specified */ + function senderIsEntity_ByEntityId(uint256 _entityId) internal view returns (bool) { + return _entityId == entityIdByTokenId[tokenOfOwnerByIndex(msg.sender, 0)]; + } + + /** the sender is the same entity as the one specified, and the sender is a control for that entity */ + function senderIsControl_ByEntityId(uint256 _entityId) internal view returns (bool) { + if(balanceOf(msg.sender) == 0) + return false; + uint256 tokenId = tokenOfOwnerByIndex(msg.sender, 0); + uint256 senderEntityId = entityIdByTokenId[tokenId]; + return _entityId == senderEntityId && tokenRecordById[tokenId].control; + } + + /** the sender is a non-locked validator via control token */ + function senderIsControlValidator() internal view returns (bool) { + if(balanceOf(msg.sender) == 0) + return false; + uint256 tokenId = tokenOfOwnerByIndex(msg.sender, 0); + uint256 senderEntityId = entityIdByTokenId[tokenId]; + EntityRecord storage e = entityRecordById[senderEntityId]; + return tokenRecordById[tokenId].control + && !e.locked + && entityRecordById[senderEntityId].yesMarkByKey[yesKey(0, YESMARK_VALIDATOR)].yes > 0; + } + + /** the sender is the same entity as the one tied to the token specified */ + function senderIsEntity_ByTokenId(uint256 _tokenId) internal view returns (bool) { + if(balanceOf(msg.sender) == 0) + return false; + return entityIdByTokenId[_tokenId] == entityIdByTokenId[tokenOfOwnerByIndex(msg.sender, 0)]; + } + + /** the sender is the same entity as the one tied to the token specified, and the sender is a control for that entity */ + function senderIsControl_ByTokenId(uint256 _tokenId) internal view returns (bool) { + if(balanceOf(msg.sender) == 0) + return false; + uint256 senderEntityId = entityIdByTokenId[tokenOfOwnerByIndex(msg.sender, 0)]; + return entityIdByTokenId[_tokenId] == senderEntityId && tokenRecordById[_tokenId].control; + } + + /** checks if sender is the singular ecosystem owner */ + function senderIsEcosystemControl() internal view returns (bool) { + // todo deprecate ownerAddress ?! + return msg.sender == ownerAddress || senderIsControl_ByEntityId(OWNER_ENTITY_ID); + } + + /** a key for a YES attestation mark */ + function yesKey(uint16 _countryCode, uint8 _yes) internal pure returns(bytes4) { + return bytes4(keccak256(abi.encodePacked(_countryCode, _yes))); + } - /** @notice removes all attestations in a given country for a particular entity. idempotent */ - function clearYes(uint256 _entityId, uint16 _countryCode) external; + // PERMISSIONS MODIFIERS ---------------------------------------------------------------- - /** @notice removes all attestations for a particular entity. idempotent */ - function clearYes(uint256 _entityId) external; + modifier permission_validator { + require(senderIsControlValidator(), 'not authorized as validator'); + _; + } - /** @notice assigns a lock to an entity, rendering all isYes queries false. idempotent */ - function setLocked(uint256 _entityId, bool _lock) external; + modifier permission_super { + require(senderIsEcosystemControl(), 'not authorized as ecosystem control'); + _; + } - /** @notice checks whether or not a particular entity is locked */ - function isLocked(uint256 _entityId) external view returns(bool); +// modifier permission_access_entityId(uint256 _entityId) { +// require(senderIsEcosystemControl() || senderIsEntity_ByEntityId(_entityId)); +// _; +// } - /** @notice returns true if the specified token has been finalized (cannot be moved) */ - function isFinalized(uint256 _tokenId) external view returns(bool); + modifier permission_control_entityId(uint256 _entityId) { + require(senderIsEcosystemControl() || senderIsControl_ByEntityId(_entityId), 'not authorized entity controller'); + _; + } - /** @notice finalizes a token by ID preventing it from getting moved. idempotent */ - function finalize(uint256 _tokenId) external; + modifier permission_access_tokenId(uint256 _tokenId) { + require(senderIsEcosystemControl() || senderIsEntity_ByTokenId(_tokenId)); + _; + } - /** @return the entity ID associated with an address (or fail if there is not one) */ - function getEntityId(address _address) external view returns(uint256); + modifier permission_control_tokenId(uint256 _tokenId) { + require(senderIsEcosystemControl() || senderIsControl_ByTokenId(_tokenId), 'not authorized token controller'); + _; + } }
\ No newline at end of file diff --git a/packages/contracts/test/extensions/compliant_forwarder.ts b/packages/contracts/test/extensions/compliant_forwarder.ts new file mode 100644 index 000000000..41603e3c2 --- /dev/null +++ b/packages/contracts/test/extensions/compliant_forwarder.ts @@ -0,0 +1,191 @@ +import { BlockchainLifecycle } from '@0x/dev-utils'; +import { assetDataUtils } from '@0x/order-utils'; +import { RevertReason, SignedOrder } from '@0x/types'; +import { BigNumber } from '@0x/utils'; +import { Web3Wrapper } from '@0x/web3-wrapper'; +import * as chai from 'chai'; +import { TransactionReceiptWithDecodedLogs } from 'ethereum-types'; + +import { DummyERC20TokenContract } from '../../generated-wrappers/dummy_erc20_token'; +import { ExchangeContract } from '../../generated-wrappers/exchange'; +import { DummyYesComplianceContract } from '../../generated-wrappers/forwarder'; +import { WETH9Contract } from '../../generated-wrappers/weth9'; +import { artifacts } from '../../src/artifacts'; +import { + expectContractCreationFailedAsync, + expectTransactionFailedAsync, + sendTransactionResult, +} from '../utils/assertions'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { ERC20Wrapper } from '../utils/erc20_wrapper'; +import { ERC721Wrapper } from '../utils/erc721_wrapper'; +import { ExchangeWrapper } from '../utils/exchange_wrapper'; +import { ForwarderWrapper } from '../utils/forwarder_wrapper'; +import { OrderFactory } from '../utils/order_factory'; +import { ContractName, ERC20BalancesByOwner } from '../utils/types'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); +const DECIMALS_DEFAULT = 18; +const MAX_WETH_FILL_PERCENTAGE = 95; + +describe(ContractName.Forwarder, () => { + let makerAddress: string; + let owner: string; + let takerAddress: string; + let feeRecipientAddress: string; + let otherAddress: string; + let defaultMakerAssetAddress: string; + let zrxAssetData: string; + let wethAssetData: string; + + let weth: DummyERC20TokenContract; + let zrxToken: DummyERC20TokenContract; + let erc20TokenA: DummyERC20TokenContract; + let erc721Token: DummyERC721TokenContract; + let forwarderContract: ForwarderContract; + let wethContract: WETH9Contract; + let forwarderWrapper: ForwarderWrapper; + let exchangeWrapper: ExchangeWrapper; + + let orderWithoutFee: SignedOrder; + let orderWithFee: SignedOrder; + let feeOrder: SignedOrder; + let orderFactory: OrderFactory; + let erc20Wrapper: ERC20Wrapper; + let erc20Balances: ERC20BalancesByOwner; + let tx: TransactionReceiptWithDecodedLogs; + + let erc721MakerAssetIds: BigNumber[]; + let takerEthBalanceBefore: BigNumber; + let feePercentage: BigNumber; + let gasPrice: BigNumber; + + before(async () => { + await blockchainLifecycle.startAsync(); + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + const usedAddresses = ([owner, makerAddress, takerAddress, feeRecipientAddress, otherAddress] = accounts); + + const txHash = await web3Wrapper.sendTransactionAsync({ from: accounts[0], to: accounts[0], value: 0 }); + const transaction = await web3Wrapper.getTransactionByHashAsync(txHash); + gasPrice = new BigNumber(transaction.gasPrice); + + const erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner); + erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner); + + const numDummyErc20ToDeploy = 3; + [erc20TokenA, zrxToken] = await erc20Wrapper.deployDummyTokensAsync( + numDummyErc20ToDeploy, + constants.DUMMY_TOKEN_DECIMALS, + ); + const erc20Proxy = await erc20Wrapper.deployProxyAsync(); + await erc20Wrapper.setBalancesAndAllowancesAsync(); + + [erc721Token] = await erc721Wrapper.deployDummyTokensAsync(); + const erc721Proxy = await erc721Wrapper.deployProxyAsync(); + await erc721Wrapper.setBalancesAndAllowancesAsync(); + const erc721Balances = await erc721Wrapper.getBalancesAsync(); + erc721MakerAssetIds = erc721Balances[makerAddress][erc721Token.address]; + + wethContract = await WETH9Contract.deployFrom0xArtifactAsync(artifacts.WETH9, provider, txDefaults); + weth = new DummyERC20TokenContract(wethContract.abi, wethContract.address, provider); + erc20Wrapper.addDummyTokenContract(weth); + + wethAssetData = assetDataUtils.encodeERC20AssetData(wethContract.address); + zrxAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + const exchangeInstance = await ExchangeContract.deployFrom0xArtifactAsync( + artifacts.Exchange, + provider, + txDefaults, + zrxAssetData, + ); + exchangeWrapper = new ExchangeWrapper(exchangeInstance, provider); + await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, owner); + await exchangeWrapper.registerAssetProxyAsync(erc721Proxy.address, owner); + + await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeInstance.address, { + from: owner, + }); + await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeInstance.address, { + from: owner, + }); + + defaultMakerAssetAddress = erc20TokenA.address; + const defaultTakerAssetAddress = wethContract.address; + const defaultOrderParams = { + exchangeAddress: exchangeInstance.address, + makerAddress, + feeRecipientAddress, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), DECIMALS_DEFAULT), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), DECIMALS_DEFAULT), + makerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT), + }; + const privateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddress)]; + orderFactory = new OrderFactory(privateKey, defaultOrderParams); + + const forwarderInstance = await ForwarderContract.deployFrom0xArtifactAsync( + artifacts.Forwarder, + provider, + txDefaults, + exchangeInstance.address, + zrxAssetData, + wethAssetData, + ); + forwarderContract = new ForwarderContract(forwarderInstance.abi, forwarderInstance.address, provider); + forwarderWrapper = new ForwarderWrapper(forwarderContract, provider); + const zrxDepositAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(10000), 18); + await web3Wrapper.awaitTransactionSuccessAsync( + await zrxToken.transfer.sendTransactionAsync(forwarderContract.address, zrxDepositAmount), + ); + erc20Wrapper.addTokenOwnerAddress(forwarderInstance.address); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + erc20Balances = await erc20Wrapper.getBalancesAsync(); + takerEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + orderWithoutFee = await orderFactory.newSignedOrderAsync(); + feeOrder = await orderFactory.newSignedOrderAsync({ + makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), + }); + orderWithFee = await orderFactory.newSignedOrderAsync({ + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), + }); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + + describe('constructor', () => { + it('should revert if assetProxy is unregistered', async () => { + const exchangeInstance = await ExchangeContract.deployFrom0xArtifactAsync( + artifacts.Exchange, + provider, + txDefaults, + zrxAssetData, + ); + return expectContractCreationFailedAsync( + (ForwarderContract.deployFrom0xArtifactAsync( + artifacts.Forwarder, + provider, + txDefaults, + exchangeInstance.address, + zrxAssetData, + wethAssetData, + ) as any) as sendTransactionResult, + RevertReason.UnregisteredAssetProxy, + ); + }); + }); +}); +// tslint:disable:max-file-line-count +// tslint:enable:no-unnecessary-type-assertion |