aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--contracts/test-utils/src/types.ts1
-rw-r--r--packages/contracts/contracts/tokens/YesComplianceToken/IYesComplianceToken.sol118
-rw-r--r--packages/contracts/contracts/tokens/YesComplianceToken/WyreERC721Token/ERC721.sol40
-rw-r--r--packages/contracts/contracts/tokens/YesComplianceToken/WyreERC721Token/ERC721Basic.sol47
-rw-r--r--packages/contracts/contracts/tokens/YesComplianceToken/WyreERC721Token/ERC721BasicToken.sol353
-rw-r--r--packages/contracts/contracts/tokens/YesComplianceToken/WyreERC721Token/ERC721Token.sol218
-rw-r--r--packages/contracts/contracts/tokens/YesComplianceToken/YesComplianceToken.sol693
-rw-r--r--packages/contracts/test/extensions/compliant_forwarder.ts191
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