Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add wrapper functionality & bridge integration #281

Merged
merged 47 commits into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
8116aae
add creator token functionality & interfaces
loic1 Feb 7, 2025
2bedd14
implement wrapper
loic1 Feb 7, 2025
6173a79
add bridge & wrap txs, baseTokenURI
loic1 Feb 9, 2025
bf68366
call hook from _update (oz v5 removed hooks), add IERC2981 conformance
loic1 Feb 10, 2025
6169b18
add comments
loic1 Feb 10, 2025
8b98082
add bridge permissions, fulfillment
loic1 Feb 10, 2025
916007f
add bridge tests
loic1 Feb 10, 2025
3b11214
Merge branch 'loic/add-creator-token-implementation' into loic/implem…
loic1 Feb 10, 2025
dee93eb
fix ci
loic1 Feb 10, 2025
f612e73
rename param
loic1 Feb 10, 2025
a29f2ab
update bridge extensions
loic1 Feb 11, 2025
a2d7e50
add tests, comments
loic1 Feb 11, 2025
3a84536
update readme
loic1 Feb 11, 2025
2647f75
add unwrap, transfer txs
loic1 Feb 11, 2025
f1b9961
update readme
loic1 Feb 11, 2025
fb3e123
update bridge interface from flow-evm-bridge #168
loic1 Feb 11, 2025
e0754c2
add metadata events
loic1 Feb 12, 2025
329b8c8
fix readme, tx param
loic1 Feb 12, 2025
7305d64
fix unwrap tx
loic1 Feb 12, 2025
f04742a
update readme
loic1 Feb 12, 2025
5c0cc01
rename method
loic1 Feb 12, 2025
86b876c
implement IERC2981
loic1 Feb 12, 2025
73bded9
keep event declaration
loic1 Feb 12, 2025
5e8f8de
Merge branch 'loic/add-creator-token-implementation' into loic/implem…
loic1 Feb 12, 2025
1e70a1d
update crossvm extensions to final
loic1 Feb 12, 2025
25367b1
address pr comments
loic1 Feb 14, 2025
0f5633b
add CrossVMMetadataViews view
loic1 Feb 16, 2025
5c2ffd6
add tx fixes, pr comments
loic1 Feb 16, 2025
d1c4be9
fix ci
loic1 Feb 16, 2025
b158d18
fix type
loic1 Feb 17, 2025
b2ba91a
Merge branch 'evm-bridging' into loic/implement-wrapper-functionality
loic1 Feb 17, 2025
9c6a36c
fix permissions updated event
loic1 Feb 18, 2025
8f626e1
fix tx
loic1 Feb 18, 2025
9a39230
add IERC4906
loic1 Feb 18, 2025
ec30197
fix isNFTWrapped, royalty tx
loic1 Feb 18, 2025
03e4d86
return false if empty
loic1 Feb 18, 2025
56fab2c
fix type
loic1 Feb 19, 2025
986f4ed
fix tx param
loic1 Feb 19, 2025
db51016
fix tx
loic1 Feb 19, 2025
bea276f
parameterize address in metdata view
loic1 Feb 19, 2025
f6a88e6
fix tests
loic1 Feb 19, 2025
83e8380
fix json
loic1 Feb 20, 2025
e2a0c9a
uint256 type
loic1 Feb 20, 2025
cf9b9b9
add log details
loic1 Feb 20, 2025
f0bbd4c
increase gas limit
loic1 Feb 20, 2025
c921ee3
rename txs
loic1 Feb 20, 2025
0b18873
add comments
loic1 Feb 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
46 changes: 32 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,26 @@ 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,
// TODO: Replace with actual EVM contract address
evmContractAddress: EVM.addressFromString("0x1234565789012345657890123456578901234565"),
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
}

}
122 changes: 93 additions & 29 deletions evm-bridging/README.md
Original file line number Diff line number Diff line change
@@ -1,76 +1,139 @@
# <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

> **Note**: Populate arguments in json file before submitting the transactions.

```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 and wrap NFTs
flow transactions send ./evm-bridging/cadence/transactions/bridge_nfts_to_evm_and_wrap.cdc --args-json "$(cat ./evm-bridging/cadence/transactions/bridge_nfts_to_evm_and_wrap_args.json)" --network <network> --signer <signer> --gas-limit 8000

# Wrap already-bridged NFTs
flow transactions send ./evm-bridging/cadence/transactions/wrap_nfts.cdc --args-json "$(cat ./evm-bridging/cadence/transactions/wrap_nfts_args.json)" --network <network> --signer <signer>

# Unwrap and bridge back NFTs
flow transactions send ./evm-bridging/cadence/transactions/unwrap_nfts_and_bridge_from_evm.cdc --args-json "$(cat ./evm-bridging/cadence/transactions/unwrap_nfts_and_bridge_from_evm_args.json)" --network <network> --signer <signer> --gas-limit 8000

# Unwrap NFTs
flow transactions send ./evm-bridging/cadence/transactions/unwrap_nfts.cdc --args-json "$(cat ./evm-bridging/cadence/transactions/unwrap_nfts_args.json)" --network <network> --signer <signer>

# 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 +146,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