ATR Module
Last updated
Last updated
This contract represents the tokenised asset transfer rights as an ERC-721 token, keeps track of tokenised balances and handles transfers of the tokenised assets. PWN Safes are expected to use this contract to mint new Asset Transfer Right (ATR) tokens. The contract owner can also set a whitelist to restrict minting ATR tokens only for selected assets.
AssetTransferRights.sol is written in Solidity version 0.8.15
ATR token minting and burning
Keeping track of all ATR tokens
Transferring of assets with minted ATR token
Handling recipient permissions
Whitelist to allow tokenisation of only selected collections
This contract inherits other contracts. Please see their reference for a complete overview of the ATR Module.
mintAssetTransferRightsToken
This function is used to mint an ATR token. It has to be called by a valid PWN Safe, which is defined by the PWN Safe Factory isValid
function. It is not permitted to tokenise transfer rights of ATR tokens or any tokens that have approved operators.
This function takes one argument supplied by the PWN Safe Proxy:
MultiToken.Asset memory
asset
- An asset struct (see MultiToken)
function mintAssetTransferRightsToken(MultiToken.Asset memory asset) public returns (uint256) {
// Check that msg.sender is PWNSafe
require(safeValidator.isValidSafe(msg.sender) == true, "Caller is not a PWNSafe");
// Check that asset address is not zero address
require(asset.assetAddress != address(0), "Attempting to tokenize zero address asset");
// Check that asset address is not ATR contract address
require(asset.assetAddress != address(this), "Attempting to tokenize ATR token");
// Check that address is whitelisted
require(whitelist.canBeTokenized(asset.assetAddress) == true, "Asset is not whitelisted");
// Check that provided asset category is correct
if (asset.category == MultiToken.Category.ERC20) {
if (asset.assetAddress.supportsERC165()) {
require(asset.assetAddress.supportsERC165InterfaceUnchecked(type(IERC20).interfaceId), "Invalid provided category");
} else {
// Fallback check for ERC20 tokens not implementing ERC165
try IERC20(asset.assetAddress).totalSupply() returns (uint256) {
} catch { revert("Invalid provided category"); }
}
} else if (asset.category == MultiToken.Category.ERC721) {
require(asset.assetAddress.supportsInterface(type(IERC721).interfaceId), "Invalid provided category");
} else if (asset.category == MultiToken.Category.ERC1155) {
require(asset.assetAddress.supportsInterface(type(IERC1155).interfaceId), "Invalid provided category");
} else {
revert("Invalid provided category");
}
// Check that given asset is valid
require(asset.isValid(), "Asset is not valid");
// Check that asset collection doesn't have approvals
require(atrGuard.hasOperatorFor(msg.sender, asset.assetAddress) == false, "Some asset from collection has an approval");
// Check that ERC721 asset don't have approval
if (asset.category == MultiToken.Category.ERC721) {
address approved = IERC721(asset.assetAddress).getApproved(asset.id);
require(approved == address(0), "Asset has an approved address");
}
// Check if asset can be tokenized
require(_canBeTokenized(msg.sender, asset), "Insufficient balance to tokenize");
// Set ATR token id
uint256 atrTokenId = ++lastTokenId;
// Store asset data
_storeTokenizedAsset(atrTokenId, asset);
// Update tokenized balance
_increaseTokenizedBalance(atrTokenId, msg.sender, asset);
// Mint ATR token
_mint(msg.sender, atrTokenId);
emit TransferViaATR(address(0), msg.sender, atrTokenId, asset);
return atrTokenId;
}
mintAssetTransferRightsTokenBatch
In case a user wants to tokenise more than one asset it is recommended to use this function instead of calling mintAssetTransferRightsToken
multiple times.
This function takes one argument supplied by the PWN Safe Proxy:
MultiToken.Asset[] calldata
assets
- An array of asset structs (see MultiToken)
function mintAssetTransferRightsTokenBatch(
MultiToken.Asset[] calldata assets
) external {
for (uint256 i; i < assets.length; ++i) {
mintAssetTransferRightsToken(assets[i]);
}
}
burnAssetTransferRightsToken
A user can call this function to burn an ATR token. This will allow the owner of the asset to transfer the asset without the ATR token again.
This function takes one argument supplied by the caller:
uint256
atrTokenId
- ID of the ATR token to burn
function burnAssetTransferRightsToken(uint256 atrTokenId) public {
// Load asset
MultiToken.Asset memory asset = assets[atrTokenId];
// Check that token is indeed tokenized
require(
asset.assetAddress != address(0),
"Asset transfer rights are not tokenized"
);
// Check that caller is ATR token owner
require(
ownerOf(atrTokenId) == msg.sender,
"Caller is not ATR token owner"
);
if (isInvalid[atrTokenId] == false) {
// Check asset balance
require(
asset.balanceOf(msg.sender) >= asset.amount,
"Insufficient balance of a tokenize asset"
);
// Update tokenized balance
require(
_decreaseTokenizedBalance(atrTokenId, msg.sender, asset),
"Tokenized asset is not in a safe"
);
emit TransferViaATR(msg.sender, address(0), atrTokenId, asset);
}
// Clear asset data
_clearTokenizedAsset(atrTokenId);
// Burn ATR token
_burn(atrTokenId);
}
burnAssetTransferRightsTokenBatch
In case a user wants to burn more than one ATR token it is recommended to use this function instead of calling burnAssetTransferRightsToken
multiple times.
This function takes one argument supplied by the caller:
uint256[] calldata
atrTokenIds
- Array of ATR token IDs to burn
function burnAssetTransferRightsTokenBatch(uint256[] calldata atrTokenIds)
external
{
for (uint256 i; i < atrTokenIds.length; ++i) {
burnAssetTransferRightsToken(atrTokenIds[i]);
}
}
claimAssetFrom
Transfer functions are divided into two separate ones to prevent the Stalking Attack.
This function allows a holder of an ATR token to transfer the tokenised asset to the ATR token holder address. The caller can decide if the ATR token should be burned in the process. The caller can be any account if the ATR token is burned. If the ATR token is not burned, the caller has to be a valid PWN Safe.
This function takes three arguments supplied by the caller:
address payable
from
- Address of the PWN Safe, which holds the asset to be transferred
uint256
atrTokenId
- ID of the ATR token corresponding to the asset to be transferred
bool
burnToken
- A flag to decide if the ATR token should be burned during the transfer process
function claimAssetFrom(
address payable from,
uint256 atrTokenId,
bool burnToken
) external {
// Load asset
MultiToken.Asset memory asset = assets[atrTokenId];
_initialChecks(asset, from, msg.sender, atrTokenId);
// Process asset transfer
_processTransferAssetFrom(
asset,
from,
msg.sender,
atrTokenId,
burnToken
);
}
transferAssetFrom
Transfer functions are divided into two separate ones to prevent the Stalking Attack.
This function allows for a transfer of tokenised asset to an arbitrary address. Recipient Permission Manager is used to prevent the Stalking Attack. The permission can be granted on-chain, signed off-chain or via ERC-1271. The caller can also decide if the ATR token should be burned in the process.
This function takes five arguments supplied by the caller:
address payable
from
- Address of the PWN Safe which holds the asset to be transferred
uint256
atrTokenId
- ID of the ATR token corresponding to the asset to be transferred
bool
burnToken
- A flag to decide if the ATR token should be burned during the transfer process
RecipientPermission memory
permission
- Struct representing recipient permission. (see Recipient Permission Struct)
bytes calldata
permissionSignature
- Signature of permission struct hash. Pass empty data in case of on-chain signature or usage of ERC-1271.
function transferAssetFrom(
address payable from,
uint256 atrTokenId,
bool burnToken,
RecipientPermission memory permission,
bytes calldata permissionSignature
) external {
// Load asset
MultiToken.Asset memory asset = assets[atrTokenId];
_initialChecks(asset, from, permission.recipient, atrTokenId);
// Use valid permission
_useValidPermission(msg.sender, asset, permission, permissionSignature);
// Process asset transfer
_processTransferAssetFrom(
asset,
from,
permission.recipient,
atrTokenId,
burnToken
);
}
tokenURI
This function is used to retrieve ATR token metadata URI.
This function takes one argument supplied by the called:
uint256
tokenId
- The new URI to set
function tokenURI(
uint256 tokenId
) public view override returns (string memory) {
_requireMinted(tokenId);
return _metadataUri;
}
setMetadataUri
This function is used to set metadata URI for the ATR token. It can only be called by the owner.
This function takes one argument supplied by the owner:
string memory
metadataUri
- The new URI to set
function setMetadataUri(string memory metadataUri) external onlyOwner {
_metadataUri = metadataUri;
}
_processTransferAssetFrom
Overview
Function to process the actual transfer of assets which have their transfer rights tokenised. It is called only by the claimAssetFrom
and transferAssetFrom
functions.
This function takes five arguments supplied by the ATR Module:
address payable
from
- Address of the PWN Safe which holds the asset to be transferred
uint256
atrTokenId
- ID of the ATR token corresponding to the asset to be transferred
bool
burnToken
- A flag to decide if the ATR token should be burned during the transfer process
RecipientPermission memory
permission
- Struct representing recipient permission. (see Recipient Permission Struct)
bytes calldata
permissionSignature
- Signature of permission struct hash. Pass empty data in case of on-chain signature or usage of ERC-1271.
Implementation
function _processTransferAssetFrom(
MultiToken.Asset memory asset,
address payable from,
address to,
uint256 atrTokenId,
bool burnToken
) private {
// Update tokenized balance (would fail for invalid ATR token)
require(
_decreaseTokenizedBalance(atrTokenId, from, asset),
"Asset is not in a target safe"
);
if (burnToken == true) {
// Burn the ATR token
_clearTokenizedAsset(atrTokenId);
_burn(atrTokenId);
} else {
// Fail if recipient is not PWNSafe
require(
safeValidator.isValidSafe(to) == true,
"Attempting to transfer asset to non PWNSafe address"
);
// Check that recipient doesn't have approvals for the token collection
require(
atrGuard.hasOperatorFor(to, asset.assetAddress) == false,
"Receiver has approvals set for an asset"
);
// Update tokenized balance
_increaseTokenizedBalance(atrTokenId, to, asset);
}
// Transfer asset from `from` safe
bool success = GnosisSafe(from).execTransactionFromModule({
to: asset.assetAddress,
value: 0,
data: asset.transferAssetFromCalldata(from, to, true),
operation: Enum.Operation.Call
});
require(success, "Asset transfer failed");
emit TransferViaATR(
from,
burnToken ? address(0) : to,
atrTokenId,
asset
);
}
The Asset Transfer Rights contract inherits events from Recipient Permission Manager, Tokenized Asset Manager, Ownable and ERC-721 contracts and does not define any additional custom events or errors.