Skip to content

Commit

Permalink
Add wrapper functionality & bridge integration (#281)
Browse files Browse the repository at this point in the history
* add creator token functionality & interfaces

* implement wrapper

* add bridge & wrap txs, baseTokenURI

* call hook from _update (oz v5 removed hooks), add IERC2981 conformance

* add comments

* add bridge permissions, fulfillment

* add bridge tests

* fix ci

* rename param

* update bridge extensions

* add tests, comments

* update readme

* add unwrap, transfer txs

* update readme

* update bridge interface from flow-evm-bridge #168

* add metadata events

* fix readme, tx param

* fix unwrap tx

* update readme

* rename method

* implement IERC2981

* keep event declaration

* update crossvm extensions to final

* address pr comments

* add CrossVMMetadataViews view

* add tx fixes, pr comments

* fix ci

* fix type

* fix permissions updated event

* fix tx

* add IERC4906

* fix isNFTWrapped, royalty tx

* return false if empty

* fix type

* fix tx param

* fix tx

* parameterize address in metdata view

* fix tests

* fix json

* uint256 type

* add log details

* increase gas limit

* rename txs

* add comments
  • Loading branch information
loic1 authored Feb 20, 2025
1 parent 80b179f commit d6c8835
Show file tree
Hide file tree
Showing 40 changed files with 2,156 additions and 2,388 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ flow.json
.vscode
.env
**/vendor/
*.pkey
3 changes: 0 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
export GOFLAGS :=-tags=no_cgo
export CGO_ENABLED := 0

.PHONY: test
test:
$(MAKE) generate -C lib/go
Expand Down
45 changes: 31 additions & 14 deletions contracts/TopShot.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ import NonFungibleToken from 0xNFTADDRESS
import MetadataViews from 0xMETADATAVIEWSADDRESS
import TopShotLocking from 0xTOPSHOTLOCKINGADDRESS
import ViewResolver from 0xVIEWRESOLVERADDRESS
import CrossVMMetadataViews from 0xCROSSVMMETADATAVIEWSADDRESS
import EVM from 0xEVMADDRESS

access(all) contract TopShot: NonFungibleToken {
// -----------------------------------------------------------------------
Expand Down Expand Up @@ -727,6 +729,7 @@ access(all) contract TopShot: NonFungibleToken {
Type<MetadataViews.ExternalURL>(),
Type<MetadataViews.NFTCollectionData>(),
Type<MetadataViews.NFTCollectionDisplay>(),
Type<CrossVMMetadataViews.EVMPointer>(),
Type<MetadataViews.Serial>(),
Type<MetadataViews.Traits>(),
Type<MetadataViews.Medias>()
Expand Down Expand Up @@ -796,6 +799,8 @@ access(all) contract TopShot: NonFungibleToken {
return TopShot.resolveContractView(resourceType: nil, viewType: Type<MetadataViews.NFTCollectionData>())
case Type<MetadataViews.NFTCollectionDisplay>():
return TopShot.resolveContractView(resourceType: nil, viewType: Type<MetadataViews.NFTCollectionDisplay>())
case Type<CrossVMMetadataViews.EVMPointer>():
return TopShot.resolveContractView(resourceType: nil, viewType: Type<CrossVMMetadataViews.EVMPointer>())
case Type<MetadataViews.Traits>():
return self.resolveTraitsView()
case Type<MetadataViews.Medias>():
Expand Down Expand Up @@ -1673,11 +1678,16 @@ access(all) contract TopShot: NonFungibleToken {
// getContractViews returns the metadata view types available for this contract
access(all) view fun getContractViews(resourceType: Type?): [Type] {
return [Type<MetadataViews.NFTCollectionData>(), Type<MetadataViews.NFTCollectionDisplay>(), Type<MetadataViews.Royalties>()]
return [
Type<MetadataViews.NFTCollectionData>(),
Type<MetadataViews.NFTCollectionDisplay>(),
Type<CrossVMMetadataViews.EVMPointer>(),
Type<MetadataViews.Royalties>()
]
}

// resolveContractView resolves this contract's metadata views
access(all) view fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
post {
result == nil || result!.getType() == viewType: "The returned view must be of the given type or nil"
}
Expand Down Expand Up @@ -1717,18 +1727,25 @@ access(all) contract TopShot: NonFungibleToken {
"instagram": MetadataViews.ExternalURL("https://www.instagram.com/nbatopshot")
}
)
case Type<MetadataViews.Royalties>():
let royaltyReceiver: Capability<&{FungibleToken.Receiver}> =
getAccount(TopShot.RoyaltyAddress()).capabilities.get<&{FungibleToken.Receiver}>(MetadataViews.getRoyaltyReceiverPublicPath())!
return MetadataViews.Royalties(
[
MetadataViews.Royalty(
receiver: royaltyReceiver,
cut: 0.05,
description: "NBATopShot marketplace royalty"
)
]
)
case Type<MetadataViews.Royalties>():
let royaltyReceiver: Capability<&{FungibleToken.Receiver}> =
getAccount(TopShot.RoyaltyAddress()).capabilities.get<&{FungibleToken.Receiver}>(MetadataViews.getRoyaltyReceiverPublicPath())!
return MetadataViews.Royalties(
[
MetadataViews.Royalty(
receiver: royaltyReceiver,
cut: 0.05,
description: "NBATopShot marketplace royalty"
)
]
)
case Type<CrossVMMetadataViews.EVMPointer>():
return CrossVMMetadataViews.EVMPointer(
cadenceType: Type<@TopShot.NFT>(),
cadenceContractAddress: self.account.address,
evmContractAddress: EVM.addressFromString(${EVMCONTRACTADDRESS}),
nativeVM: CrossVMMetadataViews.VM.Cadence
)
}
return nil
}
Expand Down
83 changes: 83 additions & 0 deletions contracts/imports/CrossVMMetadataViews.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import ViewResolver from 0xVIEWRESOLVERADDRESS
import EVM from 0xEVMADDRESS

/// This contract implements views originally proposed in FLIP-318 supporting NFT collections
/// with project-defined implementations across both Cadence & EVM.
/// The View structs in this contract should be implemented in the same way that views from `MetadataViews` are implemented
///
access(all) contract CrossVMMetadataViews {

/// An enum denoting a VM. For now, there are only two VMs on Flow, but this enum could be
/// expanded in the event other VMs are supported on the network.
///
access(all) enum VM : UInt8 {
access(all) case Cadence
access(all) case EVM
}

/// View resolved at contract & resource level pointing to the associated EVM implementation.
/// NOTE: This view alone is not sufficient to validate an association across Cadence & EVM!
/// Both the Cadence Type/contract *and* the EVM contract should point to each other, with the
/// EVM pointer being facilitated by ICrossVM.sol contract interface methods. For more
/// information and context, see FLIP-318: https://github.com/onflow/flips/issues/318
///
access(all) struct EVMPointer {
/// The associated Cadence Type defined in the contract that this view is returned from
access(all) let cadenceType: Type
/// The defining Cadence contract address
access(all) let cadenceContractAddress: Address
/// The associated EVM contract address that the Cadence contract will bridge to
access(all) let evmContractAddress: EVM.EVMAddress
/// Whether the asset is Cadence- or EVM-native. Native here meaning the VM in which the
/// asset is originally distributed.
access(all) let nativeVM: VM

init(
cadenceType: Type,
cadenceContractAddress: Address,
evmContractAddress: EVM.EVMAddress,
nativeVM: VM
) {
self.cadenceType = cadenceType
self.cadenceContractAddress = cadenceContractAddress
self.evmContractAddress = evmContractAddress
self.nativeVM = nativeVM
}
}

access(all) fun getEVMPointer(_ viewResolver: &{ViewResolver.Resolver}): EVMPointer? {
if let view = viewResolver.resolveView(Type<EVMPointer>()) {
if let v = view as? EVMPointer {
return v
}
}
return nil
}

/// View resolved at resource level denoting any metadata to be passed to the associated EVM
/// contract at the time of bridging. This optional view would allow EVM side metadata to be
/// updated based on current Cadence state. If the view is not supported, no bytes will be
/// passed into EVM when bridging.
///
access(all) struct EVMBytesMetadata {
/// Returns the bytes to be passed to the EVM contract on `fulfillToEVM` call, allowing the
/// EVM contract to update the metadata associated with the NFT. The corresponding Solidity
/// `bytes` type allows the implementer greater flexibility by enabling them to pass
/// arbitrary data between VMs.
access(all) let bytes: EVM.EVMBytes

init(bytes: EVM.EVMBytes) {
self.bytes = bytes
}
}

access(all) fun getEVMBytesMetadata(_ viewResolver: &{ViewResolver.Resolver}): EVMBytesMetadata? {
if let view = viewResolver.resolveView(Type<EVMBytesMetadata>()) {
if let v = view as? EVMBytesMetadata {
return v
}
}
return nil
}

}
119 changes: 90 additions & 29 deletions evm-bridging/README.md
Original file line number Diff line number Diff line change
@@ -1,76 +1,136 @@
# <h1 align="center"> NBA TopShot on FlowEVM [Initial Draft Version] </h1>

**! This directory currently contains work in progress only !**
# <h1 align="center"> NBA TopShot on FlowEVM </h1>

## Introduction

The `BridgedTopShotMoments` smart contract facilitates the creation of 1:1 ERC721 references for existing Cadence-native NBA Top Shot moments. By associating these references with the same metadata, it ensures seamless integration and interaction between Cadence and FlowEVM environments. This allows users to enjoy the benefits of both ecosystems while maintaining the integrity and uniqueness of their NBA Top Shot moments.
The `BridgedTopShotMoments` smart contract enables NBA Top Shot moments to exist on FlowEVM as ERC721 tokens. Each ERC721 token is a 1:1 reference to a Cadence-native NBA Top Shot moment, maintaining the same metadata and uniqueness while allowing users to leverage both Flow and EVM ecosystems.

### Core Features

1. **ERC721 Implementation**
- Full ERC721 compliance with enumeration and burning capabilities
- NFT metadata support with customizable base URI
- Ownable contract for admin operations
- Upgradeable via UUPS proxy

2. **Bridge Integration**
- Wrapper functionality for ERC721s from bridged-deployed contract
- Cross-VM compatibility for Flow ↔ EVM bridging (after [FLIP-318](https://github.com/onflow/flips/pull/319) implementation allowing custom associations, and after contract is onboarded to the bridge)
- Fulfillment of ERC721s from Flow to EVM
- Bridge permissions management
- Cadence-specific identifiers tracking

3. **Royalty Management**
- ERC2981 royalty standard implementation
- Transfer validation for royalty enforcement via ERC721C/Token Creator Standard
- Configurable royalty rates (in basis points)
- Updatable royalty receiver address

## Getting Started
> **Note**: This contract is under active development. Features and implementations may change.
Install Foundry:

## Prerequisites

1. Install Foundry:

```sh
curl -L https://foundry.paradigm.xyz | bash
foundryup
```

Compile contracts and run tests:
2. Install Flow CLI: [Instructions](https://developers.flow.com/tools/flow-cli/install)

## Development

1. Compile and test contracts:

```sh
forge test --force -vvv
```

### Deploy & Verify Contracts
2. Set up environment:

Load environment variables after populating address and key details:

```sh
cp .env.example.testnet .env
cp .env.flowevm.testnet.example .env
# Add your account details to .env and source it
source .env
```

Run script to deploy and verify contracts (proxy and implementation):
3. Deploy and verify contracts:

```sh
# Deploy both proxy and implementation contracts
forge script --rpc-url $RPC_URL --private-key $DEPLOYER_PRIVATE_KEY --legacy script/Deploy.s.sol:DeployScript --broadcast --verify --verifier $VERIFIER_PROVIDER --verifier-url $VERIFIER_URL
```

If verification fails for one or both contracts, verify separately:

```sh
# If verification fails, verify individually
forge verify-contract --rpc-url $RPC_URL --verifier $VERIFIER_PROVIDER --verifier-url $VERIFIER_URL <address-of-contract-to-verify>
```

## Run Transactions
## Usage

Set NFT symbol (admin):
### EVM Operations

```sh
cast send $DEPLOYED_PROXY_CONTRACT_ADDRESS --rpc-url $RPC_URL --private-key $DEPLOYER_PRIVATE_KEY --legacy "setSymbol(string)" <new-nft-symbol>
```
# Approve operator for a NFT
cast send $DEPLOYED_PROXY_CONTRACT_ADDRESS --rpc-url $RPC_URL --private-key <private-key> --legacy "approve(address,uint256)" <operator-address> <token-id>

## Execute Queries
# Approve operator for all NFTs
cast send $DEPLOYED_PROXY_CONTRACT_ADDRESS --rpc-url $RPC_URL --private-key <private-key> --legacy "setApprovalForAll(address,bool)" <operator-address> <true>

BalanceOf:
```sh
# Transfer NFT
cast send $DEPLOYED_PROXY_CONTRACT_ADDRESS --rpc-url $RPC_URL --private-key <private-key> --legacy "safeTransferFrom(address,address,uint256)" <from-address> <to-address> <token-id>

# Query balance
cast call $DEPLOYED_PROXY_CONTRACT_ADDRESS --rpc-url $RPC_URL "balanceOf(address)(uint256)" $DEPLOYER_ADDRESS
```

OwnerOf:
```sh
# Query owner
cast call $DEPLOYED_PROXY_CONTRACT_ADDRESS --rpc-url $RPC_URL "ownerOf(uint256)(address)" <nft-id>

# Query token URI
cast call $DEPLOYED_PROXY_CONTRACT_ADDRESS --rpc-url $RPC_URL "tokenURI(uint256)(string)" <nft-id>

# Set NFT symbol (admin only)
cast send $DEPLOYED_PROXY_CONTRACT_ADDRESS --rpc-url $RPC_URL --private-key $DEPLOYER_PRIVATE_KEY --legacy "setSymbol(string)" <new-nft-symbol>

# Set transfer validator (admin only)
cast send $DEPLOYED_PROXY_CONTRACT_ADDRESS --rpc-url $RPC_URL --private-key $DEPLOYER_PRIVATE_KEY --legacy "setTransferValidator(address)" <validator-address>

# Set royalty info (admin only)
cast send $DEPLOYED_PROXY_CONTRACT_ADDRESS --rpc-url $RPC_URL --private-key $DEPLOYER_PRIVATE_KEY --legacy "setRoyaltyInfo((address,uint96))" "(<royalty-receiver-address>,<royalty-basis-points>)"
```

## Misc
### Cadence Operations

#### Notes

- Ensure all transaction arguments are populated in the corresponding JSON file template before submission
- If you encounter an `insufficient computation` error, increase the gas limit (i.e., `--gas-limit <new-gas-limit>`)

```sh
# Transfer erc721 NFTs
flow transactions send ./evm-bridging/cadence/transactions/transfer_erc721s_to_evm_address.cdc --args-json "$(cat ./evm-bridging/cadence/transactions/transfer_erc721s_to_evm_address_args.json)" --network <network> --signer <signer>

# Bridge NFTs to EVM (wraps NFTs if applicable)
flow transactions send ./evm-bridging/cadence/transactions/bridge_nfts_to_evm.cdc --args-json "$(cat ./evm-bridging/cadence/transactions/bridge_nfts_to_evm_args.json)" --network <network> --signer <signer> --gas-limit 8000

# Bridge NFTs from EVM (unwraps NFTs if applicable)
flow transactions send ./evm-bridging/cadence/transactions/bridge_nfts_from_evm.cdc --args-json "$(cat ./evm-bridging/cadence/transactions/bridge_nfts_from_evm_args.json)" --network <network> --signer <signer> --gas-limit 8000

# Query ERC721 address
flow scripts execute ./evm-bridging/cadence/scripts/get_underlying_erc721_address.cdc <nft_contract_flow_address> <nft_contract_evm_address> --network testnet

# Set up royalty management (admin only)
flow transactions send ./evm-bridging/cadence/transactions/admin/set_up_royalty_management.cdc --args-json "$(cat ./evm-bridging/cadence/transactions/admin/set_up_royalty_management_args.json)" --network <network> --signer <signer>
```

Fund testnet Flow EVM account:
### Testnet Setup

1. Use Flow Faucet: https://faucet.flow.com/fund-account
1. Get testnet FLOW from [Flow Faucet](https://faucet.flow.com/fund-account)

2. Transfer FLOW to EVM address:

```sh
flow transactions send ./cadence/transfer_flow_to_evm_address.cdc <evm_address_hex> <ufix64_amount> --network testnet --signer testnet-account
flow transactions send ./evm-bridging/cadence/transactions/transfer_flow_to_evm_address.cdc <evm_address_hex> <ufix64_amount> --network testnet --signer testnet-account
```

## Useful links
Expand All @@ -83,3 +143,4 @@ flow transactions send ./cadence/transfer_flow_to_evm_address.cdc <evm_address_h
- [OpenZeppelin Doc - ERC721 Contracts v5](https://docs.openzeppelin.com/contracts/5.x/api/token/erc721)
- [GitHub - OpenZeppelin Upgradeable Contracts](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable)
- [GitHub - LimitBreak Creator Token Standards](https://github.com/limitbreakinc/creator-token-standards)
- [OpenSea Doc - Creator Fee Enforcement](https://docs.opensea.io/docs/creator-fee-enforcement)
11 changes: 11 additions & 0 deletions evm-bridging/cadence/scripts/get_evm_address_string.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import "EVM"

/// Returns the hex encoded address of the COA in the given Flow address
///
access(all) fun main(flowAddress: Address): String? {
return getAuthAccount<auth(BorrowValue) &Account>(flowAddress)
.storage.borrow<&EVM.CadenceOwnedAccount>(from: /storage/evm)
?.address()
?.toString()
?? nil
}
33 changes: 33 additions & 0 deletions evm-bridging/cadence/scripts/get_underlying_erc721_address.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import "EVM"

/// Returns the hex encoded address of the underlying ERC721 contract
///
access(all) fun main(flowNftAddress: Address, wrapperERC721Address: String): String? {
let coa = getAuthAccount<auth(BorrowValue) &Account>(flowNftAddress)
.storage.borrow<auth(EVM.Call) &EVM.CadenceOwnedAccount>(from: /storage/evm)
?? panic("No COA found in signer's account")

return getUnderlyingERC721Address(coa,
EVM.addressFromString(wrapperERC721Address)
).toString()
}

/// Gets the underlying ERC721 address
///
access(all) fun getUnderlyingERC721Address(
_ coa: auth(EVM.Call) &EVM.CadenceOwnedAccount,
_ wrapperAddress: EVM.EVMAddress
): EVM.EVMAddress {
let res = coa.call(
to: wrapperAddress,
data: EVM.encodeABIWithSignature("underlying()", []),
gasLimit: 100_000,
value: EVM.Balance(attoflow: 0)
)

assert(res.status == EVM.Status.successful, message: "Call to get underlying ERC721 address failed")
let decodedResult = EVM.decodeABI(types: [Type<EVM.EVMAddress>()], data: res.data)
assert(decodedResult.length == 1, message: "Invalid response length")

return decodedResult[0] as! EVM.EVMAddress
}
Loading

0 comments on commit d6c8835

Please sign in to comment.